morok-bot-sdk 1.0.1
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/LICENSE +201 -0
- package/README.md +602 -0
- package/README.ru.md +602 -0
- package/dist/bot.d.ts +232 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +558 -0
- package/dist/bot.js.map +1 -0
- package/dist/crypto/channel-cipher.d.ts +32 -0
- package/dist/crypto/channel-cipher.d.ts.map +1 -0
- package/dist/crypto/channel-cipher.js +77 -0
- package/dist/crypto/channel-cipher.js.map +1 -0
- package/dist/crypto/channel-key-store.d.ts +37 -0
- package/dist/crypto/channel-key-store.d.ts.map +1 -0
- package/dist/crypto/channel-key-store.js +149 -0
- package/dist/crypto/channel-key-store.js.map +1 -0
- package/dist/crypto/cross-signing.d.ts +57 -0
- package/dist/crypto/cross-signing.d.ts.map +1 -0
- package/dist/crypto/cross-signing.js +111 -0
- package/dist/crypto/cross-signing.js.map +1 -0
- package/dist/crypto/file-cipher.d.ts +36 -0
- package/dist/crypto/file-cipher.d.ts.map +1 -0
- package/dist/crypto/file-cipher.js +61 -0
- package/dist/crypto/file-cipher.js.map +1 -0
- package/dist/crypto/group-secret-cipher.d.ts +49 -0
- package/dist/crypto/group-secret-cipher.d.ts.map +1 -0
- package/dist/crypto/group-secret-cipher.js +69 -0
- package/dist/crypto/group-secret-cipher.js.map +1 -0
- package/dist/crypto/group-secret-store.d.ts +35 -0
- package/dist/crypto/group-secret-store.d.ts.map +1 -0
- package/dist/crypto/group-secret-store.js +149 -0
- package/dist/crypto/group-secret-store.js.map +1 -0
- package/dist/crypto/signal.d.ts +81 -0
- package/dist/crypto/signal.d.ts.map +1 -0
- package/dist/crypto/signal.js +125 -0
- package/dist/crypto/signal.js.map +1 -0
- package/dist/crypto/stores.d.ts +130 -0
- package/dist/crypto/stores.d.ts.map +1 -0
- package/dist/crypto/stores.js +314 -0
- package/dist/crypto/stores.js.map +1 -0
- package/dist/flow/attachments.d.ts +110 -0
- package/dist/flow/attachments.d.ts.map +1 -0
- package/dist/flow/attachments.js +409 -0
- package/dist/flow/attachments.js.map +1 -0
- package/dist/flow/conv-cache.d.ts +36 -0
- package/dist/flow/conv-cache.d.ts.map +1 -0
- package/dist/flow/conv-cache.js +84 -0
- package/dist/flow/conv-cache.js.map +1 -0
- package/dist/flow/direct.d.ts +109 -0
- package/dist/flow/direct.d.ts.map +1 -0
- package/dist/flow/direct.js +346 -0
- package/dist/flow/direct.js.map +1 -0
- package/dist/flow/groups.d.ts +146 -0
- package/dist/flow/groups.d.ts.map +1 -0
- package/dist/flow/groups.js +768 -0
- package/dist/flow/groups.js.map +1 -0
- package/dist/flow/prekeys.d.ts +45 -0
- package/dist/flow/prekeys.d.ts.map +1 -0
- package/dist/flow/prekeys.js +111 -0
- package/dist/flow/prekeys.js.map +1 -0
- package/dist/flow/receive.d.ts +125 -0
- package/dist/flow/receive.d.ts.map +1 -0
- package/dist/flow/receive.js +773 -0
- package/dist/flow/receive.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/morokbot-file.d.ts +14 -0
- package/dist/morokbot-file.d.ts.map +1 -0
- package/dist/morokbot-file.js +88 -0
- package/dist/morokbot-file.js.map +1 -0
- package/dist/ratelimit.d.ts +40 -0
- package/dist/ratelimit.d.ts.map +1 -0
- package/dist/ratelimit.js +76 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/sessions.d.ts +34 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +69 -0
- package/dist/sessions.js.map +1 -0
- package/dist/state-lock.d.ts +17 -0
- package/dist/state-lock.d.ts.map +1 -0
- package/dist/state-lock.js +66 -0
- package/dist/state-lock.js.map +1 -0
- package/dist/transport/http.d.ts +48 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +112 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/ws.d.ts +65 -0
- package/dist/transport/ws.d.ts.map +1 -0
- package/dist/transport/ws.js +219 -0
- package/dist/transport/ws.js.map +1 -0
- package/dist/types.d.ts +254 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { webcrypto } from 'node:crypto';
|
|
2
|
+
const subtle = webcrypto.subtle;
|
|
3
|
+
export const CHANNEL_KEY_BYTES = 32;
|
|
4
|
+
export const CHANNEL_IV_BYTES = 12;
|
|
5
|
+
export const CHANNEL_TAG_BYTES = 16;
|
|
6
|
+
export const CHANNEL_MAGIC = new TextEncoder().encode('MOK1');
|
|
7
|
+
export const CHANNEL_MAGIC_BYTES = CHANNEL_MAGIC.length;
|
|
8
|
+
export const CHANNEL_EPOCH_BYTES = 4;
|
|
9
|
+
export const CHANNEL_HEADER_BYTES = CHANNEL_MAGIC_BYTES + CHANNEL_EPOCH_BYTES + CHANNEL_IV_BYTES;
|
|
10
|
+
async function importChannelKey(raw, usages) {
|
|
11
|
+
if (raw.byteLength !== CHANNEL_KEY_BYTES) {
|
|
12
|
+
throw new Error(`channel-cipher: key must be ${CHANNEL_KEY_BYTES} bytes; got ${raw.byteLength}`);
|
|
13
|
+
}
|
|
14
|
+
return subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, usages);
|
|
15
|
+
}
|
|
16
|
+
export function channelAad(conversationId) {
|
|
17
|
+
if (!Number.isInteger(conversationId) || conversationId < 1) {
|
|
18
|
+
throw new Error(`channel-cipher: bad conversationId ${conversationId}`);
|
|
19
|
+
}
|
|
20
|
+
return new TextEncoder().encode(`morok-channel-${conversationId}`);
|
|
21
|
+
}
|
|
22
|
+
function writeUint32BE(out, offset, value) {
|
|
23
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
|
|
24
|
+
throw new Error(`channel-cipher: epoch ${value} out of uint32 range`);
|
|
25
|
+
}
|
|
26
|
+
new DataView(out.buffer, out.byteOffset, out.byteLength).setUint32(offset, value, false);
|
|
27
|
+
}
|
|
28
|
+
function readUint32BE(buf, offset) {
|
|
29
|
+
if (offset + 4 > buf.byteLength) {
|
|
30
|
+
throw new Error(`channel-cipher: readUint32BE past end (offset=${offset}, len=${buf.byteLength})`);
|
|
31
|
+
}
|
|
32
|
+
return new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(offset, false);
|
|
33
|
+
}
|
|
34
|
+
function hasChannelMagic(buf) {
|
|
35
|
+
if (buf.byteLength < CHANNEL_HEADER_BYTES + CHANNEL_TAG_BYTES)
|
|
36
|
+
return false;
|
|
37
|
+
for (let i = 0; i < CHANNEL_MAGIC_BYTES; i++) {
|
|
38
|
+
if (buf[i] !== CHANNEL_MAGIC[i])
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export function parseChannelWire(wire) {
|
|
44
|
+
if (hasChannelMagic(wire)) {
|
|
45
|
+
const epoch = readUint32BE(wire, CHANNEL_MAGIC_BYTES);
|
|
46
|
+
const iv = wire.subarray(CHANNEL_MAGIC_BYTES + CHANNEL_EPOCH_BYTES, CHANNEL_HEADER_BYTES);
|
|
47
|
+
const ct = wire.subarray(CHANNEL_HEADER_BYTES);
|
|
48
|
+
return { epoch, iv, ct };
|
|
49
|
+
}
|
|
50
|
+
if (wire.byteLength < CHANNEL_IV_BYTES + CHANNEL_TAG_BYTES) {
|
|
51
|
+
throw new Error(`channel-cipher: legacy wire too short (${wire.byteLength} < ${CHANNEL_IV_BYTES + CHANNEL_TAG_BYTES})`);
|
|
52
|
+
}
|
|
53
|
+
const iv = wire.subarray(0, CHANNEL_IV_BYTES);
|
|
54
|
+
const ct = wire.subarray(CHANNEL_IV_BYTES);
|
|
55
|
+
return { epoch: 0, iv, ct };
|
|
56
|
+
}
|
|
57
|
+
export async function encryptChannelWire(secret, conversationId, epoch, plaintext) {
|
|
58
|
+
if (!Number.isInteger(epoch) || epoch < 0 || epoch > 0xffffffff) {
|
|
59
|
+
throw new Error(`channel-cipher: epoch ${epoch} out of uint32 range`);
|
|
60
|
+
}
|
|
61
|
+
const key = await importChannelKey(secret, ['encrypt']);
|
|
62
|
+
const iv = webcrypto.getRandomValues(new Uint8Array(CHANNEL_IV_BYTES));
|
|
63
|
+
const aad = channelAad(conversationId);
|
|
64
|
+
const ct = new Uint8Array(await subtle.encrypt({ name: 'AES-GCM', iv, additionalData: aad }, key, plaintext));
|
|
65
|
+
const wire = new Uint8Array(CHANNEL_HEADER_BYTES + ct.byteLength);
|
|
66
|
+
wire.set(CHANNEL_MAGIC, 0);
|
|
67
|
+
writeUint32BE(wire, CHANNEL_MAGIC_BYTES, epoch);
|
|
68
|
+
wire.set(iv, CHANNEL_MAGIC_BYTES + CHANNEL_EPOCH_BYTES);
|
|
69
|
+
wire.set(ct, CHANNEL_HEADER_BYTES);
|
|
70
|
+
return wire;
|
|
71
|
+
}
|
|
72
|
+
export async function decryptChannelWire(secret, conversationId, parsed) {
|
|
73
|
+
const key = await importChannelKey(secret, ['decrypt']);
|
|
74
|
+
const aad = channelAad(conversationId);
|
|
75
|
+
return new Uint8Array(await subtle.decrypt({ name: 'AES-GCM', iv: parsed.iv, additionalData: aad }, key, parsed.ct));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=channel-cipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-cipher.js","sourceRoot":"","sources":["../../src/crypto/channel-cipher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation channel-key state on disk at {stateDir}/channel-keys/<conversationId>.json
|
|
3
|
+
* { "currentEpoch": N, "keys": { "0": "<base64 32 bytes>", ... } }
|
|
4
|
+
*
|
|
5
|
+
* Writes are atomic (tmp + rename). A per-conv in-memory lock serialises read-modify-write so concurrent
|
|
6
|
+
* mergeEpochs don't lose updates. Corrupt JSON is moved to <id>.json.corrupt-<ts> and the SDK refetches
|
|
7
|
+
*/
|
|
8
|
+
import type { SdkLogger } from '../types.js';
|
|
9
|
+
export interface ChannelKeyState {
|
|
10
|
+
currentEpoch: number;
|
|
11
|
+
/** epoch (int as decimal string) -> base64(32 bytes) */
|
|
12
|
+
keys: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export declare class ChannelKeyStore {
|
|
15
|
+
private readonly logger?;
|
|
16
|
+
private readonly dir;
|
|
17
|
+
private readonly chains;
|
|
18
|
+
constructor(stateDir: string, logger?: SdkLogger | undefined);
|
|
19
|
+
/** Ensure dir exists, sweep stale tmp files from a prior crash */
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
load(conversationId: number): Promise<ChannelKeyState | null>;
|
|
22
|
+
/** Merge a batch of (epoch, base64-secret) pairs. currentEpoch tracks max. Creates the file if missing */
|
|
23
|
+
mergeEpochs(conversationId: number, entries: Array<{
|
|
24
|
+
epoch: number;
|
|
25
|
+
secretBase64: string;
|
|
26
|
+
}>): Promise<ChannelKeyState>;
|
|
27
|
+
/** Returns a fresh 32-byte copy. Caller may mutate / zero it without touching the store's state */
|
|
28
|
+
getSecret(conversationId: number, epoch: number): Promise<Uint8Array | null>;
|
|
29
|
+
/** Wipe state for a conversation (called on conversation_kicked) */
|
|
30
|
+
drop(conversationId: number): Promise<void>;
|
|
31
|
+
private pathFor;
|
|
32
|
+
private loadInner;
|
|
33
|
+
private parseState;
|
|
34
|
+
private saveInner;
|
|
35
|
+
private withLock;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=channel-key-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-key-store.d.ts","sourceRoot":"","sources":["../../src/crypto/channel-key-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAE,MAA4C,aAAa,CAAA;AAGlF,MAAM,WAAW,eAAe;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAGD,qBAAa,eAAe;IAIM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAHtD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsC;gBAEjD,QAAQ,EAAE,MAAM,EAAmB,MAAM,CAAC,EAAE,SAAS,YAAA;IAKjE,kEAAkE;IAC5D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAKnE,0GAA0G;IACpG,WAAW,CACb,cAAc,EAAE,MAAM,EACtB,OAAO,EAAS,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,GAC/D,OAAO,CAAC,eAAe,CAAC;IAgC3B,mGAAmG;IAC7F,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAalF,oEAAoE;IAC9D,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjD,OAAO,CAAC,OAAO;YAQD,SAAS;IAuBvB,OAAO,CAAC,UAAU;YAaJ,SAAS;IAWvB,OAAO,CAAC,QAAQ;CAYnB"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile, unlink, readdir } from 'node:fs/promises';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { CHANNEL_KEY_BYTES } from './channel-cipher.js';
|
|
5
|
+
export class ChannelKeyStore {
|
|
6
|
+
logger;
|
|
7
|
+
dir;
|
|
8
|
+
chains = new Map();
|
|
9
|
+
constructor(stateDir, logger) {
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.dir = join(stateDir, 'channel-keys');
|
|
12
|
+
}
|
|
13
|
+
async init() {
|
|
14
|
+
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
15
|
+
let entries = [];
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(this.dir);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const e of entries) {
|
|
23
|
+
if (e.includes('.tmp.')) {
|
|
24
|
+
try {
|
|
25
|
+
await unlink(join(this.dir, e));
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async load(conversationId) {
|
|
32
|
+
return this.withLock(conversationId, () => this.loadInner(conversationId));
|
|
33
|
+
}
|
|
34
|
+
async mergeEpochs(conversationId, entries) {
|
|
35
|
+
if (entries.length === 0) {
|
|
36
|
+
const existing = await this.load(conversationId);
|
|
37
|
+
return existing ?? { currentEpoch: -1, keys: {} };
|
|
38
|
+
}
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
if (!Number.isInteger(e.epoch) || e.epoch < 0 || e.epoch > 0xffffffff) {
|
|
41
|
+
throw new Error(`channel-key-store: bad epoch ${e.epoch}`);
|
|
42
|
+
}
|
|
43
|
+
const raw = Buffer.from(e.secretBase64, 'base64');
|
|
44
|
+
if (raw.byteLength !== CHANNEL_KEY_BYTES) {
|
|
45
|
+
throw new Error(`channel-key-store: secret for epoch ${e.epoch} decodes to ${raw.byteLength} bytes; expected ${CHANNEL_KEY_BYTES}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.withLock(conversationId, async () => {
|
|
49
|
+
const cur = await this.loadInner(conversationId);
|
|
50
|
+
const next = cur
|
|
51
|
+
? { currentEpoch: cur.currentEpoch, keys: { ...cur.keys } }
|
|
52
|
+
: { currentEpoch: -1, keys: {} };
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
next.keys[String(e.epoch)] = e.secretBase64;
|
|
55
|
+
if (e.epoch > next.currentEpoch)
|
|
56
|
+
next.currentEpoch = e.epoch;
|
|
57
|
+
}
|
|
58
|
+
await this.saveInner(conversationId, next);
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async getSecret(conversationId, epoch) {
|
|
63
|
+
const state = await this.load(conversationId);
|
|
64
|
+
if (!state)
|
|
65
|
+
return null;
|
|
66
|
+
const b64 = state.keys[String(epoch)];
|
|
67
|
+
if (!b64)
|
|
68
|
+
return null;
|
|
69
|
+
const raw = Buffer.from(b64, 'base64');
|
|
70
|
+
if (raw.byteLength !== CHANNEL_KEY_BYTES)
|
|
71
|
+
return null;
|
|
72
|
+
const out = new Uint8Array(CHANNEL_KEY_BYTES);
|
|
73
|
+
out.set(raw);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
async drop(conversationId) {
|
|
77
|
+
return this.withLock(conversationId, async () => {
|
|
78
|
+
const p = this.pathFor(conversationId);
|
|
79
|
+
try {
|
|
80
|
+
await unlink(p);
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
pathFor(conversationId) {
|
|
86
|
+
if (!Number.isInteger(conversationId) || conversationId < 1) {
|
|
87
|
+
throw new Error(`channel-key-store: bad conversationId ${conversationId}`);
|
|
88
|
+
}
|
|
89
|
+
return join(this.dir, `${conversationId}.json`);
|
|
90
|
+
}
|
|
91
|
+
async loadInner(conversationId) {
|
|
92
|
+
const p = this.pathFor(conversationId);
|
|
93
|
+
let raw;
|
|
94
|
+
try {
|
|
95
|
+
raw = await readFile(p, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err?.code === 'ENOENT')
|
|
99
|
+
return null;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return this.parseState(raw);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const quarantined = `${p}.corrupt-${Date.now()}`;
|
|
107
|
+
try {
|
|
108
|
+
await rename(p, quarantined);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
this.logger?.warn({ conversationId, err: err.message, quarantined }, '[channel-key-store] quarantined corrupt state');
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
parseState(raw) {
|
|
116
|
+
const obj = JSON.parse(raw);
|
|
117
|
+
if (!obj || typeof obj !== 'object')
|
|
118
|
+
throw new Error('not an object');
|
|
119
|
+
if (typeof obj.currentEpoch !== 'number')
|
|
120
|
+
throw new Error('missing currentEpoch');
|
|
121
|
+
if (!obj.keys || typeof obj.keys !== 'object')
|
|
122
|
+
throw new Error('missing keys');
|
|
123
|
+
for (const k of Object.keys(obj.keys)) {
|
|
124
|
+
if (typeof obj.keys[k] !== 'string')
|
|
125
|
+
throw new Error(`epoch ${k} not a string`);
|
|
126
|
+
}
|
|
127
|
+
return obj;
|
|
128
|
+
}
|
|
129
|
+
async saveInner(conversationId, state) {
|
|
130
|
+
const p = this.pathFor(conversationId);
|
|
131
|
+
const tmp = `${p}.tmp.${randomBytes(4).toString('hex')}`;
|
|
132
|
+
await mkdir(dirname(p), { recursive: true, mode: 0o700 });
|
|
133
|
+
const json = JSON.stringify(state);
|
|
134
|
+
await writeFile(tmp, json, { mode: 0o600 });
|
|
135
|
+
await rename(tmp, p);
|
|
136
|
+
}
|
|
137
|
+
withLock(conversationId, fn) {
|
|
138
|
+
const prev = this.chains.get(conversationId) ?? Promise.resolve();
|
|
139
|
+
const next = prev.then(fn, fn);
|
|
140
|
+
const cleanup = next.finally(() => {
|
|
141
|
+
if (this.chains.get(conversationId) === cleanup) {
|
|
142
|
+
this.chains.delete(conversationId);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
this.chains.set(conversationId, cleanup);
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=channel-key-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-key-store.js","sourceRoot":"","sources":["../../src/crypto/channel-key-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D cross-signing primitives for the bot SDK, the headless counterpart to frontend/src/signal/cross-signing.ts
|
|
3
|
+
* with the server mirror in src/routes/prekeys.ts. All three must produce byte-identical canonical messages
|
|
4
|
+
* and use the same XEdDSA (Curve25519), or certificates won't verify across ends
|
|
5
|
+
*
|
|
6
|
+
* signDeviceCert signs a cert over the bot's own device identity key under the account signing key (XSK)
|
|
7
|
+
* from the .morokbot file, then POSTs it to /crypto/cross-signing (see bot.ts:ensureCrossSigning)
|
|
8
|
+
* Without the cert the bot stays an uncertified legacy device and users can't derive a cross-signed safety number
|
|
9
|
+
*
|
|
10
|
+
* verifyPeerCert is the encrypt-side gate wired into signal.ts:processPreKeyBundle
|
|
11
|
+
* When a peer bundle carries both an XSK and a device cert, the cert is verified before opening a session
|
|
12
|
+
* Present-but-invalid is the one hard-reject case (tampered bundle, server identity injection)
|
|
13
|
+
* NULL on either side falls back to TOFU, since most peers may legitimately be uncertified
|
|
14
|
+
* and rejecting NULL is a lock-out trap
|
|
15
|
+
*
|
|
16
|
+
* Canonical message (mirror, a change here means changing the other two):
|
|
17
|
+
* SHA-256( userId u64-BE || deviceId u32-BE || identity_key raw bytes )
|
|
18
|
+
* identity_key is the libsignal-serialised public key (33-byte, 0x05-prefixed)
|
|
19
|
+
* exactly as the server stored it in devices.identity_key
|
|
20
|
+
* The 32-byte digest is the message that is signed and verified directly, with no second hashing
|
|
21
|
+
*
|
|
22
|
+
* Primitive: Curve25519Wrapper, the same WASM library libsignal, the server, and the FE use
|
|
23
|
+
* Sign with the 32-byte XSK private, verify with signatureIsValid, NEVER verify (it returns true on invalid signatures)
|
|
24
|
+
*
|
|
25
|
+
* A one-time self-test at wrapper init, a valid round-trip + tamper-reject + wrong-key-reject
|
|
26
|
+
* over a full canonicalCrossSignMessage cycle, tripwires an encoding regression or a broken wrapper
|
|
27
|
+
* before it can silently reject every valid peer cert or mint a cert that fails server-side
|
|
28
|
+
* On failure getWrapper throws and verifyPeerCert maps it to 'uncertified' (TOFU)
|
|
29
|
+
* so a crypto fault degrades the bot and never cuts it off from every peer
|
|
30
|
+
*/
|
|
31
|
+
import type { SdkLogger } from '../types.js';
|
|
32
|
+
/**
|
|
33
|
+
* Canonical bytes the device cert is signed over, byte-identical to the server's and the FE's
|
|
34
|
+
* The 32-byte SHA-256 digest is the message that is signed and verified directly, no second hashing
|
|
35
|
+
*/
|
|
36
|
+
export declare function canonicalCrossSignMessage(userId: number, deviceId: number, identityKeyB64: string): Uint8Array;
|
|
37
|
+
/**
|
|
38
|
+
* Signs the canonical message for (userId, deviceId, identityKeyB64) under the 32-byte XSK private key
|
|
39
|
+
* Returns base64 of the 64-byte XEdDSA signature, passed as `deviceCertificate` to POST /crypto/cross-signing
|
|
40
|
+
*/
|
|
41
|
+
export declare function signDeviceCert(xskPrivB64: string, userId: number, deviceId: number, identityKeyB64: string): Promise<string>;
|
|
42
|
+
/** verifyPeerCert result. Mirrors the FE's PeerDeviceCertStatus */
|
|
43
|
+
export type PeerCertStatus = 'verified' | 'uncertified' | 'invalid';
|
|
44
|
+
/**
|
|
45
|
+
* Encrypt-side gate. Verifies a peer device's cert over the canonical message
|
|
46
|
+
* NULL-tolerant, a missing XSK or cert means 'uncertified' (caller proceeds with TOFU)
|
|
47
|
+
* Present-but-unverifiable returns 'invalid' and the caller hard-rejects the session
|
|
48
|
+
* Never throws. If the curve wrapper is unavailable it returns 'uncertified' and logs, degrading the bot to TOFU
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyPeerCert(args: {
|
|
51
|
+
userId: number;
|
|
52
|
+
deviceId: number;
|
|
53
|
+
identityKeyB64: string;
|
|
54
|
+
accountSigningKey?: string | null;
|
|
55
|
+
deviceCertificate?: string | null;
|
|
56
|
+
}, logger?: SdkLogger): Promise<PeerCertStatus>;
|
|
57
|
+
//# sourceMappingURL=cross-signing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-signing.d.ts","sourceRoot":"","sources":["../../src/crypto/cross-signing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAa,aAAa,CAAA;AAyBnD;;;GAGG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GACzD,UAAU,CAgBZ;AAgDD;;;GAGG;AACH,wBAAsB,cAAc,CAChC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAC7E,OAAO,CAAC,MAAM,CAAC,CAYjB;AAGD,mEAAmE;AACnE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAA;AAGnE;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACvC,MAAM,EAAc,MAAM,CAAA;IAC1B,QAAQ,EAAY,MAAM,CAAA;IAC1B,cAAc,EAAM,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,CA0B9C"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { Curve25519Wrapper } from '@privacyresearch/curve25519-typescript';
|
|
3
|
+
import { KeyHelper } from '@privacyresearch/libsignal-protocol-typescript';
|
|
4
|
+
const DJB_PREFIX_BYTE = 0x05;
|
|
5
|
+
const PUBKEY_RAW_BYTES = 32;
|
|
6
|
+
const PUBKEY_PREFIXED = 33;
|
|
7
|
+
const PRIVKEY_BYTES = 32;
|
|
8
|
+
const SIGNATURE_BYTES = 64;
|
|
9
|
+
function toArrayBuffer(u8) {
|
|
10
|
+
const out = new ArrayBuffer(u8.byteLength);
|
|
11
|
+
new Uint8Array(out).set(u8);
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
function stripDjbPrefix(bytes) {
|
|
15
|
+
if (bytes.length === PUBKEY_RAW_BYTES)
|
|
16
|
+
return bytes;
|
|
17
|
+
if (bytes.length === PUBKEY_PREFIXED && bytes[0] === DJB_PREFIX_BYTE)
|
|
18
|
+
return bytes.subarray(1);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function canonicalCrossSignMessage(userId, deviceId, identityKeyB64) {
|
|
22
|
+
if (!Number.isInteger(userId) || userId < 1) {
|
|
23
|
+
throw new Error('canonicalCrossSignMessage: bad userId');
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isInteger(deviceId) || deviceId < 1) {
|
|
26
|
+
throw new Error('canonicalCrossSignMessage: bad deviceId');
|
|
27
|
+
}
|
|
28
|
+
const identity = Buffer.from(identityKeyB64, 'base64');
|
|
29
|
+
if (identity.length === 0) {
|
|
30
|
+
throw new Error('canonicalCrossSignMessage: empty identity');
|
|
31
|
+
}
|
|
32
|
+
const buf = Buffer.alloc(8 + 4 + identity.length);
|
|
33
|
+
buf.writeBigUInt64BE(BigInt(userId), 0);
|
|
34
|
+
buf.writeUInt32BE(deviceId, 8);
|
|
35
|
+
identity.copy(buf, 12);
|
|
36
|
+
return new Uint8Array(createHash('sha256').update(buf).digest());
|
|
37
|
+
}
|
|
38
|
+
let wrapperPromise = null;
|
|
39
|
+
function getWrapper() {
|
|
40
|
+
if (!wrapperPromise) {
|
|
41
|
+
wrapperPromise = (async () => {
|
|
42
|
+
const w = await Curve25519Wrapper.create();
|
|
43
|
+
await selfTest(w);
|
|
44
|
+
return w;
|
|
45
|
+
})().catch(err => {
|
|
46
|
+
wrapperPromise = null;
|
|
47
|
+
throw err;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return wrapperPromise;
|
|
51
|
+
}
|
|
52
|
+
async function selfTest(w) {
|
|
53
|
+
const kp = await KeyHelper.generateIdentityKeyPair();
|
|
54
|
+
const wrongKp = await KeyHelper.generateIdentityKeyPair();
|
|
55
|
+
const pubRaw = new Uint8Array(kp.pubKey).slice(1);
|
|
56
|
+
const wrongRaw = new Uint8Array(wrongKp.pubKey).slice(1);
|
|
57
|
+
const identityB64 = Buffer.from(new Uint8Array(kp.pubKey)).toString('base64');
|
|
58
|
+
const msg = canonicalCrossSignMessage(424242, 7, identityB64);
|
|
59
|
+
const sig = w.sign(kp.privKey, toArrayBuffer(msg));
|
|
60
|
+
if (!w.signatureIsValid(toArrayBuffer(pubRaw), toArrayBuffer(msg), sig)) {
|
|
61
|
+
throw new Error('cross-signing self-test FAIL: valid signature verifies false (wrapper broken or encoding regression)');
|
|
62
|
+
}
|
|
63
|
+
const tampered = new Uint8Array(sig);
|
|
64
|
+
tampered[0] ^= 0x01;
|
|
65
|
+
if (w.signatureIsValid(toArrayBuffer(pubRaw), toArrayBuffer(msg), toArrayBuffer(tampered))) {
|
|
66
|
+
throw new Error('cross-signing self-test FAIL: tampered signature verifies true (signatureIsValid replaced with legacy verify?)');
|
|
67
|
+
}
|
|
68
|
+
if (w.signatureIsValid(toArrayBuffer(wrongRaw), toArrayBuffer(msg), sig)) {
|
|
69
|
+
throw new Error('cross-signing self-test FAIL: signature verifies under the wrong public key (wrapper broken)');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function signDeviceCert(xskPrivB64, userId, deviceId, identityKeyB64) {
|
|
73
|
+
const priv = Buffer.from(xskPrivB64, 'base64');
|
|
74
|
+
if (priv.length !== PRIVKEY_BYTES) {
|
|
75
|
+
throw new Error(`signDeviceCert: bad XSK private length ${priv.length} (expected ${PRIVKEY_BYTES})`);
|
|
76
|
+
}
|
|
77
|
+
const w = await getWrapper();
|
|
78
|
+
const msg = canonicalCrossSignMessage(userId, deviceId, identityKeyB64);
|
|
79
|
+
const sig = new Uint8Array(w.sign(toArrayBuffer(new Uint8Array(priv)), toArrayBuffer(msg)));
|
|
80
|
+
if (sig.length !== SIGNATURE_BYTES) {
|
|
81
|
+
throw new Error(`signDeviceCert: unexpected signature length ${sig.length}`);
|
|
82
|
+
}
|
|
83
|
+
return Buffer.from(sig).toString('base64');
|
|
84
|
+
}
|
|
85
|
+
export async function verifyPeerCert(args, logger) {
|
|
86
|
+
if (!args.accountSigningKey || !args.deviceCertificate)
|
|
87
|
+
return 'uncertified';
|
|
88
|
+
let w;
|
|
89
|
+
try {
|
|
90
|
+
w = await getWrapper();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
logger?.warn({ err: err.message, peer: `${args.userId}.${args.deviceId}` }, '[cross-signing] curve unavailable; treating peer cert as uncertified (TOFU)');
|
|
94
|
+
return 'uncertified';
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const pubRaw = stripDjbPrefix(new Uint8Array(Buffer.from(args.accountSigningKey, 'base64')));
|
|
98
|
+
if (!pubRaw)
|
|
99
|
+
return 'invalid';
|
|
100
|
+
const sig = new Uint8Array(Buffer.from(args.deviceCertificate, 'base64'));
|
|
101
|
+
if (sig.length !== SIGNATURE_BYTES)
|
|
102
|
+
return 'invalid';
|
|
103
|
+
const msg = canonicalCrossSignMessage(args.userId, args.deviceId, args.identityKeyB64);
|
|
104
|
+
const ok = w.signatureIsValid(toArrayBuffer(pubRaw), toArrayBuffer(msg), toArrayBuffer(sig));
|
|
105
|
+
return ok ? 'verified' : 'invalid';
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return 'invalid';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=cross-signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-signing.js","sourceRoot":"","sources":["../../src/crypto/cross-signing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file AES-256-GCM, byte-identical to frontend/src/lib/crypto-utils.ts
|
|
3
|
+
* Any drift on this layout breaks decryption on the peer with an AES-GCM tag error
|
|
4
|
+
*
|
|
5
|
+
* Single-shot wire (packSealed): [ iv (12) || ct+tag (plaintext + 16) ], total plaintext + 28
|
|
6
|
+
* Chunked: each chunk packed with its own IV and AAD "morok-chunk-${index}-of-${total}", same layout
|
|
7
|
+
*/
|
|
8
|
+
import { webcrypto } from 'node:crypto';
|
|
9
|
+
type CryptoKey = webcrypto.CryptoKey;
|
|
10
|
+
export interface SealedData {
|
|
11
|
+
iv: Uint8Array;
|
|
12
|
+
ct: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export declare const AES_IV_BYTES = 12;
|
|
15
|
+
export declare const AES_TAG_BYTES = 16;
|
|
16
|
+
/** Fresh AES-256-GCM key, extractable so callers can serialise the raw 32 bytes into the message envelope */
|
|
17
|
+
export declare function generateAesKey(): Promise<CryptoKey>;
|
|
18
|
+
/** Returns the raw 32 bytes. Throws on a non-extractable key */
|
|
19
|
+
export declare function exportKeyRaw(key: CryptoKey): Promise<Uint8Array>;
|
|
20
|
+
/** Re-import a raw 32-byte key for decrypt. Non-extractable */
|
|
21
|
+
export declare function importKeyForDecrypt(rawKey: Uint8Array): Promise<CryptoKey>;
|
|
22
|
+
/**
|
|
23
|
+
* Encrypt under `key` with a random IV. Optional `aad` binds extra bytes into the tag,
|
|
24
|
+
* the chunked path uses it to make chunks non-reorderable
|
|
25
|
+
*/
|
|
26
|
+
export declare function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array, aad?: Uint8Array): Promise<SealedData>;
|
|
27
|
+
/** Decrypt. Throws "decryption failed" on tag mismatch (same wording as the FE so log greps line up) */
|
|
28
|
+
export declare function aesGcmDecrypt(key: CryptoKey, sealed: SealedData, aad?: Uint8Array): Promise<Uint8Array>;
|
|
29
|
+
/** [ iv(12) || ct+tag ] */
|
|
30
|
+
export declare function packSealed(s: SealedData): Uint8Array;
|
|
31
|
+
/** Reverse of packSealed. Returns subarrays (views, no copy) */
|
|
32
|
+
export declare function unpackSealed(packed: Uint8Array): SealedData;
|
|
33
|
+
/** AAD for chunked uploads. Matches FE chunkAad */
|
|
34
|
+
export declare function chunkAad(index: number, total: number): Uint8Array;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=file-cipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-cipher.d.ts","sourceRoot":"","sources":["../../src/crypto/file-cipher.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAe,SAAS,EAAE,MAAM,aAAa,CAAA;AAGpD,KAAK,SAAS,GAAG,SAAS,CAAC,SAAS,CAAA;AAIpC,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,UAAU,CAAA;IACd,EAAE,EAAE,UAAU,CAAA;CACjB;AAED,eAAO,MAAM,YAAY,KAAM,CAAA;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAA;AAG/B,6GAA6G;AAC7G,wBAAsB,cAAc,IAAI,OAAO,CAAC,SAAS,CAAC,CAMzD;AAED,gEAAgE;AAChE,wBAAsB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAEtE;AAED,+DAA+D;AAC/D,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAWhF;AAGD;;;GAGG;AACH,wBAAsB,aAAa,CAC/B,GAAG,EAAQ,SAAS,EACpB,SAAS,EAAE,UAAU,EACrB,GAAG,CAAC,EAAO,UAAU,GACtB,OAAO,CAAC,UAAU,CAAC,CAYrB;AAED,wGAAwG;AACxG,wBAAsB,aAAa,CAC/B,GAAG,EAAK,SAAS,EACjB,MAAM,EAAE,UAAU,EAClB,GAAG,CAAC,EAAI,UAAU,GACnB,OAAO,CAAC,UAAU,CAAC,CAerB;AAGD,2BAA2B;AAC3B,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAQpD;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAS3D;AAGD,mDAAmD;AACnD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAEjE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomBytes, webcrypto } from 'node:crypto';
|
|
2
|
+
const subtle = webcrypto.subtle;
|
|
3
|
+
export const AES_IV_BYTES = 12;
|
|
4
|
+
export const AES_TAG_BYTES = 16;
|
|
5
|
+
export async function generateAesKey() {
|
|
6
|
+
return subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
7
|
+
}
|
|
8
|
+
export async function exportKeyRaw(key) {
|
|
9
|
+
return new Uint8Array(await subtle.exportKey('raw', key));
|
|
10
|
+
}
|
|
11
|
+
export async function importKeyForDecrypt(rawKey) {
|
|
12
|
+
if (rawKey.byteLength !== 32) {
|
|
13
|
+
throw new Error(`AES key must be 32 bytes, got ${rawKey.byteLength}`);
|
|
14
|
+
}
|
|
15
|
+
return subtle.importKey('raw', rawKey, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
16
|
+
}
|
|
17
|
+
export async function aesGcmEncrypt(key, plaintext, aad) {
|
|
18
|
+
const iv = randomBytes(AES_IV_BYTES);
|
|
19
|
+
const ct = await subtle.encrypt({
|
|
20
|
+
name: 'AES-GCM',
|
|
21
|
+
iv: iv,
|
|
22
|
+
...(aad ? { additionalData: aad } : {}),
|
|
23
|
+
}, key, plaintext);
|
|
24
|
+
return { iv: new Uint8Array(iv), ct: new Uint8Array(ct) };
|
|
25
|
+
}
|
|
26
|
+
export async function aesGcmDecrypt(key, sealed, aad) {
|
|
27
|
+
try {
|
|
28
|
+
const pt = await subtle.decrypt({
|
|
29
|
+
name: 'AES-GCM',
|
|
30
|
+
iv: sealed.iv,
|
|
31
|
+
...(aad ? { additionalData: aad } : {}),
|
|
32
|
+
}, key, sealed.ct);
|
|
33
|
+
return new Uint8Array(pt);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error('decryption failed');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function packSealed(s) {
|
|
40
|
+
if (s.iv.byteLength !== AES_IV_BYTES) {
|
|
41
|
+
throw new Error(`packSealed: iv must be ${AES_IV_BYTES} bytes, got ${s.iv.byteLength}`);
|
|
42
|
+
}
|
|
43
|
+
const out = new Uint8Array(s.iv.byteLength + s.ct.byteLength);
|
|
44
|
+
out.set(s.iv, 0);
|
|
45
|
+
out.set(s.ct, s.iv.byteLength);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function unpackSealed(packed) {
|
|
49
|
+
const min = AES_IV_BYTES + AES_TAG_BYTES;
|
|
50
|
+
if (packed.byteLength < min) {
|
|
51
|
+
throw new Error(`unpackSealed: too short (need >= ${min} bytes, got ${packed.byteLength})`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
iv: packed.subarray(0, AES_IV_BYTES),
|
|
55
|
+
ct: packed.subarray(AES_IV_BYTES),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function chunkAad(index, total) {
|
|
59
|
+
return new TextEncoder().encode(`morok-chunk-${index}-of-${total}`);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=file-cipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-cipher.js","sourceRoot":"","sources":["../../src/crypto/file-cipher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM seal/unseal for the "sealed-to-group-secret" bundles served alongside per-device
|
|
3
|
+
* channel-key wraps in GET /channel-key
|
|
4
|
+
* A member holding the group_secret of a version can unwrap any epoch sealed under it
|
|
5
|
+
* Byte-identical to frontend/src/signal/group-secret.ts
|
|
6
|
+
*
|
|
7
|
+
* Wire:
|
|
8
|
+
* aad = "morok-group-secret-${conv}-v${version}" (utf-8)
|
|
9
|
+
* iv = 12 random bytes
|
|
10
|
+
* plaintext = 32 bytes (legacy) OR [uint32 BE epoch (4) || key (32)]
|
|
11
|
+
* ct = AES-256-GCM(plaintext, key=group_secret(version), iv, aad)
|
|
12
|
+
*
|
|
13
|
+
* The version goes into the AAD, so a bundle sealed under v=5 can't be relabelled as v=6 on the wire
|
|
14
|
+
*/
|
|
15
|
+
export declare const GROUP_SECRET_BYTES = 32;
|
|
16
|
+
export declare const GROUP_SECRET_IV_BYTES = 12;
|
|
17
|
+
export declare const GROUP_SECRET_EPOCH_HEADER = 4;
|
|
18
|
+
export declare function groupSecretAad(conversationId: number, version: number): Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* Seal a 32-byte epoch key under group_secret(version). Plaintext is [uint32 BE predictedEpoch || epochKey] (36 bytes)
|
|
21
|
+
* The inner epoch claim binds the bundle to one epoch, a relabel on the wire fails the inner check on unseal
|
|
22
|
+
*
|
|
23
|
+
* predictedEpoch is the bot's guess at what the server will assign (usually localMax + 1)
|
|
24
|
+
* If it's wrong the receiver's inner check throws and ignores the bundle, the per-device wraps still land
|
|
25
|
+
*/
|
|
26
|
+
export declare function sealEpochKey(args: {
|
|
27
|
+
epochKey: Uint8Array;
|
|
28
|
+
groupSecret: Uint8Array;
|
|
29
|
+
conversationId: number;
|
|
30
|
+
version: number;
|
|
31
|
+
predictedEpoch: number;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
ciphertext: string;
|
|
34
|
+
iv: string;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Unseal an epoch bundle. AAD binds conversationId + version. With expectedEpoch and a new-format bundle
|
|
38
|
+
* (36 bytes) the inner epoch is verified, legacy 32-byte bundles skip that check
|
|
39
|
+
* Throws on wrong secret size, wrong iv size, AES-GCM tag fail, unknown plaintext length, inner-epoch mismatch
|
|
40
|
+
*/
|
|
41
|
+
export declare function unsealEpochKey(args: {
|
|
42
|
+
ciphertextBase64: string;
|
|
43
|
+
ivBase64: string;
|
|
44
|
+
groupSecret: Uint8Array;
|
|
45
|
+
conversationId: number;
|
|
46
|
+
version: number;
|
|
47
|
+
expectedEpoch?: number;
|
|
48
|
+
}): Promise<Uint8Array>;
|
|
49
|
+
//# sourceMappingURL=group-secret-cipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-secret-cipher.d.ts","sourceRoot":"","sources":["../../src/crypto/group-secret-cipher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,eAAO,MAAM,kBAAkB,KAAY,CAAA;AAC3C,eAAO,MAAM,qBAAqB,KAAS,CAAA;AAC3C,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAG1C,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,CAQlF;AAWD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACrC,QAAQ,EAAQ,UAAU,CAAA;IAC1B,WAAW,EAAK,UAAU,CAAA;IAC1B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAS,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;CACzB,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB9C;AAGD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACvC,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAU,MAAM,CAAA;IACxB,WAAW,EAAO,UAAU,CAAA;IAC5B,cAAc,EAAI,MAAM,CAAA;IACxB,OAAO,EAAW,MAAM,CAAA;IACxB,aAAa,CAAC,EAAI,MAAM,CAAA;CAC3B,GAAG,OAAO,CAAC,UAAU,CAAC,CAuCtB"}
|