@oxidezap/baileyrs 0.0.8 → 0.0.9
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/Utils/__tests__/_legacy-store-fixtures.d.ts +65 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts.map +1 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.js +107 -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 +298 -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 +48 -175
- 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 +118 -297
- 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 +128 -273
- 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,50 @@
|
|
|
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
15
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
36
|
-
// @ts-expect-error --
|
|
16
|
+
// @ts-expect-error -- libsignal lacks .d.ts
|
|
37
17
|
import SessionRecord from 'libsignal/src/session_record.js';
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge proto-types ambient
|
|
39
18
|
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
19
|
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 () => {
|
|
20
|
+
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";
|
|
21
|
+
describe('wrap-legacy-store: session key-name translation', () => {
|
|
22
|
+
test('LID → upstream `_1.deviceId`', async () => {
|
|
121
23
|
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();
|
|
24
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, buildBridgeSessionBytes());
|
|
25
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID]).toBeDefined();
|
|
26
|
+
expect(keys.raw['session']?.[BRIDGE_SESSION_KEY_LID]).toBeUndefined();
|
|
127
27
|
});
|
|
128
|
-
test('
|
|
28
|
+
test('PN no-device → bare `{user}.{signalDev}`', async () => {
|
|
129
29
|
const { wrapped, keys } = await makeWrapped();
|
|
130
|
-
await wrapped.set('session',
|
|
131
|
-
expect(keys.raw['session']?.[
|
|
30
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, buildBridgeSessionBytes());
|
|
31
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN]).toBeDefined();
|
|
132
32
|
});
|
|
133
|
-
test('
|
|
33
|
+
test('PN with JID device → device suffix preserved', async () => {
|
|
134
34
|
const { wrapped, keys } = await makeWrapped();
|
|
135
|
-
await wrapped.set('session',
|
|
136
|
-
expect(keys.raw['session']?.[
|
|
35
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN_DEV, buildBridgeSessionBytes());
|
|
36
|
+
expect(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN_DEV]).toBeDefined();
|
|
137
37
|
});
|
|
138
38
|
});
|
|
139
|
-
describe('wrap-legacy-store: session value-byte translation
|
|
140
|
-
test('bridge
|
|
39
|
+
describe('wrap-legacy-store: session value-byte translation', () => {
|
|
40
|
+
test('bridge proto bytes load via upstream `SessionRecord.deserialize` as OPEN', async () => {
|
|
141
41
|
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',
|
|
42
|
+
const aliceBaseKey = Buffer.from(fill(33, 100));
|
|
43
|
+
const rootKey = Buffer.from(fill(32, 11));
|
|
44
|
+
const remoteIdentity = Buffer.from(fill(33, 17));
|
|
45
|
+
const senderRatchetPub = Buffer.from(fill(33, 23));
|
|
46
|
+
const senderRatchetPriv = Buffer.from(fill(32, 29));
|
|
47
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, buildBridgeSessionBytes({
|
|
148
48
|
rootKey: new Uint8Array(rootKey),
|
|
149
49
|
aliceBaseKey: new Uint8Array(aliceBaseKey),
|
|
150
50
|
remoteIdentity: new Uint8Array(remoteIdentity),
|
|
@@ -153,15 +53,11 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
153
53
|
remoteRegistrationId: 99,
|
|
154
54
|
previousCounter: 4
|
|
155
55
|
}));
|
|
156
|
-
const stored = keys.raw['session']?.[
|
|
157
|
-
|
|
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
|
|
56
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
58
|
const rec = SessionRecord.deserialize(stored);
|
|
162
59
|
expect(rec.haveOpenSession()).toBe(true);
|
|
163
60
|
const open = rec.getOpenSession();
|
|
164
|
-
expect(open).toBeDefined();
|
|
165
61
|
expect(open.registrationId).toBe(99);
|
|
166
62
|
expect(open.currentRatchet.previousCounter).toBe(4);
|
|
167
63
|
expect(Buffer.from(open.currentRatchet.rootKey).equals(rootKey)).toBe(true);
|
|
@@ -169,54 +65,44 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
169
65
|
expect(Buffer.from(open.currentRatchet.ephemeralKeyPair.privKey).equals(senderRatchetPriv)).toBe(true);
|
|
170
66
|
expect(Buffer.from(open.indexInfo.baseKey).equals(aliceBaseKey)).toBe(true);
|
|
171
67
|
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
68
|
expect(open.indexInfo.closed).toBe(-1);
|
|
175
|
-
// We expect at least one chain populated (sender + receiver).
|
|
176
69
|
expect(Object.keys(open._chains).length).toBeGreaterThanOrEqual(1);
|
|
177
70
|
});
|
|
178
|
-
test('upstream
|
|
71
|
+
test('upstream JS object → bridge GET produces valid proto with same fields', async () => {
|
|
179
72
|
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
73
|
const baseKey = Buffer.alloc(33, 0xa0);
|
|
184
74
|
const rootKey = Buffer.alloc(32, 0xb0);
|
|
185
75
|
const remoteIdent = Buffer.alloc(33, 0xc0);
|
|
186
76
|
const ephPub = Buffer.alloc(33, 0xd0);
|
|
187
77
|
const ephPriv = Buffer.alloc(32, 0xe0);
|
|
188
78
|
const lastRemoteEph = Buffer.alloc(33, 0xf0);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
pubKey: ephPub.toString('base64'),
|
|
196
|
-
|
|
79
|
+
keys.raw['session'] = {
|
|
80
|
+
[UPSTREAM_SESSION_KEY_LID]: {
|
|
81
|
+
_sessions: {
|
|
82
|
+
[baseKey.toString('base64')]: {
|
|
83
|
+
registrationId: 4242,
|
|
84
|
+
currentRatchet: {
|
|
85
|
+
ephemeralKeyPair: { pubKey: ephPub.toString('base64'), privKey: ephPriv.toString('base64') },
|
|
86
|
+
lastRemoteEphemeralKey: lastRemoteEph.toString('base64'),
|
|
87
|
+
previousCounter: 7,
|
|
88
|
+
rootKey: rootKey.toString('base64')
|
|
197
89
|
},
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
version: 'v1'
|
|
90
|
+
indexInfo: {
|
|
91
|
+
baseKey: baseKey.toString('base64'),
|
|
92
|
+
baseKeyType: 1,
|
|
93
|
+
closed: -1,
|
|
94
|
+
used: Date.now(),
|
|
95
|
+
created: Date.now(),
|
|
96
|
+
remoteIdentityKey: remoteIdent.toString('base64')
|
|
97
|
+
},
|
|
98
|
+
_chains: {}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
version: 'v1'
|
|
102
|
+
}
|
|
214
103
|
};
|
|
215
|
-
|
|
216
|
-
const protoOut = (await wrapped.get('session', SAMPLE_BRIDGE_KEY_LID));
|
|
217
|
-
expect(protoOut).not.toBe(null);
|
|
104
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
218
105
|
const decoded = bridgeProto.RecordStructure.decode(protoOut);
|
|
219
|
-
expect(decoded.currentSession).toBeDefined();
|
|
220
106
|
const cs = decoded.currentSession;
|
|
221
107
|
expect(cs.remoteRegistrationId).toBe(4242);
|
|
222
108
|
expect(cs.previousCounter).toBe(7);
|
|
@@ -226,32 +112,24 @@ describe('wrap-legacy-store: session value-byte translation (cross-impl)', () =>
|
|
|
226
112
|
expect(Buffer.from(cs.senderChain.senderRatchetKey).equals(ephPub)).toBe(true);
|
|
227
113
|
expect(Buffer.from(cs.senderChain.senderRatchetKeyPrivate).equals(ephPriv)).toBe(true);
|
|
228
114
|
});
|
|
229
|
-
test('round-trip: bridge SET → upstream loadSession
|
|
115
|
+
test('round-trip: bridge SET → upstream loadSession → still open + same identity', async () => {
|
|
230
116
|
const { wrapped, keys } = await makeWrapped();
|
|
231
117
|
const remoteIdentity = Buffer.alloc(33, 0x11);
|
|
232
|
-
await wrapped.set('session',
|
|
233
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_PN];
|
|
234
|
-
expect(stored).toBeDefined();
|
|
118
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, buildBridgeSessionBytes({ remoteIdentity: new Uint8Array(remoteIdentity), remoteRegistrationId: 7 }));
|
|
235
119
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
|
-
const rec = SessionRecord.deserialize(
|
|
237
|
-
expect(rec.haveOpenSession()).toBe(true);
|
|
120
|
+
const rec = SessionRecord.deserialize(keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN]);
|
|
238
121
|
const open = rec.getOpenSession();
|
|
239
122
|
expect(open.registrationId).toBe(7);
|
|
240
123
|
expect(Buffer.from(open.indexInfo.remoteIdentityKey).equals(remoteIdentity)).toBe(true);
|
|
241
124
|
});
|
|
242
125
|
});
|
|
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.
|
|
126
|
+
describe('wrap-legacy-store: session edge cases', () => {
|
|
127
|
+
test('lastRemoteEphemeralKey does NOT collide with senderRatchetKey when receiverChains empty', async () => {
|
|
128
|
+
// Regression: the converter previously used senderRatchetKey as a
|
|
129
|
+
// placeholder, which made upstream's `maybeStepRatchet` close the
|
|
130
|
+
// SENDER chain on the next peer ratchet (corrupting outbound
|
|
131
|
+
// encryption). Now uses aliceBaseKey (when bob) or empty buffer.
|
|
251
132
|
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
133
|
const senderRatchetPub = fill(33, 2);
|
|
256
134
|
const aliceBaseKey = fill(33, 7);
|
|
257
135
|
const protoBytes = bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
@@ -274,56 +152,18 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
274
152
|
}),
|
|
275
153
|
previousSessions: []
|
|
276
154
|
})).finish();
|
|
277
|
-
await wrapped.set('session',
|
|
278
|
-
const stored = keys.raw['session']?.[
|
|
279
|
-
expect(stored).toBeDefined();
|
|
155
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, protoBytes);
|
|
156
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
280
157
|
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
158
|
expect(entry.currentRatchet.lastRemoteEphemeralKey).not.toBe(Buffer.from(senderRatchetPub).toString('base64'));
|
|
285
159
|
});
|
|
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({
|
|
296
|
-
sessionVersion: 3,
|
|
297
|
-
rootKey: fill(32, 1),
|
|
298
|
-
previousCounter: 0,
|
|
299
|
-
senderChain: {
|
|
300
|
-
senderRatchetKey: fill(33, 2),
|
|
301
|
-
senderRatchetKeyPrivate: fill(32, 3),
|
|
302
|
-
chainKey: { index: 0, key: fill(32, 4) },
|
|
303
|
-
messageKeys: []
|
|
304
|
-
},
|
|
305
|
-
receiverChains: [
|
|
306
|
-
{
|
|
307
|
-
senderRatchetKey: fill(33, 5),
|
|
308
|
-
chainKey: { index: 1, key: fill(32, 6) },
|
|
309
|
-
messageKeys: []
|
|
310
|
-
}
|
|
311
|
-
],
|
|
312
|
-
remoteIdentityPublic: fill(33, 8),
|
|
313
|
-
remoteRegistrationId: 1,
|
|
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({
|
|
160
|
+
test('baseKeyType: pendingPreKey present → OURS (alice), absent → THEIRS (bob)', async () => {
|
|
161
|
+
// Regression: previously set OURS whenever aliceBaseKey existed
|
|
162
|
+
// (always), making upstream `getSession(byBaseKey)` throw.
|
|
163
|
+
const baseKeyTypeFor = async (withPendingPreKey) => {
|
|
164
|
+
const { wrapped, keys } = await makeWrapped();
|
|
165
|
+
const aliceBaseKey = fill(33, 7);
|
|
166
|
+
const session = bridgeProto.SessionStructure.create({
|
|
327
167
|
sessionVersion: 3,
|
|
328
168
|
rootKey: fill(32, 1),
|
|
329
169
|
previousCounter: 0,
|
|
@@ -333,24 +173,28 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
333
173
|
chainKey: { index: 0, key: fill(32, 4) },
|
|
334
174
|
messageKeys: []
|
|
335
175
|
},
|
|
336
|
-
receiverChains:
|
|
176
|
+
receiverChains: withPendingPreKey
|
|
177
|
+
? []
|
|
178
|
+
: [
|
|
179
|
+
{
|
|
180
|
+
senderRatchetKey: fill(33, 5),
|
|
181
|
+
chainKey: { index: 1, key: fill(32, 6) },
|
|
182
|
+
messageKeys: []
|
|
183
|
+
}
|
|
184
|
+
],
|
|
337
185
|
remoteIdentityPublic: fill(33, 8),
|
|
338
186
|
remoteRegistrationId: 1,
|
|
339
187
|
aliceBaseKey,
|
|
340
|
-
pendingPreKey: { preKeyId: 1, signedPreKeyId: 2, baseKey: aliceBaseKey }
|
|
341
|
-
})
|
|
342
|
-
previousSessions: []
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expect(
|
|
188
|
+
...(withPendingPreKey ? { pendingPreKey: { preKeyId: 1, signedPreKeyId: 2, baseKey: aliceBaseKey } } : {})
|
|
189
|
+
});
|
|
190
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_PN, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({ currentSession: session, previousSessions: [] })).finish());
|
|
191
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_PN];
|
|
192
|
+
return Object.values(stored._sessions)[0].indexInfo.baseKeyType;
|
|
193
|
+
};
|
|
194
|
+
expect(await baseKeyTypeFor(true)).toBe(1); // OURS
|
|
195
|
+
expect(await baseKeyTypeFor(false)).toBe(2); // THEIRS
|
|
348
196
|
});
|
|
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.
|
|
197
|
+
test('multi-session: currentSession + previousSessions[] all land in upstream `_sessions`', async () => {
|
|
354
198
|
const { wrapped, keys } = await makeWrapped();
|
|
355
199
|
const mkSession = (rootSeed, baseKeySeed) => bridgeProto.SessionStructure.create({
|
|
356
200
|
sessionVersion: 3,
|
|
@@ -373,38 +217,28 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
373
217
|
remoteRegistrationId: 1,
|
|
374
218
|
aliceBaseKey: fill(33, baseKeySeed)
|
|
375
219
|
});
|
|
376
|
-
|
|
220
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
377
221
|
currentSession: mkSession(1, 100),
|
|
378
222
|
previousSessions: [mkSession(2, 200), mkSession(3, 250)]
|
|
379
|
-
})).finish();
|
|
380
|
-
|
|
381
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_LID];
|
|
223
|
+
})).finish());
|
|
224
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
382
225
|
expect(stored.version).toBe('v1');
|
|
383
|
-
// 1 current + 2 previous, distinct baseKeys → 3 entries.
|
|
384
226
|
expect(Object.keys(stored._sessions).length).toBe(3);
|
|
385
|
-
// Exactly one OPEN session (closed=-1).
|
|
386
227
|
const open = Object.values(stored._sessions).filter(e => e.indexInfo.closed === -1);
|
|
387
228
|
expect(open.length).toBe(1);
|
|
388
229
|
expect(open[0].indexInfo.baseKey).toBe(Buffer.from(fill(33, 100)).toString('base64'));
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// (matching Rust's archive ordering: front = newest).
|
|
230
|
+
// Front of Rust's previous_sessions = newest archived → must get
|
|
231
|
+
// the LARGER `closed` timestamp so upstream's removeOldSessions
|
|
232
|
+
// evicts the oldest first.
|
|
393
233
|
const closedEntries = Object.values(stored._sessions).filter(e => e.indexInfo.closed > 0);
|
|
394
|
-
expect(closedEntries.length).toBe(2);
|
|
395
234
|
const newer = closedEntries.find(e => e.indexInfo.baseKey === Buffer.from(fill(33, 200)).toString('base64'));
|
|
396
235
|
const older = closedEntries.find(e => e.indexInfo.baseKey === Buffer.from(fill(33, 250)).toString('base64'));
|
|
397
|
-
expect(newer).toBeDefined();
|
|
398
|
-
expect(older).toBeDefined();
|
|
399
236
|
expect(newer.indexInfo.closed).toBeGreaterThan(older.indexInfo.closed);
|
|
400
237
|
});
|
|
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`)
|
|
238
|
+
test('pendingPreKey round-trips with field rename `signedPreKeyId↔signedKeyId`', async () => {
|
|
405
239
|
const { wrapped, keys } = await makeWrapped();
|
|
406
240
|
const aliceBaseKey = fill(33, 7);
|
|
407
|
-
|
|
241
|
+
await wrapped.set('session', BRIDGE_SESSION_KEY_LID, bridgeProto.RecordStructure.encode(bridgeProto.RecordStructure.create({
|
|
408
242
|
currentSession: bridgeProto.SessionStructure.create({
|
|
409
243
|
sessionVersion: 3,
|
|
410
244
|
rootKey: fill(32, 1),
|
|
@@ -422,46 +256,36 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
422
256
|
pendingPreKey: { preKeyId: 314, signedPreKeyId: 271, baseKey: aliceBaseKey }
|
|
423
257
|
}),
|
|
424
258
|
previousSessions: []
|
|
425
|
-
})).finish();
|
|
426
|
-
|
|
427
|
-
const stored = keys.raw['session']?.[EXPECTED_UPSTREAM_KEY_LID];
|
|
259
|
+
})).finish());
|
|
260
|
+
const stored = keys.raw['session']?.[UPSTREAM_SESSION_KEY_LID];
|
|
428
261
|
const entry = Object.values(stored._sessions)[0];
|
|
429
|
-
expect(entry.pendingPreKey).toBeDefined();
|
|
430
262
|
expect(entry.pendingPreKey.preKeyId).toBe(314);
|
|
431
|
-
//
|
|
432
|
-
expect(entry.pendingPreKey.signedKeyId).toBe(271);
|
|
263
|
+
expect(entry.pendingPreKey.signedKeyId).toBe(271); // rename: signedPreKeyId → signedKeyId
|
|
433
264
|
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));
|
|
265
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
437
266
|
const decoded = bridgeProto.RecordStructure.decode(protoOut);
|
|
438
267
|
expect(decoded.currentSession?.pendingPreKey?.preKeyId).toBe(314);
|
|
439
268
|
expect(decoded.currentSession?.pendingPreKey?.signedPreKeyId).toBe(271);
|
|
440
269
|
});
|
|
441
|
-
test('aliceBaseKey
|
|
442
|
-
// Regression
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
// `find_matching_previous_session_index` lookups on subsequent
|
|
446
|
-
// PreKeySignal arrivals.
|
|
270
|
+
test('aliceBaseKey preserved on JS→bridge regardless of baseKeyType (bob-side restoration)', async () => {
|
|
271
|
+
// Regression: previously dropped aliceBaseKey when baseKeyType=THEIRS,
|
|
272
|
+
// breaking Rust's `find_matching_previous_session_index` lookup on
|
|
273
|
+
// subsequent PreKeySignal arrivals.
|
|
447
274
|
const { wrapped, keys } = await makeWrapped();
|
|
448
275
|
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
276
|
keys.raw['session'] = {
|
|
456
|
-
[
|
|
277
|
+
[UPSTREAM_SESSION_KEY_LID]: {
|
|
457
278
|
_sessions: {
|
|
458
279
|
[baseKey.toString('base64')]: {
|
|
459
280
|
registrationId: 4242,
|
|
460
281
|
currentRatchet: {
|
|
461
|
-
ephemeralKeyPair: {
|
|
462
|
-
|
|
282
|
+
ephemeralKeyPair: {
|
|
283
|
+
pubKey: Buffer.alloc(33, 0xd0).toString('base64'),
|
|
284
|
+
privKey: Buffer.alloc(32, 0xe0).toString('base64')
|
|
285
|
+
},
|
|
286
|
+
lastRemoteEphemeralKey: Buffer.alloc(33, 0xf0).toString('base64'),
|
|
463
287
|
previousCounter: 0,
|
|
464
|
-
rootKey:
|
|
288
|
+
rootKey: Buffer.alloc(32, 0xb0).toString('base64')
|
|
465
289
|
},
|
|
466
290
|
indexInfo: {
|
|
467
291
|
baseKey: baseKey.toString('base64'),
|
|
@@ -469,7 +293,7 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
469
293
|
closed: -1,
|
|
470
294
|
used: Date.now(),
|
|
471
295
|
created: Date.now(),
|
|
472
|
-
remoteIdentityKey:
|
|
296
|
+
remoteIdentityKey: Buffer.alloc(33, 0xc0).toString('base64')
|
|
473
297
|
},
|
|
474
298
|
_chains: {}
|
|
475
299
|
}
|
|
@@ -477,11 +301,8 @@ describe('wrap-legacy-store: session edge cases (multi-session, pendingPreKey, l
|
|
|
477
301
|
version: 'v1'
|
|
478
302
|
}
|
|
479
303
|
};
|
|
480
|
-
const protoOut = (await wrapped.get('session',
|
|
304
|
+
const protoOut = (await wrapped.get('session', BRIDGE_SESSION_KEY_LID));
|
|
481
305
|
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
306
|
expect(Buffer.from(decoded.currentSession.aliceBaseKey).equals(baseKey)).toBe(true);
|
|
486
307
|
});
|
|
487
308
|
});
|