--- /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>