@wacando/agent 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/STATUS.md +3 -2
- package/THIRD-PARTY-NOTICES.txt +0 -1113
- package/docs/AGENT_TOOLS.md +5 -4
- package/docs/MIGRATION-2.0.md +3 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PLUGIN_HANDOFF_FOLLOWUP.md +1 -1
- package/docs/RELEASE.md +10 -6
- package/docs/WACANDO_PACKAGES.md +70 -22
- package/package.json +19 -7
- package/packages/onboarding/version.mjs +1 -1
- package/packages/runtime/conversations.mjs +3 -3
- package/packages/runtime/device.mjs +1 -1
- package/packages/runtime/recovery.mjs +5 -4
- package/packages/runtime/session.mjs +1 -1
- package/packages/wacando/chunks/chunk-EIGFLZ2Y.mjs +23 -0
- package/packages/wacando/chunks/chunk-GASYW3SG.mjs +422 -0
- package/packages/wacando/chunks/chunk-OI3RF7TS.mjs +421 -0
- package/packages/wacando/omemo-wire.mjs +192 -0
- package/packages/wacando/omemo.mjs +2488 -0
- package/packages/wacando/xmpp.mjs +48 -0
- package/plugins/wacando/.codex-plugin/plugin.json +1 -1
- package/plugins/wacando-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/wacando-cursor/.cursor-plugin/plugin.json +1 -1
- package/provenance.json +126 -266
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Embedded from lbframe/wacando@9f82881d4ee6cfd655727a08883877230917c955 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
|
+
import {
|
|
3
|
+
DomainError
|
|
4
|
+
} from "./chunk-EIGFLZ2Y.mjs";
|
|
5
|
+
|
|
6
|
+
// packages/omemo/src/ns.ts
|
|
7
|
+
var NS_OMEMO = "eu.siacs.conversations.axolotl";
|
|
8
|
+
var NS_DEVICELIST = `${NS_OMEMO}.devicelist`;
|
|
9
|
+
var NS_EME = "urn:xmpp:eme:0";
|
|
10
|
+
var NS_HINTS = "urn:xmpp:hints";
|
|
11
|
+
var NS_SCE = "urn:xmpp:sce:1";
|
|
12
|
+
var NS_WACANDO_OMEMO = "urn:wacando:omemo:1";
|
|
13
|
+
function bundleNode(deviceId) {
|
|
14
|
+
return `${NS_OMEMO}.bundles:${deviceId}`;
|
|
15
|
+
}
|
|
16
|
+
var PEP_ITEM_ID = "current";
|
|
17
|
+
var BODY_MARKER = "WACANDO_OMEMO_V1";
|
|
18
|
+
var MESSAGE_KEY_BYTES = 32;
|
|
19
|
+
var GCM_IV_BYTES = 12;
|
|
20
|
+
var GCM_TAG_BYTES = 16;
|
|
21
|
+
|
|
22
|
+
// packages/omemo/src/errors.ts
|
|
23
|
+
var OmemoError = class extends DomainError {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super("SECURE_MESSAGING_UNAVAILABLE", message);
|
|
26
|
+
this.name = "OmemoError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var UnsupportedSignalFormatError = class extends OmemoError {
|
|
30
|
+
};
|
|
31
|
+
var OmemoReceiveError = class extends OmemoError {
|
|
32
|
+
failure;
|
|
33
|
+
constructor(message, failure) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.failure = failure;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// packages/omemo/src/sce.ts
|
|
40
|
+
import { escapeXMLText } from "@xmpp/xml";
|
|
41
|
+
import ltxParse from "ltx/lib/parse.js";
|
|
42
|
+
function buildSceEnvelope(bodyText) {
|
|
43
|
+
return `<content xmlns="${NS_SCE}"><payload><body xmlns="jabber:client">${escapeXMLText(bodyText)}</body></payload></content>`;
|
|
44
|
+
}
|
|
45
|
+
function parseXmlDocument(xmlText) {
|
|
46
|
+
let root;
|
|
47
|
+
try {
|
|
48
|
+
root = ltxParse(xmlText);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new OmemoError(`XML parse failed: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (!root || typeof root.getName !== "function") {
|
|
53
|
+
throw new OmemoError("empty XML document");
|
|
54
|
+
}
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
function parseSceEnvelope(xmlText) {
|
|
58
|
+
const root = parseXmlDocument(xmlText);
|
|
59
|
+
if (root.getName() !== "content" || root.getNS() !== NS_SCE) {
|
|
60
|
+
throw new OmemoError("decrypted payload is not a SCE content envelope");
|
|
61
|
+
}
|
|
62
|
+
const payload = root.getChild("payload");
|
|
63
|
+
const body = payload?.getChild("body", "jabber:client");
|
|
64
|
+
if (!body) {
|
|
65
|
+
throw new OmemoError("SCE payload has no <body>");
|
|
66
|
+
}
|
|
67
|
+
return body.text();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// packages/omemo/src/wire.ts
|
|
71
|
+
import xml from "@xmpp/xml";
|
|
72
|
+
|
|
73
|
+
// packages/omemo/src/bytes.ts
|
|
74
|
+
import { fromByteArray, toByteArray } from "base64-js";
|
|
75
|
+
function b64encode(bytes) {
|
|
76
|
+
return fromByteArray(bytes);
|
|
77
|
+
}
|
|
78
|
+
function b64decode(s) {
|
|
79
|
+
return new Uint8Array(toByteArray(s));
|
|
80
|
+
}
|
|
81
|
+
function randomBytes(length) {
|
|
82
|
+
const out = new Uint8Array(length);
|
|
83
|
+
globalThis.crypto.getRandomValues(out);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function utf8Encode(s) {
|
|
87
|
+
return new Uint8Array(new TextEncoder().encode(s));
|
|
88
|
+
}
|
|
89
|
+
function utf8Decode(bytes) {
|
|
90
|
+
return new TextDecoder().decode(bytes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// packages/omemo/src/wire.ts
|
|
94
|
+
function buildEncryptedMessageEl(parts) {
|
|
95
|
+
const headerChildren = [
|
|
96
|
+
...parts.keys.map(
|
|
97
|
+
(k) => xml(
|
|
98
|
+
"key",
|
|
99
|
+
{ rid: String(k.rid), jid: k.jid, ...k.isPreKey ? { prekey: "1" } : {} },
|
|
100
|
+
k.ciphertextB64
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
xml("iv", {}, parts.ivB64)
|
|
104
|
+
];
|
|
105
|
+
return xml(
|
|
106
|
+
"message",
|
|
107
|
+
{ to: parts.roomJid, type: "groupchat", id: parts.stanzaId ?? newStanzaId() },
|
|
108
|
+
xml(
|
|
109
|
+
"encrypted",
|
|
110
|
+
{ xmlns: NS_OMEMO },
|
|
111
|
+
xml("header", { sid: String(parts.senderDeviceId) }, ...headerChildren),
|
|
112
|
+
xml("payload", {}, parts.payloadB64)
|
|
113
|
+
),
|
|
114
|
+
xml("sender", { xmlns: NS_WACANDO_OMEMO, jid: parts.senderBareJid }),
|
|
115
|
+
xml("encryption", { xmlns: NS_EME, namespace: NS_OMEMO, name: "OMEMO" }),
|
|
116
|
+
xml("store", { xmlns: NS_HINTS }),
|
|
117
|
+
xml("body", {}, BODY_MARKER)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function newStanzaId() {
|
|
121
|
+
return `om-${[...randomBytes(8)].map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
122
|
+
}
|
|
123
|
+
function parseEncrypted(stanza) {
|
|
124
|
+
const encrypted = findDescendant(stanza, "encrypted", NS_OMEMO);
|
|
125
|
+
if (!encrypted) return null;
|
|
126
|
+
const header = encrypted.getChild("header");
|
|
127
|
+
const payload = encrypted.getChild("payload");
|
|
128
|
+
const sender = findDescendant(stanza, "sender", NS_WACANDO_OMEMO);
|
|
129
|
+
const sid = Number(header?.getAttr("sid"));
|
|
130
|
+
const senderJid = sender?.getAttr("jid");
|
|
131
|
+
const ivB64 = header?.getChild("iv")?.text();
|
|
132
|
+
const payloadB64 = payload?.text();
|
|
133
|
+
if (!Number.isInteger(sid) || !senderJid || !ivB64 || !payloadB64) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const keys = [];
|
|
137
|
+
for (const k of header?.getChildren("key") ?? []) {
|
|
138
|
+
const rid = Number(k.getAttr("rid"));
|
|
139
|
+
if (!Number.isInteger(rid)) continue;
|
|
140
|
+
const jid = k.getAttr("jid");
|
|
141
|
+
keys.push({
|
|
142
|
+
rid,
|
|
143
|
+
jid: typeof jid === "string" ? jid : void 0,
|
|
144
|
+
ciphertextB64: k.text(),
|
|
145
|
+
isPreKey: k.getAttr("prekey") === "1" || k.getAttr("prekey") === "true"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (keys.length === 0) return null;
|
|
149
|
+
return { senderDeviceId: sid, senderBareJid: senderJid, keys, ivB64, payloadB64 };
|
|
150
|
+
}
|
|
151
|
+
function findDescendant(el, name, ns) {
|
|
152
|
+
for (const child of el.getChildren(name)) {
|
|
153
|
+
if (child.getNS() === ns) return child;
|
|
154
|
+
}
|
|
155
|
+
for (const child of el.children) {
|
|
156
|
+
if (typeof child === "string") continue;
|
|
157
|
+
const found = findDescendant(child, name, ns);
|
|
158
|
+
if (found) return found;
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// packages/omemo/src/recovery-wire.ts
|
|
164
|
+
import xml2 from "@xmpp/xml";
|
|
165
|
+
var NS_RECOVERY = "urn:wacando:omemo:recovery:1";
|
|
166
|
+
var RECOVERY_DOMAIN = "wacando-omemo-session-recovery-v1";
|
|
167
|
+
var RECOVERY_VERSION = "1";
|
|
168
|
+
var RECOVERY_PHASES = ["OFFER", "INIT", "CONFIRM", "COMMIT", "COMMITTED"];
|
|
169
|
+
var MAX_RECORD_BYTES = 4096;
|
|
170
|
+
var MAX_CTL_BYTES = 2048;
|
|
171
|
+
var SIGNATURE_BYTES = 64;
|
|
172
|
+
var FIELD_COUNT = 21;
|
|
173
|
+
var BARE_JID = /^[a-z0-9][a-z0-9._-]{0,127}@[a-z0-9][a-z0-9.-]{0,253}$/;
|
|
174
|
+
var CONVERSATION_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
175
|
+
var DECIMAL = /^(0|[1-9][0-9]{0,15})$/;
|
|
176
|
+
var POSITIVE = /^[1-9][0-9]{0,9}$/;
|
|
177
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
178
|
+
var STANZA_ID = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
179
|
+
var B64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
180
|
+
function signerRole(phase) {
|
|
181
|
+
return phase === "INIT" || phase === "COMMIT" ? "A" : "B";
|
|
182
|
+
}
|
|
183
|
+
function signerJid(record) {
|
|
184
|
+
return signerRole(record.phase) === "A" ? record.aJid : record.bJid;
|
|
185
|
+
}
|
|
186
|
+
function targetJid(record) {
|
|
187
|
+
return signerRole(record.phase) === "A" ? record.bJid : record.aJid;
|
|
188
|
+
}
|
|
189
|
+
function positive(value, max = 4294967295) {
|
|
190
|
+
if (!POSITIVE.test(value)) return null;
|
|
191
|
+
const n = Number(value);
|
|
192
|
+
return n >= 1 && n <= max ? n : null;
|
|
193
|
+
}
|
|
194
|
+
function encodeRecord(r) {
|
|
195
|
+
const fields = [
|
|
196
|
+
RECOVERY_DOMAIN,
|
|
197
|
+
r.phase,
|
|
198
|
+
r.conversationId,
|
|
199
|
+
r.roomJid,
|
|
200
|
+
r.aJid,
|
|
201
|
+
String(r.aDevice),
|
|
202
|
+
r.aFingerprint,
|
|
203
|
+
r.bJid,
|
|
204
|
+
String(r.bDevice),
|
|
205
|
+
r.bFingerprint,
|
|
206
|
+
r.nonce,
|
|
207
|
+
String(r.createdAt),
|
|
208
|
+
String(r.expiresAt),
|
|
209
|
+
String(r.consumedPreKeyId),
|
|
210
|
+
r.stanzaId,
|
|
211
|
+
r.keyDigest,
|
|
212
|
+
String(r.generation),
|
|
213
|
+
r.bundleDigest,
|
|
214
|
+
r.prevDigest,
|
|
215
|
+
r.ctlDigest,
|
|
216
|
+
r.selectedPreKeyId === null ? "" : String(r.selectedPreKeyId)
|
|
217
|
+
];
|
|
218
|
+
return utf8Encode(JSON.stringify(fields));
|
|
219
|
+
}
|
|
220
|
+
function parseRecord(bytes) {
|
|
221
|
+
if (bytes.length === 0 || bytes.length > MAX_RECORD_BYTES)
|
|
222
|
+
return { ok: false, reason: "oversized" };
|
|
223
|
+
let raw;
|
|
224
|
+
try {
|
|
225
|
+
raw = JSON.parse(utf8Decode(bytes));
|
|
226
|
+
} catch {
|
|
227
|
+
return { ok: false, reason: "malformed" };
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(raw) || raw.length !== FIELD_COUNT || !raw.every((f2) => typeof f2 === "string"))
|
|
230
|
+
return { ok: false, reason: "malformed" };
|
|
231
|
+
const f = raw;
|
|
232
|
+
const [
|
|
233
|
+
domain,
|
|
234
|
+
phase,
|
|
235
|
+
conversationId,
|
|
236
|
+
roomJid,
|
|
237
|
+
aJid,
|
|
238
|
+
aDevice,
|
|
239
|
+
aFingerprint,
|
|
240
|
+
bJid,
|
|
241
|
+
bDevice,
|
|
242
|
+
bFingerprint,
|
|
243
|
+
nonce,
|
|
244
|
+
createdAt,
|
|
245
|
+
expiresAt,
|
|
246
|
+
consumedPreKeyId,
|
|
247
|
+
stanzaId,
|
|
248
|
+
keyDigest,
|
|
249
|
+
generation,
|
|
250
|
+
bundleDigest,
|
|
251
|
+
prevDigest,
|
|
252
|
+
ctlDigest,
|
|
253
|
+
selected
|
|
254
|
+
] = f;
|
|
255
|
+
if (domain !== RECOVERY_DOMAIN) return { ok: false, reason: "malformed" };
|
|
256
|
+
if (!RECOVERY_PHASES.includes(phase))
|
|
257
|
+
return { ok: false, reason: "malformed" };
|
|
258
|
+
const p = phase;
|
|
259
|
+
const aDev = positive(aDevice);
|
|
260
|
+
const bDev = positive(bDevice);
|
|
261
|
+
const consumed = positive(consumedPreKeyId, 16777215);
|
|
262
|
+
const selectedId = selected === "" ? null : positive(selected, 16777215);
|
|
263
|
+
const roomLocal = roomJid.split("@")[0];
|
|
264
|
+
const valid = CONVERSATION_ID.test(conversationId) && BARE_JID.test(roomJid) && roomLocal === `c_${conversationId}` && BARE_JID.test(aJid) && BARE_JID.test(bJid) && aDev !== null && bDev !== null && HEX64.test(aFingerprint) && HEX64.test(bFingerprint) && HEX64.test(nonce) && DECIMAL.test(createdAt) && DECIMAL.test(expiresAt) && Number(expiresAt) > Number(createdAt) && consumed !== null && STANZA_ID.test(stanzaId) && HEX64.test(keyDigest) && DECIMAL.test(generation) && HEX64.test(bundleDigest) && (p === "OFFER" ? prevDigest === "" : HEX64.test(prevDigest)) && (p === "OFFER" ? ctlDigest === "" : HEX64.test(ctlDigest)) && (p === "OFFER" ? selected === "" : selectedId !== null && selectedId !== consumed) && !(aJid === bJid && aDev === bDev);
|
|
265
|
+
if (!valid) return { ok: false, reason: "malformed" };
|
|
266
|
+
const record = {
|
|
267
|
+
phase: p,
|
|
268
|
+
conversationId,
|
|
269
|
+
roomJid,
|
|
270
|
+
aJid,
|
|
271
|
+
aDevice: aDev,
|
|
272
|
+
aFingerprint,
|
|
273
|
+
bJid,
|
|
274
|
+
bDevice: bDev,
|
|
275
|
+
bFingerprint,
|
|
276
|
+
nonce,
|
|
277
|
+
createdAt: Number(createdAt),
|
|
278
|
+
expiresAt: Number(expiresAt),
|
|
279
|
+
consumedPreKeyId: consumed,
|
|
280
|
+
stanzaId,
|
|
281
|
+
keyDigest,
|
|
282
|
+
generation: Number(generation),
|
|
283
|
+
bundleDigest,
|
|
284
|
+
prevDigest,
|
|
285
|
+
ctlDigest,
|
|
286
|
+
selectedPreKeyId: selectedId
|
|
287
|
+
};
|
|
288
|
+
const again = encodeRecord(record);
|
|
289
|
+
if (again.length !== bytes.length || again.some((b, i) => b !== bytes[i]))
|
|
290
|
+
return { ok: false, reason: "noncanonical" };
|
|
291
|
+
return { ok: true, record };
|
|
292
|
+
}
|
|
293
|
+
function canonicalB64(text, maxBytes) {
|
|
294
|
+
if (text.length > Math.ceil(maxBytes / 3) * 4 + 4) return "oversized";
|
|
295
|
+
if (text.length === 0 || !B64.test(text)) return null;
|
|
296
|
+
let bytes;
|
|
297
|
+
try {
|
|
298
|
+
bytes = b64decode(text);
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (bytes.length > maxBytes) return "oversized";
|
|
303
|
+
if (bytes.length === 0 || b64encode(bytes) !== text) return null;
|
|
304
|
+
return bytes;
|
|
305
|
+
}
|
|
306
|
+
var FULL_JID = /^[a-z0-9][a-z0-9._-]{0,127}@[a-z0-9][a-z0-9.-]{0,253}\/[^\s/]{1,256}$/;
|
|
307
|
+
function parseRecoveryEl(el, withOrigin) {
|
|
308
|
+
if (!el.is("recovery", NS_RECOVERY)) return { ok: false, reason: "malformed" };
|
|
309
|
+
const attrs = Object.keys(el.attrs).filter((k) => k !== "xmlns");
|
|
310
|
+
if (attrs.length !== 1 || el.attrs.v !== RECOVERY_VERSION)
|
|
311
|
+
return { ok: false, reason: "malformed" };
|
|
312
|
+
let recordB64 = null;
|
|
313
|
+
let sigB64 = null;
|
|
314
|
+
let ctlB64 = null;
|
|
315
|
+
let origin = null;
|
|
316
|
+
let originSeen = false;
|
|
317
|
+
for (const child of el.children) {
|
|
318
|
+
if (typeof child === "string") {
|
|
319
|
+
if (child.trim() !== "") return { ok: false, reason: "malformed" };
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const name = child.getName();
|
|
323
|
+
if (child.getNS() !== NS_RECOVERY) return { ok: false, reason: "malformed" };
|
|
324
|
+
if (name === "origin") {
|
|
325
|
+
if (!withOrigin) return { ok: false, reason: "unexpected-origin" };
|
|
326
|
+
if (originSeen || child.children.length !== 0) return { ok: false, reason: "malformed" };
|
|
327
|
+
const keys = Object.keys(child.attrs).filter((k) => k !== "xmlns");
|
|
328
|
+
const jid = child.attrs.jid;
|
|
329
|
+
if (keys.length !== 1 || typeof jid !== "string" || !FULL_JID.test(jid))
|
|
330
|
+
return { ok: false, reason: "malformed" };
|
|
331
|
+
origin = jid;
|
|
332
|
+
originSeen = true;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const text = (() => {
|
|
336
|
+
const plain = { ...child.attrs };
|
|
337
|
+
delete plain.xmlns;
|
|
338
|
+
if (Object.keys(plain).length !== 0) return null;
|
|
339
|
+
return child.children.every((c) => typeof c === "string") ? child.children.join("") : null;
|
|
340
|
+
})();
|
|
341
|
+
if (text === null) return { ok: false, reason: "malformed" };
|
|
342
|
+
if (name === "record" && recordB64 === null) recordB64 = text;
|
|
343
|
+
else if (name === "sig" && sigB64 === null) sigB64 = text;
|
|
344
|
+
else if (name === "ctl" && ctlB64 === null) ctlB64 = text;
|
|
345
|
+
else return { ok: false, reason: "malformed" };
|
|
346
|
+
}
|
|
347
|
+
if (withOrigin && origin === null) return { ok: false, reason: "missing-origin" };
|
|
348
|
+
if (recordB64 === null || sigB64 === null) return { ok: false, reason: "malformed" };
|
|
349
|
+
const recordBytes = canonicalB64(recordB64, MAX_RECORD_BYTES);
|
|
350
|
+
const signature = canonicalB64(sigB64, SIGNATURE_BYTES);
|
|
351
|
+
const ctl = ctlB64 === null ? null : canonicalB64(ctlB64, MAX_CTL_BYTES);
|
|
352
|
+
if (recordBytes === "oversized" || ctl === "oversized") return { ok: false, reason: "oversized" };
|
|
353
|
+
if (!recordBytes || !signature || signature === "oversized" || signature.length !== SIGNATURE_BYTES || ctlB64 !== null && !ctl)
|
|
354
|
+
return { ok: false, reason: "malformed" };
|
|
355
|
+
const parsed = parseRecord(recordBytes);
|
|
356
|
+
if (!parsed.ok) return parsed;
|
|
357
|
+
if (parsed.record.phase === "OFFER" !== (ctl === null))
|
|
358
|
+
return { ok: false, reason: "malformed" };
|
|
359
|
+
return {
|
|
360
|
+
ok: true,
|
|
361
|
+
control: {
|
|
362
|
+
packet: { recordB64, sigB64, ...ctlB64 !== null ? { ctlB64 } : {} },
|
|
363
|
+
record: parsed.record,
|
|
364
|
+
recordBytes,
|
|
365
|
+
signature,
|
|
366
|
+
ctl,
|
|
367
|
+
originJid: origin
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function buildRecoveryEl(packet, originJid) {
|
|
372
|
+
return xml2(
|
|
373
|
+
"recovery",
|
|
374
|
+
{ xmlns: NS_RECOVERY, v: RECOVERY_VERSION },
|
|
375
|
+
xml2("record", {}, packet.recordB64),
|
|
376
|
+
xml2("sig", {}, packet.sigB64),
|
|
377
|
+
...packet.ctlB64 !== void 0 ? [xml2("ctl", {}, packet.ctlB64)] : [],
|
|
378
|
+
...originJid !== void 0 ? [xml2("origin", { jid: originJid })] : []
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export {
|
|
383
|
+
NS_OMEMO,
|
|
384
|
+
NS_DEVICELIST,
|
|
385
|
+
NS_EME,
|
|
386
|
+
NS_HINTS,
|
|
387
|
+
NS_SCE,
|
|
388
|
+
NS_WACANDO_OMEMO,
|
|
389
|
+
bundleNode,
|
|
390
|
+
PEP_ITEM_ID,
|
|
391
|
+
BODY_MARKER,
|
|
392
|
+
MESSAGE_KEY_BYTES,
|
|
393
|
+
GCM_IV_BYTES,
|
|
394
|
+
GCM_TAG_BYTES,
|
|
395
|
+
OmemoError,
|
|
396
|
+
UnsupportedSignalFormatError,
|
|
397
|
+
OmemoReceiveError,
|
|
398
|
+
b64encode,
|
|
399
|
+
b64decode,
|
|
400
|
+
randomBytes,
|
|
401
|
+
utf8Encode,
|
|
402
|
+
buildSceEnvelope,
|
|
403
|
+
parseXmlDocument,
|
|
404
|
+
parseSceEnvelope,
|
|
405
|
+
buildEncryptedMessageEl,
|
|
406
|
+
parseEncrypted,
|
|
407
|
+
NS_RECOVERY,
|
|
408
|
+
RECOVERY_DOMAIN,
|
|
409
|
+
RECOVERY_VERSION,
|
|
410
|
+
RECOVERY_PHASES,
|
|
411
|
+
MAX_RECORD_BYTES,
|
|
412
|
+
MAX_CTL_BYTES,
|
|
413
|
+
SIGNATURE_BYTES,
|
|
414
|
+
signerRole,
|
|
415
|
+
signerJid,
|
|
416
|
+
targetJid,
|
|
417
|
+
encodeRecord,
|
|
418
|
+
parseRecord,
|
|
419
|
+
parseRecoveryEl,
|
|
420
|
+
buildRecoveryEl
|
|
421
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Embedded from lbframe/wacando@9f82881d4ee6cfd655727a08883877230917c955 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
|
|
2
|
+
import {
|
|
3
|
+
BODY_MARKER,
|
|
4
|
+
GCM_IV_BYTES,
|
|
5
|
+
GCM_TAG_BYTES,
|
|
6
|
+
MAX_CTL_BYTES,
|
|
7
|
+
MAX_RECORD_BYTES,
|
|
8
|
+
MESSAGE_KEY_BYTES,
|
|
9
|
+
NS_DEVICELIST,
|
|
10
|
+
NS_EME,
|
|
11
|
+
NS_HINTS,
|
|
12
|
+
NS_OMEMO,
|
|
13
|
+
NS_RECOVERY,
|
|
14
|
+
NS_SCE,
|
|
15
|
+
NS_WACANDO_OMEMO,
|
|
16
|
+
PEP_ITEM_ID,
|
|
17
|
+
RECOVERY_DOMAIN,
|
|
18
|
+
RECOVERY_PHASES,
|
|
19
|
+
RECOVERY_VERSION,
|
|
20
|
+
SIGNATURE_BYTES,
|
|
21
|
+
buildEncryptedMessageEl,
|
|
22
|
+
buildRecoveryEl,
|
|
23
|
+
bundleNode,
|
|
24
|
+
encodeRecord,
|
|
25
|
+
parseEncrypted,
|
|
26
|
+
parseRecord,
|
|
27
|
+
parseRecoveryEl,
|
|
28
|
+
parseXmlDocument,
|
|
29
|
+
signerJid,
|
|
30
|
+
signerRole,
|
|
31
|
+
targetJid
|
|
32
|
+
} from "./chunks/chunk-OI3RF7TS.mjs";
|
|
33
|
+
import "./chunks/chunk-EIGFLZ2Y.mjs";
|
|
34
|
+
|
|
35
|
+
// packages/omemo/src/policy-wire.ts
|
|
36
|
+
import xml from "@xmpp/xml";
|
|
37
|
+
var NS_WACANDO_POLICY = "urn:wacando:policy:0";
|
|
38
|
+
var DIGEST_VERSION = "wacando-omemo-digest-v1";
|
|
39
|
+
var STANZA_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
40
|
+
var BARE_JID_RE = /^[^\s|/@]{1,1023}@[^\s|/@]{1,1023}$/;
|
|
41
|
+
var B64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
42
|
+
function isCanonicalBareJid(value) {
|
|
43
|
+
return typeof value === "string" && BARE_JID_RE.test(value);
|
|
44
|
+
}
|
|
45
|
+
function parseBoundEncrypted(stanza) {
|
|
46
|
+
const parsed = parseEncrypted(stanza);
|
|
47
|
+
if (!parsed) return null;
|
|
48
|
+
if (!isCanonicalBareJid(parsed.senderBareJid)) return null;
|
|
49
|
+
if (!B64_RE.test(parsed.ivB64.trim()) || !B64_RE.test(parsed.payloadB64.trim())) return null;
|
|
50
|
+
for (const k of parsed.keys) {
|
|
51
|
+
if (!isCanonicalBareJid(k.jid) || !B64_RE.test(k.ciphertextB64.trim())) return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
function canonicalCiphertext(parsed) {
|
|
56
|
+
const lines = [
|
|
57
|
+
DIGEST_VERSION,
|
|
58
|
+
`sender=${parsed.senderBareJid}`,
|
|
59
|
+
`sid=${parsed.senderDeviceId}`,
|
|
60
|
+
...parsed.keys.map(
|
|
61
|
+
(k) => `key=${k.jid}|${k.rid}|${k.isPreKey ? 1 : 0}|${k.ciphertextB64.trim()}`
|
|
62
|
+
),
|
|
63
|
+
`iv=${parsed.ivB64.trim()}`,
|
|
64
|
+
`payload=${parsed.payloadB64.trim()}`
|
|
65
|
+
];
|
|
66
|
+
return `${lines.join("\n")}
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
async function sha256Hex(text) {
|
|
70
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
71
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
72
|
+
}
|
|
73
|
+
async function ciphertextDigest(stanza) {
|
|
74
|
+
const parsed = parseBoundEncrypted(stanza);
|
|
75
|
+
return parsed ? sha256Hex(canonicalCiphertext(parsed)) : null;
|
|
76
|
+
}
|
|
77
|
+
function digestOfParsed(parsed) {
|
|
78
|
+
return sha256Hex(canonicalCiphertext(parsed));
|
|
79
|
+
}
|
|
80
|
+
function isValidStanzaId(id) {
|
|
81
|
+
return typeof id === "string" && STANZA_ID_RE.test(id);
|
|
82
|
+
}
|
|
83
|
+
function toPolicySubmission(groupchat, componentJid) {
|
|
84
|
+
const room = groupchat.getAttr("to");
|
|
85
|
+
const id = groupchat.getAttr("id");
|
|
86
|
+
const parsed = parseBoundEncrypted(groupchat);
|
|
87
|
+
if (typeof room !== "string" || !isValidStanzaId(id) || !parsed) {
|
|
88
|
+
throw new Error("not an encrypted groupchat stanza");
|
|
89
|
+
}
|
|
90
|
+
const canonical = buildHeldGroupchat(room, id, parsed);
|
|
91
|
+
const encrypted = canonical.getChild("encrypted", NS_OMEMO);
|
|
92
|
+
const sender = canonical.getChild("sender", NS_WACANDO_OMEMO);
|
|
93
|
+
if (!encrypted || !sender) throw new Error("not an encrypted groupchat stanza");
|
|
94
|
+
return xml(
|
|
95
|
+
"message",
|
|
96
|
+
{ to: componentJid, id },
|
|
97
|
+
xml("submit", { xmlns: NS_WACANDO_POLICY, room }),
|
|
98
|
+
encrypted,
|
|
99
|
+
sender
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function parsePolicySubmission(stanza) {
|
|
103
|
+
if (!stanza.is("message") || stanza.getAttr("type") === "error") {
|
|
104
|
+
return { ok: false, reason: "malformed" };
|
|
105
|
+
}
|
|
106
|
+
const submit = stanza.getChild("submit", NS_WACANDO_POLICY);
|
|
107
|
+
const id = stanza.getAttr("id");
|
|
108
|
+
const from = stanza.getAttr("from");
|
|
109
|
+
const room = submit?.getAttr("room");
|
|
110
|
+
if (!submit || !isValidStanzaId(id) || typeof from !== "string" || typeof room !== "string") {
|
|
111
|
+
return { ok: false, reason: "malformed", ...isValidStanzaId(id) ? { stanzaId: id } : {} };
|
|
112
|
+
}
|
|
113
|
+
const parsed = parseBoundEncrypted(stanza);
|
|
114
|
+
if (!parsed) return { ok: false, reason: "not-encrypted", stanzaId: id };
|
|
115
|
+
const fromBareJid = from.split("/")[0] ?? "";
|
|
116
|
+
if (parsed.senderBareJid !== fromBareJid) {
|
|
117
|
+
return { ok: false, reason: "sender-mismatch", stanzaId: id };
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, roomJid: room, stanzaId: id, fromBareJid, parsed };
|
|
120
|
+
}
|
|
121
|
+
function buildHeldGroupchat(roomJid, stanzaId, parsed) {
|
|
122
|
+
return buildEncryptedMessageEl({
|
|
123
|
+
roomJid,
|
|
124
|
+
senderBareJid: parsed.senderBareJid,
|
|
125
|
+
senderDeviceId: parsed.senderDeviceId,
|
|
126
|
+
keys: parsed.keys,
|
|
127
|
+
ivB64: parsed.ivB64,
|
|
128
|
+
payloadB64: parsed.payloadB64,
|
|
129
|
+
stanzaId
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function buildRelayEl(roomJid, stanzaId, parsed, digest) {
|
|
133
|
+
const el = buildHeldGroupchat(roomJid, stanzaId, parsed);
|
|
134
|
+
el.append(xml("relay", { xmlns: NS_WACANDO_POLICY, jid: parsed.senderBareJid, digest }));
|
|
135
|
+
return el;
|
|
136
|
+
}
|
|
137
|
+
function submissionStatusEl(to, stanzaId, state, extra = {}) {
|
|
138
|
+
return xml(
|
|
139
|
+
"message",
|
|
140
|
+
{ to, type: "normal" },
|
|
141
|
+
xml("submission", {
|
|
142
|
+
xmlns: NS_WACANDO_POLICY,
|
|
143
|
+
id: stanzaId,
|
|
144
|
+
state,
|
|
145
|
+
...extra.reason ? { reason: extra.reason } : {},
|
|
146
|
+
...extra.digest ? { digest: extra.digest } : {}
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
BODY_MARKER,
|
|
152
|
+
GCM_IV_BYTES,
|
|
153
|
+
GCM_TAG_BYTES,
|
|
154
|
+
MAX_CTL_BYTES,
|
|
155
|
+
MAX_RECORD_BYTES,
|
|
156
|
+
MESSAGE_KEY_BYTES,
|
|
157
|
+
NS_DEVICELIST,
|
|
158
|
+
NS_EME,
|
|
159
|
+
NS_HINTS,
|
|
160
|
+
NS_OMEMO,
|
|
161
|
+
NS_RECOVERY,
|
|
162
|
+
NS_SCE,
|
|
163
|
+
NS_WACANDO_OMEMO,
|
|
164
|
+
NS_WACANDO_POLICY,
|
|
165
|
+
PEP_ITEM_ID,
|
|
166
|
+
RECOVERY_DOMAIN,
|
|
167
|
+
RECOVERY_PHASES,
|
|
168
|
+
RECOVERY_VERSION,
|
|
169
|
+
SIGNATURE_BYTES,
|
|
170
|
+
buildEncryptedMessageEl,
|
|
171
|
+
buildHeldGroupchat,
|
|
172
|
+
buildRecoveryEl,
|
|
173
|
+
buildRelayEl,
|
|
174
|
+
bundleNode,
|
|
175
|
+
canonicalCiphertext,
|
|
176
|
+
ciphertextDigest,
|
|
177
|
+
digestOfParsed,
|
|
178
|
+
encodeRecord,
|
|
179
|
+
isCanonicalBareJid,
|
|
180
|
+
isValidStanzaId,
|
|
181
|
+
parseBoundEncrypted,
|
|
182
|
+
parseEncrypted,
|
|
183
|
+
parsePolicySubmission,
|
|
184
|
+
parseRecord,
|
|
185
|
+
parseRecoveryEl,
|
|
186
|
+
parseXmlDocument,
|
|
187
|
+
signerJid,
|
|
188
|
+
signerRole,
|
|
189
|
+
submissionStatusEl,
|
|
190
|
+
targetJid,
|
|
191
|
+
toPolicySubmission
|
|
192
|
+
};
|