@oxidezap/baileyrs 0.0.8 → 0.0.10
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/lib/Bridge/__tests__/dts-drift.test.js +1 -3
- package/lib/Bridge/__tests__/dts-drift.test.js.map +1 -1
- package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts +73 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts.map +1 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.js +113 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts +10 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js +291 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts +4 -19
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js +31 -97
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts +6 -27
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js +49 -177
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts +10 -28
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js +116 -299
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js.map +1 -1
- package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
- package/lib/Utils/wrap-legacy-store.js +142 -289
- package/lib/Utils/wrap-legacy-store.js.map +1 -1
- package/package.json +4 -1
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* •
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* • key: `{signalUser}.{deviceId}` e.g. "100000037037034_1.0"
|
|
13
|
-
* where signalUser = `${user}` for s.whatsapp.net (domainType 0),
|
|
14
|
-
* `${user}_${domainType}` otherwise
|
|
15
|
-
* • value: a plain JS object (NOT bytes!) with shape
|
|
16
|
-
* `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`
|
|
17
|
-
* The keys store is responsible for JSON-stringifying it on disk.
|
|
18
|
-
*
|
|
19
|
-
* Two diffs at the boundary:
|
|
20
|
-
* 1. KEY format: bridge `addr@server.dev` ↔ upstream `signalUser.deviceId`
|
|
21
|
-
* 2. VALUE format: proto bytes ↔ JS object (with base64-encoded byte fields)
|
|
22
|
-
*
|
|
23
|
-
* If wrap-legacy-store doesn't translate, upstream's `loadSession` either
|
|
24
|
-
* (a) doesn't find the key at all, OR
|
|
25
|
-
* (b) finds it (Buffer of proto bytes) and throws inside SessionRecord.deserialize
|
|
26
|
-
* → returns null → falls back to fresh PreKey signal handshake. The e2e
|
|
27
|
-
* 1:1 phases still pass via that fallback, but every cross-impl swap then
|
|
28
|
-
* costs an extra ~3 wire roundtrips per peer pair. These tests pin down
|
|
29
|
-
* the contract for direct, no-re-handshake compatibility.
|
|
2
|
+
* Cross-impl gap for `session`:
|
|
3
|
+
* • Bridge: store `session`, key `{user}[:dev]@{server}.{sig}`,
|
|
4
|
+
* value = protobuf `RecordStructure` (current + previous_sessions).
|
|
5
|
+
* • Upstream: store `session`, key `{signalUser}.{deviceId}`,
|
|
6
|
+
* value = JS object `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`.
|
|
7
|
+
*
|
|
8
|
+
* Without conversion, upstream's `loadSession` either misses the key or
|
|
9
|
+
* throws inside `SessionRecord.deserialize`, falling back to a fresh
|
|
10
|
+
* PreKey signal handshake on every cross-impl swap. These tests pin
|
|
11
|
+
* direct, no-re-handshake compatibility.
|
|
30
12
|
*/
|
|
31
13
|
export {};
|
|
32
14
|
//# sourceMappingURL=wrap-legacy-store-session.test.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-legacy-store-session.test.d.ts","sourceRoot":"","sources":["../../../src/Utils/__tests__/wrap-legacy-store-session.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"wrap-legacy-store-session.test.d.ts","sourceRoot":"","sources":["../../../src/Utils/__tests__/wrap-legacy-store-session.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -1,150 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cross-impl gap for `session`:
|
|
3
|
+
* • Bridge: store `session`, key `{user}[:dev]@{server}.{sig}`,
|
|
4
|
+
* value = protobuf `RecordStructure` (current + previous_sessions).
|
|
5
|
+
* • Upstream: store `session`, key `{signalUser}.{deviceId}`,
|
|
6
|
+
* value = JS object `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (current_session + previous_sessions, both `SessionStructure`)
|
|
9
|
-
*
|
|
10
|
-
* Upstream baileys (JS libsignal) writes:
|
|
11
|
-
* • store: `session` (same name, so no rename)
|
|
12
|
-
* • key: `{signalUser}.{deviceId}` e.g. "100000037037034_1.0"
|
|
13
|
-
* where signalUser = `${user}` for s.whatsapp.net (domainType 0),
|
|
14
|
-
* `${user}_${domainType}` otherwise
|
|
15
|
-
* • value: a plain JS object (NOT bytes!) with shape
|
|
16
|
-
* `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`
|
|
17
|
-
* The keys store is responsible for JSON-stringifying it on disk.
|
|
18
|
-
*
|
|
19
|
-
* Two diffs at the boundary:
|
|
20
|
-
* 1. KEY format: bridge `addr@server.dev` ↔ upstream `signalUser.deviceId`
|
|
21
|
-
* 2. VALUE format: proto bytes ↔ JS object (with base64-encoded byte fields)
|
|
22
|
-
*
|
|
23
|
-
* If wrap-legacy-store doesn't translate, upstream's `loadSession` either
|
|
24
|
-
* (a) doesn't find the key at all, OR
|
|
25
|
-
* (b) finds it (Buffer of proto bytes) and throws inside SessionRecord.deserialize
|
|
26
|
-
* → returns null → falls back to fresh PreKey signal handshake. The e2e
|
|
27
|
-
* 1:1 phases still pass via that fallback, but every cross-impl swap then
|
|
28
|
-
* costs an extra ~3 wire roundtrips per peer pair. These tests pin down
|
|
29
|
-
* the contract for direct, no-re-handshake compatibility.
|
|
8
|
+
* Without conversion, upstream's `loadSession` either misses the key or
|
|
9
|
+
* throws inside `SessionRecord.deserialize`, falling back to a fresh
|
|
10
|
+
* PreKey signal handshake on every cross-impl swap. These tests pin
|
|
11
|
+
* direct, no-re-handshake compatibility.
|
|
30
12
|
*/
|
|
31
13
|
import { Buffer } from 'node:buffer';
|
|
32
14
|
import { describe, test } from 'node:test';
|
|
33
|
-
// libsignal has no .d.ts; importing the JS module directly is fine for a
|
|
34
|
-
// node-test runtime, we just have to widen the type at the boundary.
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
36
|
-
// @ts-expect-error -- no types
|
|
37
15
|
import SessionRecord from 'libsignal/src/session_record.js';
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge proto-types ambient
|
|
39
16
|
import { proto as bridgeProto } from 'whatsapp-rust-bridge/proto-types';
|
|
40
|
-
import { wrapLegacyStore } from "../wrap-legacy-store.js";
|
|
41
|
-
import { initAuthCreds } from "../generics.js";
|
|
42
17
|
import { expect } from "../../__tests__/expect.js";
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
raw: data,
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
-
async get(type, ids) {
|
|
50
|
-
const bucket = data[type] ?? {};
|
|
51
|
-
const out = {};
|
|
52
|
-
for (const id of ids)
|
|
53
|
-
out[id] = bucket[id] ?? null;
|
|
54
|
-
return out;
|
|
55
|
-
},
|
|
56
|
-
async set(updates) {
|
|
57
|
-
for (const [type, bucket] of Object.entries(updates)) {
|
|
58
|
-
data[type] ?? (data[type] = {});
|
|
59
|
-
for (const [id, val] of Object.entries(bucket)) {
|
|
60
|
-
if (val === null || val === undefined)
|
|
61
|
-
delete data[type][id];
|
|
62
|
-
else
|
|
63
|
-
data[type][id] = val;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
async function makeWrapped() {
|
|
70
|
-
const creds = initAuthCreds();
|
|
71
|
-
const keys = makeMemKeys();
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
-
const state = { creds, keys: keys };
|
|
74
|
-
const wrapped = await wrapLegacyStore(state, async () => { });
|
|
75
|
-
return { wrapped, keys, creds };
|
|
76
|
-
}
|
|
77
|
-
const fill = (n, base) => new Uint8Array(n).map((_, i) => (i * base + base) & 0xff);
|
|
78
|
-
/** Build a realistic, fully-populated `RecordStructure` proto byte blob —
|
|
79
|
-
* matching what Rust libsignal would produce for an established Signal
|
|
80
|
-
* session that has both a sender chain and a receiver chain. */
|
|
81
|
-
function buildBridgeSessionBytes(opts) {
|
|
82
|
-
const { rootKey = fill(32, 1), previousCounter = 0, senderRatchetPub = fill(33, 2), senderRatchetPriv = fill(32, 3), senderChainKey = fill(32, 4), senderChainIndex = 0, receiverRatchetPub = fill(33, 5), receiverChainKey = fill(32, 6), receiverChainIndex = 1, aliceBaseKey = fill(33, 7), remoteIdentity = fill(33, 8), remoteRegistrationId = 1234, localRegistrationId = 5678 } = opts;
|
|
83
|
-
const session = bridgeProto.SessionStructure.create({
|
|
84
|
-
sessionVersion: 3,
|
|
85
|
-
localIdentityPublic: fill(33, 9),
|
|
86
|
-
remoteIdentityPublic: remoteIdentity,
|
|
87
|
-
rootKey,
|
|
88
|
-
previousCounter,
|
|
89
|
-
senderChain: {
|
|
90
|
-
senderRatchetKey: senderRatchetPub,
|
|
91
|
-
senderRatchetKeyPrivate: senderRatchetPriv,
|
|
92
|
-
chainKey: { index: senderChainIndex, key: senderChainKey },
|
|
93
|
-
messageKeys: []
|
|
94
|
-
},
|
|
95
|
-
receiverChains: [
|
|
96
|
-
{
|
|
97
|
-
senderRatchetKey: receiverRatchetPub,
|
|
98
|
-
chainKey: { index: receiverChainIndex, key: receiverChainKey },
|
|
99
|
-
messageKeys: []
|
|
100
|
-
}
|
|
101
|
-
],
|
|
102
|
-
remoteRegistrationId,
|
|
103
|
-
localRegistrationId,
|
|
104
|
-
aliceBaseKey
|
|
105
|
-
});
|
|
106
|
-
const record = bridgeProto.RecordStructure.create({
|
|
107
|
-
currentSession: session,
|
|
108
|
-
previousSessions: []
|
|
109
|
-
});
|
|
110
|
-
return bridgeProto.RecordStructure.encode(record).finish();
|
|
111
|
-
}
|
|
112
|
-
const SAMPLE_BRIDGE_KEY_LID = '100000037037034@lid.0';
|
|
113
|
-
const EXPECTED_UPSTREAM_KEY_LID = '100000037037034_1.0';
|
|
114
|
-
const SAMPLE_BRIDGE_KEY_PN = '559980000003@s.whatsapp.net.0';
|
|
115
|
-
const EXPECTED_UPSTREAM_KEY_PN = '559980000003.0';
|
|
116
|
-
const SAMPLE_BRIDGE_KEY_PN_DEV = '559980000003:5@s.whatsapp.net.0';
|
|
117
|
-
const EXPECTED_UPSTREAM_KEY_PN_DEV = '559980000003.5';
|
|
118
|
-
// ── Tests ────────────────────────────────────────────────────────────────
|
|
119
|
-
describe('wrap-legacy-store: session key-name translation (cross-impl)', () => {
|
|
120
|
-
test('bridge SET writes under the upstream-formatted key (LID)', async () => {
|
|
18
|
+
import { BRIDGE_SESSION_KEY_LID, BRIDGE_SESSION_KEY_PN, BRIDGE_SESSION_KEY_PN_DEV, UPSTREAM_SESSION_KEY_LID, UPSTREAM_SESSION_KEY_PN, UPSTREAM_SESSION_KEY_PN_DEV, buildBridgeSessionBytes, fill, makeWrapped } from "./_legacy-store-fixtures.js";
|
|
19
|
+
describe('wrap-legacy-store: session key-name translation', () => {
|
|
20
|
+
test('LID → upstream `_1.deviceId`', async () => {
|
|
121
21
|
const { wrapped, keys } = await makeWrapped();
|
|
122
|
-
await wrapped.set('session',
|
|
123
|
-
expect(keys.raw['session']?.[
|
|
124
|
-
|
|
125
|
-
// `loadSession(<upstreamKey>)` won't find anything.
|
|
126
|
-
expect(keys.raw['session']?.[SAMPLE_BRIDGE_KEY_LID]).toBeUndefined();
|
|
22
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, buildBridgeSessionBytes());
|
|
23
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID]).toBeDefined();
|
|
24
|
+
expect(keys.raw['session']?.[BRIDGE_SESSION_KEY_LID]).toBeUndefined();
|
|
127
25
|
});
|
|
128
|
-
test('
|
|
26
|
+
test('PN no-device → bare `{user}.{signalDev}`', async () => {
|
|
129
27
|
const { wrapped, keys } = await makeWrapped();
|
|
130
|
-
await wrapped.set('session',
|
|
131
|
-
expect(keys.raw['session']?.[
|
|
28
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, buildBridgeSessionBytes());
|
|
29
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN]).toBeDefined();
|
|
132
30
|
});
|
|
133
|
-
test('
|
|
31
|
+
test('PN with JID device → device suffix preserved', async () => {
|
|
134
32
|
const { wrapped, keys } = await makeWrapped();
|
|
135
|
-
await wrapped.set('session',
|
|
136
|
-
expect(keys.raw['session']?.[
|
|
33
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN_DEV, buildBridgeSessionBytes());
|
|
34
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN_DEV]).toBeDefined();
|
|
137
35
|
});
|
|
138
36
|
});
|
|
139
|
-
describe('wrap-legacy-store: session value-byte translation
|
|
140
|
-
test('bridge
|
|
37
|
+
describe('wrap-legacy-store: session value-byte translation', () => {
|
|
38
|
+
test('bridge proto bytes load via upstream `SessionRecord.deserialize` as OPEN', async () => {
|
|
141
39
|
const { wrapped, keys } = await makeWrapped();
|
|
142
|
-
const aliceBaseKey = Buffer.from(
|
|
143
|
-
const rootKey = Buffer.from(
|
|
144
|
-
const remoteIdentity = Buffer.from(
|
|
145
|
-
const senderRatchetPub = Buffer.from(
|
|
146
|
-
const senderRatchetPriv = Buffer.from(
|
|
147
|
-
await wrapped.set('session',
|
|
40
|
+
const aliceBaseKey = Buffer.from(fill(33, 100));
|
|
41
|
+
const rootKey = Buffer.from(fill(32, 11));
|
|
42
|
+
const remoteIdentity = Buffer.from(fill(33, 17));
|
|
43
|
+
const senderRatchetPub = Buffer.from(fill(33, 23));
|
|
44
|
+
const senderRatchetPriv = Buffer.from(fill(32, 29));
|
|
45
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, buildBridgeSessionBytes({
|
|
148
46
|
rootKey: new Uint8Array(rootKey),
|
|
149
47
|
aliceBaseKey: new Uint8Array(aliceBaseKey),
|
|
150
48
|
remoteIdentity: new Uint8Array(remoteIdentity),
|
|
@@ -153,15 +51,10 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
153
51
|
remoteRegistrationId: 99,
|
|
154
52
|
previousCounter: 4
|
|
155
53
|
}));
|
|
156
|
-
const stored = keys.raw['session']?.[
|
|
157
|
-
expect(stored).toBeDefined();
|
|
158
|
-
// THE call upstream's loadSession makes. If it throws or produces a
|
|
159
|
-
// session with no open entries, the regression isn't fixed.
|
|
160
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- libsignal lacks .d.ts
|
|
54
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
161
55
|
const rec = SessionRecord.deserialize(stored);
|
|
162
56
|
expect(rec.haveOpenSession()).toBe(true);
|
|
163
57
|
const open = rec.getOpenSession();
|
|
164
|
-
expect(open).toBeDefined();
|
|
165
58
|
expect(open.registrationId).toBe(99);
|
|
166
59
|
expect(open.currentRatchet.previousCounter).toBe(4);
|
|
167
60
|
expect(Buffer.from(open.currentRatchet.rootKey).equals(rootKey)).toBe(true);
|
|
@@ -169,54 +62,44 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
169
62
|
expect(Buffer.from(open.currentRatchet.ephemeralKeyPair.privKey).equals(senderRatchetPriv)).toBe(true);
|
|
170
63
|
expect(Buffer.from(open.indexInfo.baseKey).equals(aliceBaseKey)).toBe(true);
|
|
171
64
|
expect(Buffer.from(open.indexInfo.remoteIdentityKey).equals(remoteIdentity)).toBe(true);
|
|
172
|
-
// indexInfo.closed === -1 means the session is OPEN; cross-impl
|
|
173
|
-
// swap of an open session is the whole point.
|
|
174
65
|
expect(open.indexInfo.closed).toBe(-1);
|
|
175
|
-
// We expect at least one chain populated (sender + receiver).
|
|
176
66
|
expect(Object.keys(open._chains).length).toBeGreaterThanOrEqual(1);
|
|
177
67
|
});
|
|
178
|
-
test('upstream
|
|
68
|
+
test('upstream JS object → bridge GET produces valid proto with same fields', async () => {
|
|
179
69
|
const { wrapped, keys } = await makeWrapped();
|
|
180
|
-
// Build a SessionRecord the upstream way (start from a synthetic
|
|
181
|
-
// session shape, since libsignal exposes no public constructor that
|
|
182
|
-
// fully populates the entry).
|
|
183
70
|
const baseKey = Buffer.alloc(33, 0xa0);
|
|
184
71
|
const rootKey = Buffer.alloc(32, 0xb0);
|
|
185
72
|
const remoteIdent = Buffer.alloc(33, 0xc0);
|
|
186
73
|
const ephPub = Buffer.alloc(33, 0xd0);
|
|
187
74
|
const ephPriv = Buffer.alloc(32, 0xe0);
|
|
188
75
|
const lastRemoteEph = Buffer.alloc(33, 0xf0);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
pubKey: ephPub.toString('base64'),
|
|
196
|
-
|
|
76
|
+
keys.raw['session'] = {
|
|
77
|
+
[UPSTREAM_SESSION_KEY_LID]: {
|
|
78
|
+
_sessions: {
|
|
79
|
+
[baseKey.toString('base64')]: {
|
|
80
|
+
registrationId: 4242,
|
|
81
|
+
currentRatchet: {
|
|
82
|
+
ephemeralKeyPair: { pubKey: ephPub.toString('base64'), privKey: ephPriv.toString('base64') },
|
|
83
|
+
lastRemoteEphemeralKey: lastRemoteEph.toString('base64'),
|
|
84
|
+
previousCounter: 7,
|
|
85
|
+
rootKey: rootKey.toString('base64')
|
|
197
86
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
version: 'v1'
|
|
87
|
+
indexInfo: {
|
|
88
|
+
baseKey: baseKey.toString('base64'),
|
|
89
|
+
baseKeyType: 1,
|
|
90
|
+
closed: -1,
|
|
91
|
+
used: Date.now(),
|
|
92
|
+
created: Date.now(),
|
|
93
|
+
remoteIdentityKey: remoteIdent.toString('base64')
|
|
94
|
+
},
|
|
95
|
+
_chains: {}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
version: 'v1'
|
|
99
|
+
}
|
|
214
100
|
};
|
|
215
|
-
|
|
216
|
-
const protoOut = (await wrapped.get('session', SAMPLE_BRIDGE_KEY_LID));
|
|
217
|
-
expect(protoOut).not.toBe(null);
|
|
101
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
218
102
|
const decoded = bridgeProto.RecordStructure.decode(protoOut);
|
|
219
|
-
expect(decoded.currentSession).toBeDefined();
|
|
220
103
|
const cs = decoded.currentSession;
|
|
221
104
|
expect(cs.remoteRegistrationId).toBe(4242);
|
|
222
105
|
expect(cs.previousCounter).toBe(7);
|
|
@@ -226,32 +109,23 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
226
109
|
expect(Buffer.from(cs.senderChain.senderRatchetKey).equals(ephPub)).toBe(true);
|
|
227
110
|
expect(Buffer.from(cs.senderChain.senderRatchetKeyPrivate).equals(ephPriv)).toBe(true);
|
|
228
111
|
});
|
|
229
|
-
test('round-trip: bridge SET → upstream loadSession
|
|
112
|
+
test('round-trip: bridge SET → upstream loadSession → still open + same identity', async () => {
|
|
230
113
|
const { wrapped, keys } = await makeWrapped();
|
|
231
114
|
const remoteIdentity = Buffer.alloc(33, 0x11);
|
|
232
|
-
await wrapped.set('session',
|
|
233
|
-
const
|
|
234
|
-
expect(stored).toBeDefined();
|
|
235
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
-
const rec = SessionRecord.deserialize(stored);
|
|
237
|
-
expect(rec.haveOpenSession()).toBe(true);
|
|
115
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, buildBridgeSessionBytes({ remoteIdentity: new Uint8Array(remoteIdentity), remoteRegistrationId: 7 }));
|
|
116
|
+
const rec = SessionRecord.deserialize(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN]);
|
|
238
117
|
const open = rec.getOpenSession();
|
|
239
118
|
expect(open.registrationId).toBe(7);
|
|
240
119
|
expect(Buffer.from(open.indexInfo.remoteIdentityKey).equals(remoteIdentity)).toBe(true);
|
|
241
120
|
});
|
|
242
121
|
});
|
|
243
|
-
describe('wrap-legacy-store: session edge cases
|
|
244
|
-
test('lastRemoteEphemeralKey does NOT collide with senderRatchetKey when receiverChains
|
|
245
|
-
// Regression
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// to return the SENDER chain and `delete chainKey.key` corrupted
|
|
250
|
-
// the outbound encryption channel.
|
|
122
|
+
describe('wrap-legacy-store: session edge cases', () => {
|
|
123
|
+
test('lastRemoteEphemeralKey does NOT collide with senderRatchetKey when receiverChains empty', async () => {
|
|
124
|
+
// Regression: the converter previously used senderRatchetKey as a
|
|
125
|
+
// placeholder, which made upstream's `maybeStepRatchet` close the
|
|
126
|
+
// SENDER chain on the next peer ratchet (corrupting outbound
|
|
127
|
+
// encryption). Now uses aliceBaseKey (when bob) or empty buffer.
|
|
251
128
|
const { wrapped, keys } = await makeWrapped();
|
|
252
|
-
// Build a freshly-initiated alice-side session: sender chain
|
|
253
|
-
// populated, no receiverChain yet, pendingPreKey set (alice waits
|
|
254
|
-
// for bob's first reply).
|
|
255
129
|
const senderRatchetPub = fill(33, 2);
|
|
256
130
|
const aliceBaseKey = fill(33, 7);
|
|
257
131
|
const protoBytes = bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
@@ -274,25 +148,18 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
274
148
|
}),
|
|
275
149
|
previousSessions: []
|
|
276
150
|
})).finish();
|
|
277
|
-
await wrapped.set('session',
|
|
278
|
-
const stored = keys.raw['session']?.[
|
|
279
|
-
expect(stored).toBeDefined();
|
|
151
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, protoBytes);
|
|
152
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
280
153
|
const entry = Object.values(stored._sessions)[0];
|
|
281
|
-
// The last-remote-eph MUST NOT equal the sender's ratchet pubkey,
|
|
282
|
-
// otherwise upstream's maybeStepRatchet will close the SENDER chain
|
|
283
|
-
// when the peer ratchets next.
|
|
284
154
|
expect(entry.currentRatchet.lastRemoteEphemeralKey).not.toBe(Buffer.from(senderRatchetPub).toString('base64'));
|
|
285
155
|
});
|
|
286
|
-
test('baseKeyType
|
|
287
|
-
// Regression
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// look it up by baseKey.
|
|
294
|
-
const bobSideBytes = bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
295
|
-
currentSession: bridgeProto.SessionStructure.create({
|
|
156
|
+
test('baseKeyType: pendingPreKey present → OURS (alice), absent → THEIRS (bob)', async () => {
|
|
157
|
+
// Regression: previously set OURS whenever aliceBaseKey existed
|
|
158
|
+
// (always), making upstream `getSession(byBaseKey)` throw.
|
|
159
|
+
const baseKeyTypeFor = async (withPendingPreKey) => {
|
|
160
|
+
const { wrapped, keys } = await makeWrapped();
|
|
161
|
+
const aliceBaseKey = fill(33, 7);
|
|
162
|
+
const session = bridgeProto.SessionStructure.create({
|
|
296
163
|
sessionVersion: 3,
|
|
297
164
|
rootKey: fill(32, 1),
|
|
298
165
|
previousCounter: 0,
|
|
@@ -302,55 +169,28 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
302
169
|
chainKey: { index: 0, key: fill(32, 4) },
|
|
303
170
|
messageKeys: []
|
|
304
171
|
},
|
|
305
|
-
receiverChains:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
aliceBaseKey: fill(33, 7) // present but we're bob (no pendingPreKey)
|
|
315
|
-
}),
|
|
316
|
-
previousSessions: []
|
|
317
|
-
})).finish();
|
|
318
|
-
await wrapped.set('session', SAMPLE_BRIDGE_KEY_PN, bobSideBytes);
|
|
319
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_PN];
|
|
320
|
-
const bobEntry = Object.values(stored._sessions)[0];
|
|
321
|
-
expect(bobEntry.indexInfo.baseKeyType).toBe(2); // THEIRS
|
|
322
|
-
// Now alice-side: same session shape but with pendingPreKey → OURS.
|
|
323
|
-
const { wrapped: w2, keys: k2 } = await makeWrapped();
|
|
324
|
-
const aliceBaseKey = fill(33, 7);
|
|
325
|
-
const aliceBytes = bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
326
|
-
currentSession: bridgeProto.SessionStructure.create({
|
|
327
|
-
sessionVersion: 3,
|
|
328
|
-
rootKey: fill(32, 1),
|
|
329
|
-
previousCounter: 0,
|
|
330
|
-
senderChain: {
|
|
331
|
-
senderRatchetKey: fill(33, 2),
|
|
332
|
-
senderRatchetKeyPrivate: fill(32, 3),
|
|
333
|
-
chainKey: { index: 0, key: fill(32, 4) },
|
|
334
|
-
messageKeys: []
|
|
335
|
-
},
|
|
336
|
-
receiverChains: [],
|
|
172
|
+
receiverChains: withPendingPreKey
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
{
|
|
176
|
+
senderRatchetKey: fill(33, 5),
|
|
177
|
+
chainKey: { index: 1, key: fill(32, 6) },
|
|
178
|
+
messageKeys: []
|
|
179
|
+
}
|
|
180
|
+
],
|
|
337
181
|
remoteIdentityPublic: fill(33, 8),
|
|
338
182
|
remoteRegistrationId: 1,
|
|
339
183
|
aliceBaseKey,
|
|
340
|
-
pendingPreKey: { preKeyId: 1, signedPreKeyId: 2, baseKey: aliceBaseKey }
|
|
341
|
-
})
|
|
342
|
-
previousSessions: []
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expect(
|
|
184
|
+
...(withPendingPreKey ? { pendingPreKey: { preKeyId: 1, signedPreKeyId: 2, baseKey: aliceBaseKey } } : {})
|
|
185
|
+
});
|
|
186
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({ currentSession: session, previousSessions: [] })).finish());
|
|
187
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN];
|
|
188
|
+
return Object.values(stored._sessions)[0].indexInfo.baseKeyType;
|
|
189
|
+
};
|
|
190
|
+
expect(await baseKeyTypeFor(true)).toBe(1); // OURS
|
|
191
|
+
expect(await baseKeyTypeFor(false)).toBe(2); // THEIRS
|
|
348
192
|
});
|
|
349
|
-
test('multi-session: currentSession + previousSessions[] all
|
|
350
|
-
// Cover the path where Rust archives sessions into previous_sessions
|
|
351
|
-
// (e.g. after identity change). Upstream should see one OPEN entry
|
|
352
|
-
// (closed=-1) plus one or more closed entries that share the same
|
|
353
|
-
// peer key.
|
|
193
|
+
test('multi-session: currentSession + previousSessions[] all land in upstream `_sessions`', async () => {
|
|
354
194
|
const { wrapped, keys } = await makeWrapped();
|
|
355
195
|
const mkSession = (rootSeed, baseKeySeed) => bridgeProto.SessionStructure.create({
|
|
356
196
|
sessionVersion: 3,
|
|
@@ -373,38 +213,28 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
373
213
|
remoteRegistrationId: 1,
|
|
374
214
|
aliceBaseKey: fill(33, baseKeySeed)
|
|
375
215
|
});
|
|
376
|
-
|
|
216
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
377
217
|
currentSession: mkSession(1, 100),
|
|
378
218
|
previousSessions: [mkSession(2, 200), mkSession(3, 250)]
|
|
379
|
-
})).finish();
|
|
380
|
-
|
|
381
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_LID];
|
|
219
|
+
})).finish());
|
|
220
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
382
221
|
expect(stored.version).toBe('v1');
|
|
383
|
-
// 1 current + 2 previous, distinct baseKeys → 3 entries.
|
|
384
222
|
expect(Object.keys(stored._sessions).length).toBe(3);
|
|
385
|
-
// Exactly one OPEN session (closed=-1).
|
|
386
223
|
const open = Object.values(stored._sessions).filter(e => e.indexInfo.closed === -1);
|
|
387
224
|
expect(open.length).toBe(1);
|
|
388
225
|
expect(open[0].indexInfo.baseKey).toBe(Buffer.from(fill(33, 100)).toString('base64'));
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// (matching Rust's archive ordering: front = newest).
|
|
226
|
+
// Front of Rust's previous_sessions = newest archived → must get
|
|
227
|
+
// the LARGER `closed` timestamp so upstream's removeOldSessions
|
|
228
|
+
// evicts the oldest first.
|
|
393
229
|
const closedEntries = Object.values(stored._sessions).filter(e => e.indexInfo.closed > 0);
|
|
394
|
-
expect(closedEntries.length).toBe(2);
|
|
395
230
|
const newer = closedEntries.find(e => e.indexInfo.baseKey === Buffer.from(fill(33, 200)).toString('base64'));
|
|
396
231
|
const older = closedEntries.find(e => e.indexInfo.baseKey === Buffer.from(fill(33, 250)).toString('base64'));
|
|
397
|
-
expect(newer).toBeDefined();
|
|
398
|
-
expect(older).toBeDefined();
|
|
399
232
|
expect(newer.indexInfo.closed).toBeGreaterThan(older.indexInfo.closed);
|
|
400
233
|
});
|
|
401
|
-
test('pendingPreKey
|
|
402
|
-
// Bridge proto: pendingPreKey { preKeyId, signedPreKeyId, baseKey }
|
|
403
|
-
// Upstream JS: session.pendingPreKey { preKeyId, signedKeyId, baseKey }
|
|
404
|
-
// (yes, the field name differs — `signedPreKeyId` vs `signedKeyId`)
|
|
234
|
+
test('pendingPreKey round-trips with field rename `signedPreKeyId↔signedKeyId`', async () => {
|
|
405
235
|
const { wrapped, keys } = await makeWrapped();
|
|
406
236
|
const aliceBaseKey = fill(33, 7);
|
|
407
|
-
|
|
237
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
408
238
|
currentSession: bridgeProto.SessionStructure.create({
|
|
409
239
|
sessionVersion: 3,
|
|
410
240
|
rootKey: fill(32, 1),
|
|
@@ -422,46 +252,36 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
422
252
|
pendingPreKey: { preKeyId: 314, signedPreKeyId: 271, baseKey: aliceBaseKey }
|
|
423
253
|
}),
|
|
424
254
|
previousSessions: []
|
|
425
|
-
})).finish();
|
|
426
|
-
|
|
427
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_LID];
|
|
255
|
+
})).finish());
|
|
256
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
428
257
|
const entry = Object.values(stored._sessions)[0];
|
|
429
|
-
expect(entry.pendingPreKey).toBeDefined();
|
|
430
258
|
expect(entry.pendingPreKey.preKeyId).toBe(314);
|
|
431
|
-
//
|
|
432
|
-
expect(entry.pendingPreKey.signedKeyId).toBe(271);
|
|
259
|
+
expect(entry.pendingPreKey.signedKeyId).toBe(271); // rename: signedPreKeyId → signedKeyId
|
|
433
260
|
expect(entry.pendingPreKey.baseKey).toBe(Buffer.from(aliceBaseKey).toString('base64'));
|
|
434
|
-
|
|
435
|
-
// should round-trip with the original field names.
|
|
436
|
-
const protoOut = (await wrapped.get('session', SAMPLE_BRIDGE_KEY_LID));
|
|
261
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
437
262
|
const decoded = bridgeProto.RecordStructure.decode(protoOut);
|
|
438
263
|
expect(decoded.currentSession?.pendingPreKey?.preKeyId).toBe(314);
|
|
439
264
|
expect(decoded.currentSession?.pendingPreKey?.signedPreKeyId).toBe(271);
|
|
440
265
|
});
|
|
441
|
-
test('aliceBaseKey
|
|
442
|
-
// Regression
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
// `find_matching_previous_session_index` lookups on subsequent
|
|
446
|
-
// PreKeySignal arrivals.
|
|
266
|
+
test('aliceBaseKey preserved on JS→bridge regardless of baseKeyType (bob-side restoration)', async () => {
|
|
267
|
+
// Regression: previously dropped aliceBaseKey when baseKeyType=THEIRS,
|
|
268
|
+
// breaking Rust's `find_matching_previous_session_index` lookup on
|
|
269
|
+
// subsequent PreKeySignal arrivals.
|
|
447
270
|
const { wrapped, keys } = await makeWrapped();
|
|
448
271
|
const baseKey = Buffer.alloc(33, 0xa0);
|
|
449
|
-
const remoteIdent = Buffer.alloc(33, 0xc0);
|
|
450
|
-
const ephPub = Buffer.alloc(33, 0xd0);
|
|
451
|
-
const ephPriv = Buffer.alloc(32, 0xe0);
|
|
452
|
-
const lastRemoteEph = Buffer.alloc(33, 0xf0);
|
|
453
|
-
const rootKey = Buffer.alloc(32, 0xb0);
|
|
454
|
-
// Bob-side stored object (baseKeyType=THEIRS, no pendingPreKey).
|
|
455
272
|
keys.raw['session'] = {
|
|
456
|
-
[
|
|
273
|
+
[UPSTREAM_SESSION_KEY_LID]: {
|
|
457
274
|
_sessions: {
|
|
458
275
|
[baseKey.toString('base64')]: {
|
|
459
276
|
registrationId: 4242,
|
|
460
277
|
currentRatchet: {
|
|
461
|
-
ephemeralKeyPair: {
|
|
462
|
-
|
|
278
|
+
ephemeralKeyPair: {
|
|
279
|
+
pubKey: Buffer.alloc(33, 0xd0).toString('base64'),
|
|
280
|
+
privKey: Buffer.alloc(32, 0xe0).toString('base64')
|
|
281
|
+
},
|
|
282
|
+
lastRemoteEphemeralKey: Buffer.alloc(33, 0xf0).toString('base64'),
|
|
463
283
|
previousCounter: 0,
|
|
464
|
-
rootKey:
|
|
284
|
+
rootKey: Buffer.alloc(32, 0xb0).toString('base64')
|
|
465
285
|
},
|
|
466
286
|
indexInfo: {
|
|
467
287
|
baseKey: baseKey.toString('base64'),
|
|
@@ -469,7 +289,7 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
469
289
|
closed: -1,
|
|
470
290
|
used: Date.now(),
|
|
471
291
|
created: Date.now(),
|
|
472
|
-
remoteIdentityKey:
|
|
292
|
+
remoteIdentityKey: Buffer.alloc(33, 0xc0).toString('base64')
|
|
473
293
|
},
|
|
474
294
|
_chains: {}
|
|
475
295
|
}
|
|
@@ -477,11 +297,8 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
477
297
|
version: 'v1'
|
|
478
298
|
}
|
|
479
299
|
};
|
|
480
|
-
const protoOut = (await wrapped.get('session',
|
|
300
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
481
301
|
const decoded = bridgeProto.RecordStructure.decode(protoOut);
|
|
482
|
-
// aliceBaseKey MUST be set even when we're bob (baseKeyType=THEIRS),
|
|
483
|
-
// otherwise Rust's session-matching path breaks.
|
|
484
|
-
expect(decoded.currentSession?.aliceBaseKey).toBeDefined();
|
|
485
302
|
expect(Buffer.from(decoded.currentSession.aliceBaseKey).equals(baseKey)).toBe(true);
|
|
486
303
|
});
|
|
487
304
|
});
|