Software /
code /
prosody-modules
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>