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,69 @@
|
|
|
1
|
+
import { webcrypto } from 'node:crypto';
|
|
2
|
+
const subtle = webcrypto.subtle;
|
|
3
|
+
export const GROUP_SECRET_BYTES = 32;
|
|
4
|
+
export const GROUP_SECRET_IV_BYTES = 12;
|
|
5
|
+
export const GROUP_SECRET_EPOCH_HEADER = 4;
|
|
6
|
+
export function groupSecretAad(conversationId, version) {
|
|
7
|
+
if (!Number.isInteger(conversationId) || conversationId < 1) {
|
|
8
|
+
throw new Error(`group-secret-cipher: bad conversationId ${conversationId}`);
|
|
9
|
+
}
|
|
10
|
+
if (!Number.isInteger(version) || version < 0) {
|
|
11
|
+
throw new Error(`group-secret-cipher: bad version ${version}`);
|
|
12
|
+
}
|
|
13
|
+
return new TextEncoder().encode(`morok-group-secret-${conversationId}-v${version}`);
|
|
14
|
+
}
|
|
15
|
+
async function importKey(raw, usages) {
|
|
16
|
+
if (raw.byteLength !== GROUP_SECRET_BYTES) {
|
|
17
|
+
throw new Error(`group-secret-cipher: secret must be ${GROUP_SECRET_BYTES} bytes; got ${raw.byteLength}`);
|
|
18
|
+
}
|
|
19
|
+
return subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, usages);
|
|
20
|
+
}
|
|
21
|
+
export async function sealEpochKey(args) {
|
|
22
|
+
if (args.epochKey.byteLength !== GROUP_SECRET_BYTES) {
|
|
23
|
+
throw new Error(`group-secret-cipher: epoch key must be ${GROUP_SECRET_BYTES} bytes; got ${args.epochKey.byteLength}`);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isInteger(args.predictedEpoch) || args.predictedEpoch < 0 || args.predictedEpoch > 0xffffffff) {
|
|
26
|
+
throw new Error(`group-secret-cipher: predictedEpoch out of range: ${args.predictedEpoch}`);
|
|
27
|
+
}
|
|
28
|
+
const plaintext = new Uint8Array(GROUP_SECRET_EPOCH_HEADER + GROUP_SECRET_BYTES);
|
|
29
|
+
new DataView(plaintext.buffer).setUint32(0, args.predictedEpoch >>> 0, false);
|
|
30
|
+
plaintext.set(args.epochKey, GROUP_SECRET_EPOCH_HEADER);
|
|
31
|
+
const aad = groupSecretAad(args.conversationId, args.version);
|
|
32
|
+
const key = await importKey(args.groupSecret, ['encrypt']);
|
|
33
|
+
const iv = webcrypto.getRandomValues(new Uint8Array(GROUP_SECRET_IV_BYTES));
|
|
34
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv, additionalData: aad }, key, plaintext);
|
|
35
|
+
plaintext.fill(0);
|
|
36
|
+
return {
|
|
37
|
+
ciphertext: Buffer.from(ct).toString('base64'),
|
|
38
|
+
iv: Buffer.from(iv).toString('base64'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export async function unsealEpochKey(args) {
|
|
42
|
+
const iv = Buffer.from(args.ivBase64, 'base64');
|
|
43
|
+
if (iv.byteLength !== GROUP_SECRET_IV_BYTES) {
|
|
44
|
+
throw new Error(`group-secret-cipher: sealed iv must be ${GROUP_SECRET_IV_BYTES} bytes; got ${iv.byteLength}`);
|
|
45
|
+
}
|
|
46
|
+
const ct = Buffer.from(args.ciphertextBase64, 'base64');
|
|
47
|
+
const aad = groupSecretAad(args.conversationId, args.version);
|
|
48
|
+
const key = await importKey(args.groupSecret, ['decrypt']);
|
|
49
|
+
let pt;
|
|
50
|
+
try {
|
|
51
|
+
pt = await subtle.decrypt({ name: 'AES-GCM', iv, additionalData: aad }, key, ct);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new Error(`group-secret-cipher: unseal v${args.version} for conv ${args.conversationId} failed (tag mismatch or wrong secret): ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
const raw = new Uint8Array(pt);
|
|
57
|
+
if (raw.byteLength === GROUP_SECRET_BYTES) {
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
if (raw.byteLength !== GROUP_SECRET_BYTES + GROUP_SECRET_EPOCH_HEADER) {
|
|
61
|
+
throw new Error(`group-secret-cipher: unwrapped epoch key has unexpected length ${raw.byteLength}`);
|
|
62
|
+
}
|
|
63
|
+
const innerEpoch = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(0, false);
|
|
64
|
+
if (args.expectedEpoch !== undefined && innerEpoch !== args.expectedEpoch) {
|
|
65
|
+
throw new Error(`group-secret-cipher: inner epoch ${innerEpoch} does not match expected ${args.expectedEpoch}`);
|
|
66
|
+
}
|
|
67
|
+
return raw.subarray(GROUP_SECRET_EPOCH_HEADER);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=group-secret-cipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-secret-cipher.js","sourceRoot":"","sources":["../../src/crypto/group-secret-cipher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation group_secret state on disk at {stateDir}/group-secrets/<conversationId>.json
|
|
3
|
+
* Same disk layout and locking as channel-key-store, with "version" in place of "epoch"
|
|
4
|
+
* Persisted because the bot needs old versions to re-seal historical epochs after a rotate
|
|
5
|
+
* and to unseal old sealed bundles, a restart re-reads from disk
|
|
6
|
+
* { "currentVersion": N, "secrets": { "0": "<b64 32 bytes>", ... } }
|
|
7
|
+
*/
|
|
8
|
+
import type { SdkLogger } from '../types.js';
|
|
9
|
+
export interface GroupSecretState {
|
|
10
|
+
currentVersion: number;
|
|
11
|
+
/** version (int as decimal string) -> base64(32 bytes) */
|
|
12
|
+
secrets: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export declare class GroupSecretStore {
|
|
15
|
+
private readonly logger?;
|
|
16
|
+
private readonly dir;
|
|
17
|
+
private readonly chains;
|
|
18
|
+
constructor(stateDir: string, logger?: SdkLogger | undefined);
|
|
19
|
+
init(): Promise<void>;
|
|
20
|
+
load(conversationId: number): Promise<GroupSecretState | null>;
|
|
21
|
+
/** Merge (version, base64-secret) pairs. currentVersion tracks max. Creates the file if missing */
|
|
22
|
+
mergeVersions(conversationId: number, entries: Array<{
|
|
23
|
+
version: number;
|
|
24
|
+
secretBase64: string;
|
|
25
|
+
}>): Promise<GroupSecretState>;
|
|
26
|
+
/** Returns a fresh 32-byte copy, or null. Same isolation as ChannelKeyStore.getSecret */
|
|
27
|
+
getSecret(conversationId: number, version: number): Promise<Uint8Array | null>;
|
|
28
|
+
drop(conversationId: number): Promise<void>;
|
|
29
|
+
private pathFor;
|
|
30
|
+
private loadInner;
|
|
31
|
+
private parseState;
|
|
32
|
+
private saveInner;
|
|
33
|
+
private withLock;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=group-secret-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-secret-store.d.ts","sourceRoot":"","sources":["../../src/crypto/group-secret-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAAE,SAAS,EAAE,MAA4C,aAAa,CAAA;AAGlF,MAAM,WAAW,gBAAgB;IAC7B,cAAc,EAAE,MAAM,CAAA;IACtB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAGD,qBAAa,gBAAgB;IAIK,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;IAK3D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAYrB,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKpE,mGAAmG;IAC7F,aAAa,CACf,cAAc,EAAE,MAAM,EACtB,OAAO,EAAS,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,GACjE,OAAO,CAAC,gBAAgB,CAAC;IA+B5B,yFAAyF;IACnF,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAa9E,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjD,OAAO,CAAC,OAAO;YAQD,SAAS;IAuBvB,OAAO,CAAC,UAAU;YAYJ,SAAS;IAUvB,OAAO,CAAC,QAAQ;CAWnB"}
|
|
@@ -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 { GROUP_SECRET_BYTES } from './group-secret-cipher.js';
|
|
5
|
+
export class GroupSecretStore {
|
|
6
|
+
logger;
|
|
7
|
+
dir;
|
|
8
|
+
chains = new Map();
|
|
9
|
+
constructor(stateDir, logger) {
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.dir = join(stateDir, 'group-secrets');
|
|
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 mergeVersions(conversationId, entries) {
|
|
35
|
+
if (entries.length === 0) {
|
|
36
|
+
const existing = await this.load(conversationId);
|
|
37
|
+
return existing ?? { currentVersion: -1, secrets: {} };
|
|
38
|
+
}
|
|
39
|
+
for (const e of entries) {
|
|
40
|
+
if (!Number.isInteger(e.version) || e.version < 0 || e.version > 0xffffffff) {
|
|
41
|
+
throw new Error(`group-secret-store: bad version ${e.version}`);
|
|
42
|
+
}
|
|
43
|
+
const raw = Buffer.from(e.secretBase64, 'base64');
|
|
44
|
+
if (raw.byteLength !== GROUP_SECRET_BYTES) {
|
|
45
|
+
throw new Error(`group-secret-store: secret for v${e.version} decodes to ${raw.byteLength} bytes; expected ${GROUP_SECRET_BYTES}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.withLock(conversationId, async () => {
|
|
49
|
+
const cur = await this.loadInner(conversationId);
|
|
50
|
+
const next = cur
|
|
51
|
+
? { currentVersion: cur.currentVersion, secrets: { ...cur.secrets } }
|
|
52
|
+
: { currentVersion: -1, secrets: {} };
|
|
53
|
+
for (const e of entries) {
|
|
54
|
+
next.secrets[String(e.version)] = e.secretBase64;
|
|
55
|
+
if (e.version > next.currentVersion)
|
|
56
|
+
next.currentVersion = e.version;
|
|
57
|
+
}
|
|
58
|
+
await this.saveInner(conversationId, next);
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async getSecret(conversationId, version) {
|
|
63
|
+
const state = await this.load(conversationId);
|
|
64
|
+
if (!state)
|
|
65
|
+
return null;
|
|
66
|
+
const b64 = state.secrets[String(version)];
|
|
67
|
+
if (!b64)
|
|
68
|
+
return null;
|
|
69
|
+
const raw = Buffer.from(b64, 'base64');
|
|
70
|
+
if (raw.byteLength !== GROUP_SECRET_BYTES)
|
|
71
|
+
return null;
|
|
72
|
+
const out = new Uint8Array(GROUP_SECRET_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(`group-secret-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 }, '[group-secret-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.currentVersion !== 'number')
|
|
120
|
+
throw new Error('missing currentVersion');
|
|
121
|
+
if (!obj.secrets || typeof obj.secrets !== 'object')
|
|
122
|
+
throw new Error('missing secrets');
|
|
123
|
+
for (const k of Object.keys(obj.secrets)) {
|
|
124
|
+
if (typeof obj.secrets[k] !== 'string')
|
|
125
|
+
throw new Error(`v${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=group-secret-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-secret-store.js","sourceRoot":"","sources":["../../src/crypto/group-secret-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libsignal wrapper. Exposes only what the SDK uses: encrypt/decrypt, processPreKeyBundle for first
|
|
3
|
+
* contact with a new peer device, and mintOneTimePreKey/mintSignedPreKey for replenish
|
|
4
|
+
* libsignal-protocol-typescript picks up node:crypto via globalThis.crypto on Node 22+, no setWebCrypto
|
|
5
|
+
*/
|
|
6
|
+
import type { FileSignalStore } from './stores.js';
|
|
7
|
+
import type { SdkLogger } from '../types.js';
|
|
8
|
+
/** Prekey bundle from GET /prekeys/:userId/:deviceId */
|
|
9
|
+
export interface PreKeyBundle {
|
|
10
|
+
userId: number;
|
|
11
|
+
deviceId: number;
|
|
12
|
+
registrationId: number;
|
|
13
|
+
identityKey: string;
|
|
14
|
+
signedPreKey: {
|
|
15
|
+
keyId: number;
|
|
16
|
+
publicKey: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
};
|
|
19
|
+
oneTimePreKey: null | {
|
|
20
|
+
keyId: number;
|
|
21
|
+
publicKey: string;
|
|
22
|
+
};
|
|
23
|
+
accountSigningKey?: string | null;
|
|
24
|
+
deviceCertificate?: string | null;
|
|
25
|
+
}
|
|
26
|
+
export declare class SignalEngine {
|
|
27
|
+
private readonly store;
|
|
28
|
+
private readonly logger?;
|
|
29
|
+
/**
|
|
30
|
+
* Per-(peerUserId.peerDeviceId) serialisation around every session-mutating libsignal call
|
|
31
|
+
* (encrypt, decrypt, processPreKeyBundle)
|
|
32
|
+
* Without it, concurrent calls to the same device race the on-disk ratchet record and corrupt the session
|
|
33
|
+
* One shared lock map so encrypt blocks decrypt and the reverse for the same address
|
|
34
|
+
*/
|
|
35
|
+
private peerChains;
|
|
36
|
+
constructor(store: FileSignalStore, logger?: SdkLogger | undefined);
|
|
37
|
+
/** Run `fn` chained after any in-flight task for the same peer-device
|
|
38
|
+
* The get-and-set of the chain tail is synchronous so two callers in the same tick can't both see an empty prev */
|
|
39
|
+
withPeerLock<T>(peerUserId: number, peerDeviceId: number, fn: () => Promise<T>): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Encrypt bytes for a peer device. Returns a Signal envelope:
|
|
42
|
+
* { type: 1, body } WhisperMessage (continuing ratchet)
|
|
43
|
+
* { type: 3, body } PreKeySignalMessage (first frame, peer installs the session on receive)
|
|
44
|
+
*
|
|
45
|
+
* libsignal's MessageType.body is a binary string (charCodes 0-255 per byte),
|
|
46
|
+
* the wire format is base64 so we re-encode
|
|
47
|
+
* The caller must processPreKeyBundle the address first for a fresh session
|
|
48
|
+
*/
|
|
49
|
+
encrypt(peerUserId: number, peerDeviceId: number, plaintext: Uint8Array): Promise<{
|
|
50
|
+
type: 1 | 3;
|
|
51
|
+
body: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Decrypt an incoming envelope. Type-3 frames install the session on first receive (X3DH receiver path)
|
|
55
|
+
* ciphertextBody is base64 from the wire, decode to ArrayBuffer before handing to libsignal
|
|
56
|
+
* Passing it with 'binary' encoding makes libsignal read the base64 chars as bytes and corrupt everything
|
|
57
|
+
*/
|
|
58
|
+
decrypt(peerUserId: number, peerDeviceId: number, messageType: number, ciphertextBody: string): Promise<Uint8Array>;
|
|
59
|
+
/** Install a Signal session from a fresh prekey bundle. Don't call this with an open session,
|
|
60
|
+
* it overwrites the ratchet state, check hasOpenSession first
|
|
61
|
+
*
|
|
62
|
+
* Encrypt-side cross-signing gate: if the bundle carries both an account signing key and a per-device cert,
|
|
63
|
+
* the cert is verified over the canonical (userId,deviceId,identityKey) message before any session state is written
|
|
64
|
+
* A present-but-invalid cert is refused (tampered bundle or server identity injection)
|
|
65
|
+
* NULL on either side falls through to TOFU, and a curve-wrapper fault also returns 'uncertified',
|
|
66
|
+
* so a crypto-init failure never cuts the bot off */
|
|
67
|
+
processPreKeyBundle(bundle: PreKeyBundle): Promise<void>;
|
|
68
|
+
hasOpenSession(peerUserId: number, peerDeviceId: number): Promise<boolean>;
|
|
69
|
+
/** Generate a one-time prekey, persist locally, return the public half + keyId for upload */
|
|
70
|
+
mintOneTimePreKey(keyId: number): Promise<{
|
|
71
|
+
keyId: number;
|
|
72
|
+
publicKey: string;
|
|
73
|
+
}>;
|
|
74
|
+
/** Generate a signed prekey, sign with the bot's identity key, persist, return the public + signature for upload */
|
|
75
|
+
mintSignedPreKey(keyId: number): Promise<{
|
|
76
|
+
keyId: number;
|
|
77
|
+
publicKey: string;
|
|
78
|
+
signature: string;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/crypto/signal.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAY,aAAa,CAAA;AAclD,wDAAwD;AACxD,MAAM,WAAW,YAAY;IACzB,MAAM,EAAU,MAAM,CAAA;IACtB,QAAQ,EAAQ,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAK,MAAM,CAAA;IACtB,YAAY,EAAE;QACV,KAAK,EAAM,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,aAAa,EAAE,IAAI,GAAG;QAClB,KAAK,EAAM,MAAM,CAAA;QACjB,SAAS,EAAE,MAAM,CAAA;KACpB,CAAA;IACD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC;AAGD,qBAAa,YAAY;IAUjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAV5B;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAsC;gBAGnC,KAAK,EAAG,eAAe,EACvB,MAAM,CAAC,EAAE,SAAS,YAAA;IAGvC;uHACmH;IACnH,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAY3F;;;;;;;;OAQG;IACG,OAAO,CACT,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,GAChE,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAYzC;;;;OAIG;IACG,OAAO,CACT,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EACxC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAC5C,OAAO,CAAC,UAAU,CAAC;IAoCtB;;;;;;;0DAOsD;IAChD,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCxD,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQhF,6FAA6F;IACvF,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IASrF,oHAAoH;IAC9G,gBAAgB,CAClB,KAAK,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAetE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { KeyHelper, SessionBuilder, SessionCipher, SignalProtocolAddress, } from '@privacyresearch/libsignal-protocol-typescript';
|
|
2
|
+
import { verifyPeerCert } from './cross-signing.js';
|
|
3
|
+
function toArrayBuffer(b64) {
|
|
4
|
+
const buf = Buffer.from(b64, 'base64');
|
|
5
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
6
|
+
}
|
|
7
|
+
function fromArrayBuffer(buf) {
|
|
8
|
+
return Buffer.from(buf).toString('base64');
|
|
9
|
+
}
|
|
10
|
+
export class SignalEngine {
|
|
11
|
+
store;
|
|
12
|
+
logger;
|
|
13
|
+
peerChains = new Map();
|
|
14
|
+
constructor(store, logger) {
|
|
15
|
+
this.store = store;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
withPeerLock(peerUserId, peerDeviceId, fn) {
|
|
19
|
+
const key = `${peerUserId}.${peerDeviceId}`;
|
|
20
|
+
const prev = this.peerChains.get(key) ?? Promise.resolve();
|
|
21
|
+
const next = prev.then(fn, fn);
|
|
22
|
+
const tail = next.catch(() => { });
|
|
23
|
+
this.peerChains.set(key, tail);
|
|
24
|
+
void tail.then(() => { if (this.peerChains.get(key) === tail)
|
|
25
|
+
this.peerChains.delete(key); });
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
async encrypt(peerUserId, peerDeviceId, plaintext) {
|
|
29
|
+
const cipher = new SessionCipher(this.store, addr(peerUserId, peerDeviceId));
|
|
30
|
+
const msg = await cipher.encrypt(plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength));
|
|
31
|
+
const t = msg.type === 3 ? 3 : 1;
|
|
32
|
+
const body = msg.body
|
|
33
|
+
? Buffer.from(msg.body, 'binary').toString('base64')
|
|
34
|
+
: '';
|
|
35
|
+
return { type: t, body };
|
|
36
|
+
}
|
|
37
|
+
async decrypt(peerUserId, peerDeviceId, messageType, ciphertextBody) {
|
|
38
|
+
const cipher = new SessionCipher(this.store, addr(peerUserId, peerDeviceId));
|
|
39
|
+
const buf = Buffer.from(ciphertextBody, 'base64');
|
|
40
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
41
|
+
let plaintext;
|
|
42
|
+
if (messageType === 3) {
|
|
43
|
+
const peerAddr = `${peerUserId}.${peerDeviceId}`;
|
|
44
|
+
const before = await this.store.peekPeerIdentity(peerAddr);
|
|
45
|
+
plaintext = await cipher.decryptPreKeyWhisperMessage(ab);
|
|
46
|
+
if (before !== undefined) {
|
|
47
|
+
const after = await this.store.peekPeerIdentity(peerAddr);
|
|
48
|
+
if (after === undefined || !Buffer.from(before).equals(Buffer.from(after))) {
|
|
49
|
+
this.logger?.warn({ peerUserId, peerDeviceId }, '[signal] PEER IDENTITY CHANGED on a type-3 frame: a KNOWN peer presented a ' +
|
|
50
|
+
'new identity key and the session was silently re-pinned. This is a legit ' +
|
|
51
|
+
'reinstall/rotation OR a malicious-server impersonation attempt, verify before ' +
|
|
52
|
+
'trusting subsequent messages from this peer.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (messageType === 1) {
|
|
57
|
+
plaintext = await cipher.decryptWhisperMessage(ab);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`unsupported DM message type ${messageType} (expected 1 or 3)`);
|
|
61
|
+
}
|
|
62
|
+
return new Uint8Array(plaintext);
|
|
63
|
+
}
|
|
64
|
+
async processPreKeyBundle(bundle) {
|
|
65
|
+
const certStatus = await verifyPeerCert({
|
|
66
|
+
userId: bundle.userId,
|
|
67
|
+
deviceId: bundle.deviceId,
|
|
68
|
+
identityKeyB64: bundle.identityKey,
|
|
69
|
+
accountSigningKey: bundle.accountSigningKey,
|
|
70
|
+
deviceCertificate: bundle.deviceCertificate,
|
|
71
|
+
}, this.logger);
|
|
72
|
+
if (certStatus === 'invalid') {
|
|
73
|
+
throw new Error(`processPreKeyBundle: peer ${bundle.userId}.${bundle.deviceId} presented an INVALID ` +
|
|
74
|
+
`cross-signing certificate - refusing to open a session (tampered bundle or server ` +
|
|
75
|
+
`identity injection)`);
|
|
76
|
+
}
|
|
77
|
+
const builder = new SessionBuilder(this.store, addr(bundle.userId, bundle.deviceId));
|
|
78
|
+
const device = {
|
|
79
|
+
identityKey: toArrayBuffer(bundle.identityKey),
|
|
80
|
+
registrationId: bundle.registrationId,
|
|
81
|
+
signedPreKey: {
|
|
82
|
+
keyId: bundle.signedPreKey.keyId,
|
|
83
|
+
publicKey: toArrayBuffer(bundle.signedPreKey.publicKey),
|
|
84
|
+
signature: toArrayBuffer(bundle.signedPreKey.signature),
|
|
85
|
+
},
|
|
86
|
+
preKey: bundle.oneTimePreKey ? {
|
|
87
|
+
keyId: bundle.oneTimePreKey.keyId,
|
|
88
|
+
publicKey: toArrayBuffer(bundle.oneTimePreKey.publicKey),
|
|
89
|
+
} : undefined,
|
|
90
|
+
};
|
|
91
|
+
await builder.processPreKey(device);
|
|
92
|
+
}
|
|
93
|
+
async hasOpenSession(peerUserId, peerDeviceId) {
|
|
94
|
+
const cipher = new SessionCipher(this.store, addr(peerUserId, peerDeviceId));
|
|
95
|
+
return cipher.hasOpenSession();
|
|
96
|
+
}
|
|
97
|
+
async mintOneTimePreKey(keyId) {
|
|
98
|
+
const kp = await KeyHelper.generatePreKey(keyId);
|
|
99
|
+
await this.store.storePreKey(kp.keyId, kp.keyPair);
|
|
100
|
+
return {
|
|
101
|
+
keyId: kp.keyId,
|
|
102
|
+
publicKey: fromArrayBuffer(kp.keyPair.pubKey),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async mintSignedPreKey(keyId) {
|
|
106
|
+
const idPair = await this.store.getIdentityKeyPair();
|
|
107
|
+
if (!idPair)
|
|
108
|
+
throw new Error('identity key pair missing - bot state not imported');
|
|
109
|
+
const spk = await KeyHelper.generateSignedPreKey(idPair, keyId);
|
|
110
|
+
await this.store.storeSignedPreKeyFull({
|
|
111
|
+
keyId: spk.keyId,
|
|
112
|
+
keyPair: spk.keyPair,
|
|
113
|
+
signature: spk.signature,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
keyId: spk.keyId,
|
|
117
|
+
publicKey: fromArrayBuffer(spk.keyPair.pubKey),
|
|
118
|
+
signature: fromArrayBuffer(spk.signature),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function addr(userId, deviceId) {
|
|
123
|
+
return new SignalProtocolAddress(String(userId), deviceId);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=signal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/crypto/signal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed Signal Protocol stores. Implements libsignal's StorageType on a directory layout
|
|
3
|
+
* Atomic writes (write-then-rename) so a crash mid-update can't corrupt state
|
|
4
|
+
* fsck on boot moves broken session/prekey files to quarantine/ so one bad file doesn't refuse the whole bot
|
|
5
|
+
*
|
|
6
|
+
* Layout under stateDir:
|
|
7
|
+
* identity.json own keypair + reg id + ids
|
|
8
|
+
* state.json bookkeeping (SPK rotation ts, next ids)
|
|
9
|
+
* sessions/<userId>.<deviceId>.json per-peer-device session
|
|
10
|
+
* prekeys/signed-<keyId>.json signed prekeys
|
|
11
|
+
* prekeys/onetime-<keyId>.json one-time prekeys
|
|
12
|
+
* identity-cache/<userId>.<deviceId>.json peer identity for TOFU
|
|
13
|
+
* quarantine/ moved here on parse failure
|
|
14
|
+
*
|
|
15
|
+
* Everything created 0o700 / 0o600, anyone with read on this dir can impersonate the bot
|
|
16
|
+
*/
|
|
17
|
+
import type { Direction, KeyPairType, SessionRecordType, StorageType } from '@privacyresearch/libsignal-protocol-typescript';
|
|
18
|
+
interface SerializedKeyPair {
|
|
19
|
+
pub: string;
|
|
20
|
+
priv: string;
|
|
21
|
+
}
|
|
22
|
+
export interface PersistedIdentity {
|
|
23
|
+
identityKeyPair: SerializedKeyPair;
|
|
24
|
+
registrationId: number;
|
|
25
|
+
/** Set on first /auth/bot-session, absent before first start */
|
|
26
|
+
userId?: number;
|
|
27
|
+
/** Always 1 for bots */
|
|
28
|
+
deviceId?: number;
|
|
29
|
+
accountSigningKey?: SerializedKeyPair;
|
|
30
|
+
}
|
|
31
|
+
interface PersistedState {
|
|
32
|
+
/** Unix ms of last signed-prekey rotation. 0 = never */
|
|
33
|
+
lastSignedPreKeyRotationMs: number;
|
|
34
|
+
/** Next OTPK id to mint. Monotonic */
|
|
35
|
+
nextOneTimePreKeyId: number;
|
|
36
|
+
/** Next signed-prekey id, bumps every rotation */
|
|
37
|
+
nextSignedPreKeyId: number;
|
|
38
|
+
}
|
|
39
|
+
export interface FsckResult {
|
|
40
|
+
quarantinedSessions: string[];
|
|
41
|
+
quarantinedPreKeys: string[];
|
|
42
|
+
}
|
|
43
|
+
declare const DIRECTION_ENUM: {
|
|
44
|
+
readonly SENDING: 1;
|
|
45
|
+
readonly RECEIVING: 2;
|
|
46
|
+
};
|
|
47
|
+
export declare class FileSignalStore implements StorageType {
|
|
48
|
+
private readonly stateDir;
|
|
49
|
+
readonly Direction: typeof DIRECTION_ENUM;
|
|
50
|
+
private identityCache?;
|
|
51
|
+
private stateCache?;
|
|
52
|
+
constructor(stateDir: string);
|
|
53
|
+
/** Create the directory tree with safe perms. Idempotent */
|
|
54
|
+
ensureLayout(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Idempotent import of a parsed .morokbot. A no-op if identity.json already exists with matching userId/regId,
|
|
57
|
+
* throws if the stateDir holds a different bot
|
|
58
|
+
*
|
|
59
|
+
* Write order matters: state.json, signed prekey, OTPKs, then identity.json
|
|
60
|
+
* last identity.json is the "import complete" marker, a crash before it lands makes the next start re-import
|
|
61
|
+
*/
|
|
62
|
+
importInitial(opts: {
|
|
63
|
+
botUserId: number;
|
|
64
|
+
registrationId: number;
|
|
65
|
+
deviceId: number;
|
|
66
|
+
identityKeyPair: SerializedKeyPair;
|
|
67
|
+
accountSigningKey?: SerializedKeyPair;
|
|
68
|
+
signedPreKey: {
|
|
69
|
+
keyId: number;
|
|
70
|
+
pub: string;
|
|
71
|
+
priv: string;
|
|
72
|
+
signature: string;
|
|
73
|
+
};
|
|
74
|
+
oneTimePreKeys: Array<{
|
|
75
|
+
keyId: number;
|
|
76
|
+
pub: string;
|
|
77
|
+
priv: string;
|
|
78
|
+
}>;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Walk sessions and prekeys, move anything that fails to parse to quarantine/
|
|
82
|
+
* The bot keeps booting, a lost session re-X3DHs when the peer next sends
|
|
83
|
+
*/
|
|
84
|
+
fsck(): Promise<FsckResult>;
|
|
85
|
+
getIdentityKeyPair(): Promise<KeyPairType | undefined>;
|
|
86
|
+
getLocalRegistrationId(): Promise<number | undefined>;
|
|
87
|
+
/** TOFU, first sight of a peer's identity key is accepted and later changes are rejected
|
|
88
|
+
* A rejected identity surfaces as a silent decrypt failure, the SDK installs a fresh session
|
|
89
|
+
* from the peer's next PreKeySignalMessage */
|
|
90
|
+
isTrustedIdentity(identifier: string, identityKey: ArrayBuffer, _direction: Direction): Promise<boolean>;
|
|
91
|
+
saveIdentity(encodedAddress: string, publicKey: ArrayBuffer, _nonblockingApproval?: boolean): Promise<boolean>;
|
|
92
|
+
/** Public read of a peer's currently-pinned identity (no TOFU logic), so SignalEngine.decrypt can detect
|
|
93
|
+
* a silent type-3 re-pin. The vendored libsignal type-3 receiver path doesn't enforce the changed-identity check
|
|
94
|
+
* (it calls isTrustedIdentity without awaiting), the encrypt path does
|
|
95
|
+
* Returns undefined for a never-seen peer */
|
|
96
|
+
peekPeerIdentity(encodedAddress: string): Promise<ArrayBuffer | undefined>;
|
|
97
|
+
loadSession(encodedAddress: string): Promise<SessionRecordType | undefined>;
|
|
98
|
+
storeSession(encodedAddress: string, record: SessionRecordType): Promise<void>;
|
|
99
|
+
loadPreKey(keyId: number | string): Promise<KeyPairType | undefined>;
|
|
100
|
+
storePreKey(keyId: number | string, keyPair: KeyPairType): Promise<void>;
|
|
101
|
+
removePreKey(keyId: number | string): Promise<void>;
|
|
102
|
+
loadSignedPreKey(keyId: number | string): Promise<KeyPairType | undefined>;
|
|
103
|
+
storeSignedPreKey(keyId: number | string, keyPair: KeyPairType): Promise<void>;
|
|
104
|
+
removeSignedPreKey(keyId: number | string): Promise<void>;
|
|
105
|
+
/** Write full SPK record including signature (for replenish) */
|
|
106
|
+
storeSignedPreKeyFull(rec: {
|
|
107
|
+
keyId: number;
|
|
108
|
+
keyPair: KeyPairType;
|
|
109
|
+
signature: ArrayBuffer;
|
|
110
|
+
}): Promise<void>;
|
|
111
|
+
getSignedPreKeyRecord(keyId: number): Promise<{
|
|
112
|
+
keyId: number;
|
|
113
|
+
pub: string;
|
|
114
|
+
priv: string;
|
|
115
|
+
signature?: string;
|
|
116
|
+
} | undefined>;
|
|
117
|
+
listOneTimePreKeyIds(): Promise<number[]>;
|
|
118
|
+
loadState(): Promise<PersistedState>;
|
|
119
|
+
patchState(patch: Partial<PersistedState>): Promise<void>;
|
|
120
|
+
private loadPersistedIdentity;
|
|
121
|
+
private loadPeerIdentity;
|
|
122
|
+
private identityFile;
|
|
123
|
+
private stateFile;
|
|
124
|
+
private sessionFile;
|
|
125
|
+
private identityCacheFile;
|
|
126
|
+
private signedPreKeyFile;
|
|
127
|
+
private oneTimePreKeyFile;
|
|
128
|
+
}
|
|
129
|
+
export {};
|
|
130
|
+
//# sourceMappingURL=stores.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stores.d.ts","sourceRoot":"","sources":["../../src/crypto/stores.ts"],"names":[],"mappings":"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"}
|