# HG changeset patch # User Matthew Wild # Date 1631557453 -3600 # Node ID e4e5474420e6ccb5e6ea5c22088bdb23ddd33ce0 # Parent edbd84bbd8fb30ebc0ed46a93bc1b9ce6d2f65fc mod_debug_omemo: OMEMO debugging tool diff -r edbd84bbd8fb -r e4e5474420e6 mod_debug_omemo/README.markdown --- /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 + ----- ------- diff -r edbd84bbd8fb -r e4e5474420e6 mod_debug_omemo/mod_debug_omemo.lua --- /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; + }; +}); diff -r edbd84bbd8fb -r e4e5474420e6 mod_debug_omemo/view.tpl.html --- /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 @@ + + + + + + +
+

OMEMO encryption information

+

OMEMO is an end-to-end encryption technology that protects communication between + users on the XMPP network. Find out more information about OMEMO + and a list of OMEMO-capable software. +

+ +

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:

+ + +

Advanced information

+

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. +

+ +

Message status

+ +

This was an {message.encryption} {message.direction} {message.type} message. The sending device id was {omemo.sender_id}.

+ +

Recipient devices

+ + + + + + + {rids% + + + + } +
Device IDStatusComment
{idx}{item.status?Unknown device} {item.prekey&Used pre-key}{item.comment?}
+ +

Account status

+

{user}'s account has {omemo.status} with OMEMO.

+ +

Registered OMEMO devices

+ + + + + + + + {omemo.devices% + + + + + } +
Device IDStatusBundleAccess
{idx}{item.status}{item.bundle}{item.access_model}
+
+ +