Changeset

4682:e4e5474420e6

mod_debug_omemo: OMEMO debugging tool
author Matthew Wild <mwild1@gmail.com>
date Mon, 13 Sep 2021 19:24:13 +0100
parents 4681:edbd84bbd8fb
children 4683:d3080767ead3
files mod_debug_omemo/README.markdown mod_debug_omemo/mod_debug_omemo.lua mod_debug_omemo/view.tpl.html
diffstat 3 files changed, 478 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_debug_omemo/README.markdown	Mon Sep 13 19:24:13 2021 +0100
@@ -0,0 +1,33 @@
+---
+summary: "Generate OMEMO debugging links"
+labels:
+- 'Stage-Alpha'
+...
+
+Introduction
+============
+
+This module allows you to view advanced information about OMEMO-encrypted messages,
+and can be helpful to diagnose decryption problems.
+
+It generates a link to itself and adds this link to the plaintext contents of
+encrypted messages. This will be shown by clients that do not support OMEMO,
+or are unable to decrypt the message.
+
+This module depends on a working HTTP setup in Prosody, and honours the [usual
+HTTP configuration options](https://prosody.im/doc/http).
+
+Configuration
+=============
+
+There is no configuration for this module, just add it to
+modules\_enabled as normal.
+
+Compatibility
+=============
+
+  ----- -------
+  0.11  Hopefully works
+  ----- -------
+  trunk Works
+  ----- -------
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_debug_omemo/mod_debug_omemo.lua	Mon Sep 13 19:24:13 2021 +0100
@@ -0,0 +1,223 @@
+local array = require "util.array";
+local jid = require "util.jid";
+local set = require "util.set";
+local st = require "util.stanza";
+local url_escape = require "util.http".urlencode;
+
+local base_url = "https://"..module.host.."/";
+
+local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, {
+	urlescape = url_escape;
+	lower = string.lower;
+	classname = function (s) return (s:gsub("%W+", "-")); end;
+	relurl = function (s)
+		if s:match("^%w+://") then
+			return s;
+		end
+		return base_url.."/"..s;
+	end;
+});
+local render_url = require "util.interpolation".new("%b{}", url_escape, {
+	urlescape = url_escape;
+	noscheme = function (url)
+		return (url:gsub("^[^:]+:", ""));
+	end;
+});
+
+local mod_pep = module:depends("pep");
+
+local mam = module:open_store("archive", "archive");
+
+local function get_user_omemo_info(username)
+	local everything_valid = true;
+	local any_device = false;
+	local omemo_status = {};
+	local omemo_devices;
+	local pep_service = mod_pep.get_pep_service(username);
+	if pep_service and pep_service.nodes then
+		local ok, _, device_list = pep_service:get_last_item("eu.siacs.conversations.axolotl.devicelist", true);
+		if ok and device_list then
+			device_list = device_list:get_child("list", "eu.siacs.conversations.axolotl");
+		end
+		if device_list then
+			omemo_devices = {};
+			for device_entry in device_list:childtags("device") do
+				any_device = true;
+				local device_info = {};
+				local device_id = tonumber(device_entry.attr.id or "");
+				if device_id then
+					device_info.id = device_id;
+					local bundle_id = ("eu.siacs.conversations.axolotl.bundles:%d"):format(device_id);
+					local have_bundle, _, bundle = pep_service:get_last_item(bundle_id, true);
+					if have_bundle and bundle and bundle:get_child("bundle", "eu.siacs.conversations.axolotl") then
+						device_info.have_bundle = true;
+						local config_ok, bundle_config = pep_service:get_node_config(bundle_id, true);
+						if config_ok and bundle_config then
+							device_info.bundle_config = bundle_config;
+							if bundle_config.max_items == 1
+							and bundle_config.access_model == "open"
+							and bundle_config.persist_items == true
+							and bundle_config.publish_model == "publishers" then
+								device_info.valid = true;
+							end
+						end
+					end
+				end
+				if device_info.valid == nil then
+					device_info.valid = false;
+					everything_valid = false;
+				end
+				table.insert(omemo_devices, device_info);
+			end
+
+			local config_ok, list_config = pep_service:get_node_config("eu.siacs.conversations.axolotl.devicelist", true);
+			if config_ok and list_config then
+				omemo_status.config = list_config;
+				if list_config.max_items == 1
+				and list_config.access_model == "open"
+				and list_config.persist_items == true
+				and list_config.publish_model == "publishers" then
+					omemo_status.config_valid = true;
+				end
+			end
+			if omemo_status.config_valid == nil then
+				omemo_status.config_valid = false;
+				everything_valid = false;
+			end
+		end
+	end
+	omemo_status.valid = everything_valid and any_device;
+	return {
+		status = omemo_status;
+		devices = omemo_devices;
+	};
+end
+
+local access_model_text = {
+	open = "Public";
+	whitelist = "Private";
+	roster = "Contacts only";
+	presence = "Contacts only";
+};
+
+local function render_message(event, path)
+	local username, message_id = path:match("^([^/]+)/(.+)$");
+	if not username then
+		return 400;
+	end
+	local message;
+	for _, result in mam:find(username, { key = message_id }) do
+		message = result;
+	end
+	if not message then
+		return 404;
+	end
+
+	local user_omemo_status = get_user_omemo_info(username);
+
+	local user_rids = set.new(array.pluck(user_omemo_status.devices or {}, "id")) / tostring;
+
+	local message_omemo_header = message:find("{eu.siacs.conversations.axolotl}encrypted/header");
+	local message_rids = set.new();
+	local rid_info = {};
+	if message_omemo_header then
+		for key_el in message_omemo_header:childtags("key") do
+			local rid = key_el.attr.rid;
+			if rid then
+				message_rids:add(rid);
+				local prekey = key_el.attr.prekey;
+				rid_info = {
+					prekey = prekey and (prekey == "1" or prekey:lower() == "true");
+				};
+			end
+		end
+	end
+
+	local rids = user_rids + message_rids;
+
+	local direction = jid.bare(message.attr.to) == (username.."@"..module.host) and "incoming" or "outgoing";
+
+	local is_encrypted = not not message_omemo_header;
+
+	local sender_id = message_omemo_header and message_omemo_header.attr.sid or nil;
+
+	local f = module:load_resource("view.tpl.html");
+	if not f then
+		return 500;
+	end
+	local tpl = f:read("*a");
+
+	local data = { user = username, rids = {} };
+	for rid in rids do
+		data.rids[rid] = {
+			status = message_rids:contains(rid) and "Encrypted" or user_rids:contains(rid) and "Missing" or nil;
+			prekey = rid_info.prekey;
+		};
+	end
+
+	data.message = {
+		type = message.attr.type or "normal";
+		direction = direction;
+		encryption = is_encrypted and "encrypted" or "unencrypted";
+	};
+
+	data.omemo = {
+		sender_id = sender_id;
+		status = user_omemo_status.status.valid and "no known issues" or "problems";
+	};
+
+	data.omemo.devices = {};
+	for _, device_info in ipairs(user_omemo_status.devices) do
+		data.omemo.devices[("%d"):format(device_info.id)] = {
+			status = device_info.valid and "OK" or "Problem";
+			bundle = device_info.have_bundle and "Published" or "Missing";
+			access_model = access_model_text[device_info.bundle_config and device_info.bundle_config.access_model or nil];
+		};
+	end
+
+	event.response.headers.content_type = "text/html; charset=utf-8";
+	return render_html_template(tpl, data);
+end
+
+local function check_omemo_fallback(event)
+	local message = event.stanza;
+
+	local message_omemo_header = message:find("{eu.siacs.conversations.axolotl}encrypted/header");
+	if not message_omemo_header then return; end
+
+	local to_bare = jid.bare(message.attr.to);
+
+	local archive_stanza_id;
+	for stanza_id_tag in message:childtags("stanza-id", "urn:xmpp:sid:0") do
+		if stanza_id_tag.attr.by == to_bare then
+			archive_stanza_id = stanza_id_tag.attr.id;
+		end
+	end
+	if not archive_stanza_id then
+		return;
+	end
+
+	local debug_url = render_url(module:http_url().."/view/{username}/{message_id}", {
+		username = jid.node(to_bare);
+		message_id = archive_stanza_id;
+	});
+
+	local body = message:get_child("body");
+	if not body then
+		body = st.stanza("body")
+			:text("This message is encrypted using OMEMO, but could not be decrypted by your device.\nFor more information see: "..debug_url);
+		message:reset():add_child(body);
+	else
+		body:text("\n\nOMEMO debug information: "..debug_url);
+	end
+end
+
+module:hook("message/bare", check_omemo_fallback, 1);
+module:hook("message/full", check_omemo_fallback, 1);
+
+module:depends("http")
+module:provides("http", {
+	route = {
+		["GET /view/*"] = render_message;
+	};
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_debug_omemo/view.tpl.html	Mon Sep 13 19:24:13 2021 +0100
@@ -0,0 +1,222 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+/*
+
+MIT License
+
+Copyright (c) 2020 Simple.css (Kev Quirk)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+*/
+
+:root {
+  --sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, "Nimbus Sans L", Roboto, Noto, "Segoe UI", Arial, Helvetica, "Helvetica Neue", sans-serif;
+  --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
+
+  --base-fontsize: 1.15rem;
+
+  --header-scale: 1.25;
+
+  --line-height: 1.618;
+
+  /* Default (light) theme */
+  --bg: #FFF;
+  --accent-bg: #F5F7FF;
+  --text: #212121;
+  --text-light: #585858;
+  --border: #D8DAE1;
+  --accent: #0D47A1;
+  --accent-light: #90CAF9;
+  --code: #D81B60;
+  --preformatted: #444;
+  --marked: #FFDD33;
+  --disabled: #EFEFEF;
+}
+
+/* Dark theme */
+@media (prefers-color-scheme: dark) {
+  :root {
+    --bg: #212121;
+    --accent-bg: #2B2B2B;
+    --text: #DCDCDC;
+    --text-light: #ABABAB;
+    --border: #666;
+    --accent: #FFB300;
+    --accent-light: #FFECB3;
+    --code: #F06292;
+    --preformatted: #CCC;
+    --disabled: #111;
+  }
+
+  img, video {
+    opacity: .6;
+  }
+}
+
+html {
+  /* Set the font globally */
+  font-family: var(--sans-font);
+}
+
+/* Make the body a nice central block */
+body {
+  color: var(--text);
+  background: var(--bg);
+  font-size: var(--base-fontsize);
+  line-height: var(--line-height);
+  display: flex;
+  min-height: 100vh;
+  flex-direction: column;
+  flex: 1;
+  margin: 0 auto;
+  max-width: 45rem;
+  padding: 0 .5rem;
+  overflow-x: hidden;
+  word-break: break-word;
+  overflow-wrap: break-word;
+}
+
+/* Fix line height when title wraps */
+h1, h2, h3 {
+  line-height: 1.1;
+}
+
+/* Format headers */
+h1 {
+  font-size: calc(var(--base-fontsize) * var(--header-scale) * var(--header-scale) * var(--header-scale) * var(--header-scale));
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+h2 {
+  font-size: calc(var(--base-fontsize) * var(--header-scale) * var(--header-scale) * var(--header-scale));
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+h3 {
+  font-size: calc(var(--base-fontsize) * var(--header-scale) * var(--header-scale));
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+h4 {
+  font-size: calc(var(--base-fontsize) * var(--header-scale));
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+h5 {
+  font-size: var(--base-fontsize);
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+h6 {
+  font-size: calc(var(--base-fontsize) / var(--header-scale));
+  margin-top: calc(var(--line-height) * 1.5rem);
+}
+
+/* Format links & buttons */
+a,
+a:visited {
+  color: var(--accent);
+}
+
+a:hover {
+  text-decoration: none;
+}
+
+/* Format tables */
+table {
+  border-collapse: collapse;
+  width: 100%;
+  margin: 1.5rem 0;
+}
+
+td,
+th {
+  border: 1px solid var(--border);
+  text-align: left;
+  padding: .5rem;
+}
+
+th {
+  background: var(--accent-bg);
+  font-weight: bold;
+}
+
+tr:nth-child(even) {
+  background: var(--accent-bg);
+}
+
+/* Lists */
+ol, ul {
+  padding-left: 3rem;
+}
+</style>
+</head>
+<body>
+<div class="container">
+	<h1>OMEMO encryption information</h1>
+	<p>OMEMO is an end-to-end encryption technology that protects communication between
+	   users on the XMPP network. Find out more information <a href="https://conversations.im/omemo/">about OMEMO</a>
+	   and <a href="https://omemo.top/">a list of OMEMO-capable software</a>.
+	</p>
+
+	<p>If you are on this page, it may mean that you received an encrypted message that your client could not decrypt.
+	   Some possible causes of this problem are:</p>
+	<ul>
+	  <li>Your XMPP client does not support OMEMO, or does not have it enabled.</li>
+	  <li>Your server software is too old (Prosody 0.11.x is recommended) or misconfigured.</li>
+	  <li>The sender's client, or your client, has a bug in its OMEMO support.</li>
+	</ul>
+	
+	<h2>Advanced information</h2>
+	<p>Here you can find some advanced information that may be useful
+	   when debugging why an OMEMO message could not be decrypted. You may
+	   share this page privately with XMPP developers to help them
+	   diagnose your problem.
+	</p>
+
+	<h3>Message status</h3>
+
+	<p>This was an {message.encryption} {message.direction} {message.type} message. The sending device id was <tt>{omemo.sender_id}</tt>.</p>
+
+	<h4>Recipient devices</h4>
+	<table class="table">
+	<tr>
+		<th>Device ID</th>
+		<th>Status</th>
+		<th>Comment</th>
+	</tr>
+	{rids%<tr>
+		<td>{idx}</td>
+		<td>{item.status?Unknown device} {item.prekey&<span class="badge badge-warning">Used pre-key</span>}</td>
+		<td>{item.comment?}</td>
+	</tr>}
+	</table>
+
+	<h2>Account status</h2>
+	<p>{user}'s account has {omemo.status} with OMEMO.</p>
+
+	<h4>Registered OMEMO devices</h4>
+	<table class="table">
+		<tr>
+			<th>Device ID</th>
+			<th>Status</th>
+			<th>Bundle</th>
+			<th>Access</th>
+		</tr>
+		{omemo.devices%<tr>
+			<td>{idx}</td>
+			<td>{item.status}</td>
+			<td>{item.bundle}</td>
+			<td>{item.access_model}</td>
+		</tr>}
+	</table>
+</div>
+</body>
+</html>