@peers-app/peers-sdk 0.25.14 → 0.25.16
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/dist/context/user-context-identity.test.d.ts +1 -0
- package/dist/context/user-context-identity.test.js +220 -0
- package/dist/context/user-context.d.ts +5 -1
- package/dist/context/user-context.js +63 -8
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/system/identity-contract.d.ts +156 -0
- package/dist/contracts/system/identity-contract.js +172 -0
- package/dist/contracts/system/index.d.ts +1 -1
- package/dist/data/assistants.d.ts +4 -4
- package/dist/data/devices.d.ts +70 -4
- package/dist/data/devices.js +3 -0
- package/dist/data/group-members.d.ts +2 -2
- package/dist/data/groups.d.ts +2 -2
- package/dist/data/invites.d.ts +93 -25
- package/dist/data/persistent-vars.d.ts +8 -0
- package/dist/data/persistent-vars.js +16 -0
- package/dist/data/tool-access-approvals.d.ts +2 -2
- package/dist/data/user-permissions.d.ts +10 -2
- package/dist/data/user-permissions.js +35 -4
- package/dist/data/user-permissions.test.js +26 -0
- package/dist/data/users.d.ts +48 -4
- package/dist/data/users.js +61 -5
- package/dist/data/workflows.d.ts +1 -1
- package/dist/device/connection.d.ts +7 -1
- package/dist/device/connection.js +38 -6
- package/dist/device/connection.test.js +33 -4
- package/dist/device/device.d.ts +2 -2
- package/dist/device/device.js +4 -2
- package/dist/device/get-trust-level-fn.d.ts +1 -0
- package/dist/device/get-trust-level-fn.js +28 -4
- package/dist/device/get-trust-level-fn.test.js +95 -0
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/host-credentials.d.ts +78 -0
- package/dist/host-credentials.js +128 -0
- package/dist/host-credentials.test.d.ts +1 -0
- package/dist/host-credentials.test.js +88 -0
- package/dist/identity/account-email.d.ts +59 -0
- package/dist/identity/account-email.js +87 -0
- package/dist/identity/account-email.test.d.ts +1 -0
- package/dist/identity/account-email.test.js +59 -0
- package/dist/identity/anchor-document.d.ts +205 -0
- package/dist/identity/anchor-document.js +119 -0
- package/dist/identity/anchor-document.test.d.ts +1 -0
- package/dist/identity/anchor-document.test.js +16 -0
- package/dist/identity/anchor-resolution.d.ts +61 -0
- package/dist/identity/anchor-resolution.js +117 -0
- package/dist/identity/anchor-resolution.test.d.ts +1 -0
- package/dist/identity/anchor-resolution.test.js +130 -0
- package/dist/identity/key-change.d.ts +142 -0
- package/dist/identity/key-change.js +472 -0
- package/dist/identity/key-change.test.d.ts +1 -0
- package/dist/identity/key-change.test.js +811 -0
- package/dist/identity/key-contest.d.ts +91 -0
- package/dist/identity/key-contest.js +55 -0
- package/dist/identity/key-resolutions.d.ts +55 -0
- package/dist/identity/key-resolutions.js +42 -0
- package/dist/identity/passkey.d.ts +22 -0
- package/dist/identity/passkey.js +33 -0
- package/dist/identity/user-row-change.d.ts +45 -0
- package/dist/identity/user-row-change.js +81 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/invites/invite-messages.d.ts +512 -116
- package/dist/invites/invite-token.d.ts +114 -24
- package/dist/invites/invite-token.js +6 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +20 -0
- package/dist/system-ids.d.ts +16 -0
- package/dist/system-ids.js +16 -0
- package/dist/types/workflow.d.ts +2 -2
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +43 -0
- package/dist/utils.test.js +23 -0
- package/package.json +1 -1
package/dist/data/workflows.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import type { DataContext } from "../context/data-context";
|
|
|
2
2
|
export declare function Workflows(dataContext?: DataContext): import("..").Table<{
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
|
-
createdAt: Date;
|
|
6
5
|
updatedAt: Date;
|
|
6
|
+
createdAt: Date;
|
|
7
7
|
workflowId: string;
|
|
8
8
|
defaultAssistantId: string;
|
|
9
9
|
instructions: {
|
|
@@ -71,9 +71,9 @@ export declare class Connection {
|
|
|
71
71
|
/** Copy of the authenticated remote device identity (empty before the handshake). */
|
|
72
72
|
get remoteDeviceInfo(): {
|
|
73
73
|
publicKey: string;
|
|
74
|
-
publicBoxKey: string;
|
|
75
74
|
userId: string;
|
|
76
75
|
deviceId: string;
|
|
76
|
+
publicBoxKey: string;
|
|
77
77
|
userName?: string | undefined;
|
|
78
78
|
deviceName?: string | undefined;
|
|
79
79
|
};
|
|
@@ -140,6 +140,12 @@ export declare class Connection {
|
|
|
140
140
|
* frame cipher is armed in both directions before this resolves.
|
|
141
141
|
*/
|
|
142
142
|
doHandshake(remoteAddress: string): Promise<IDeviceHandshake>;
|
|
143
|
+
/**
|
|
144
|
+
* Resets this side and asks the remote to do the same. The remote call is a courtesy: a
|
|
145
|
+
* peer that is not a server has no `reset` handler, and a peer we just refused may already
|
|
146
|
+
* be gone, so its failure is logged and never allowed to escape as an unhandled rejection.
|
|
147
|
+
*/
|
|
148
|
+
private resetRemote;
|
|
143
149
|
private closeLocal;
|
|
144
150
|
/** Round-trip time in milliseconds. @throws if the connection is not verified */
|
|
145
151
|
ping(): Promise<number>;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Connection = void 0;
|
|
4
4
|
exports.normalizeAddress = normalizeAddress;
|
|
5
|
+
const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
5
6
|
const data_1 = require("../data");
|
|
7
|
+
const key_change_1 = require("../identity/key-change");
|
|
6
8
|
const keys_1 = require("../keys");
|
|
7
9
|
const peer_device_1 = require("../types/peer-device");
|
|
8
10
|
const utils_1 = require("../utils");
|
|
@@ -370,7 +372,7 @@ class Connection {
|
|
|
370
372
|
ephemeralSecretKey = ephemeral.secretKey;
|
|
371
373
|
frameCrypto = { version: 1, ephemeralBoxKey: (0, keys_1.encodeBase64)(ephemeral.publicKey) };
|
|
372
374
|
}
|
|
373
|
-
const handshake = this.localDevice.getHandshake(this.connectionId, serverAddress, frameCrypto);
|
|
375
|
+
const handshake = this.localDevice.getHandshake(this.connectionId, serverAddress, frameCrypto, (0, key_change_1.peekLocalKeyContest)());
|
|
374
376
|
if (ephemeralSecretKey) {
|
|
375
377
|
this.pendingClientHandshake = {
|
|
376
378
|
ephemeralSecretKey,
|
|
@@ -418,7 +420,8 @@ class Connection {
|
|
|
418
420
|
});
|
|
419
421
|
frameKeys = (0, frame_cipher_1.frameKeysForRole)(derived, "server");
|
|
420
422
|
}
|
|
421
|
-
const handshakeResponse = this.localDevice.handshakeResponse(signedHandshake, this.connectionId, thisServerAddress, frameCrypto);
|
|
423
|
+
const handshakeResponse = this.localDevice.handshakeResponse(signedHandshake, this.connectionId, thisServerAddress, frameCrypto, (0, key_change_1.peekLocalKeyContest)());
|
|
424
|
+
await noteHandshakeContest(_handshake.keyContest);
|
|
422
425
|
this._remoteDeviceInfo = {
|
|
423
426
|
userId: _handshake.userId,
|
|
424
427
|
deviceId: _handshake.deviceId,
|
|
@@ -429,8 +432,7 @@ class Connection {
|
|
|
429
432
|
};
|
|
430
433
|
this.trustLevel = await this.getTrustLevel(_handshake, true);
|
|
431
434
|
if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
|
|
432
|
-
this.
|
|
433
|
-
this.emit("reset");
|
|
435
|
+
this.resetRemote();
|
|
434
436
|
throw new Error("Untrusted connection");
|
|
435
437
|
}
|
|
436
438
|
this._verified = true;
|
|
@@ -465,9 +467,16 @@ class Connection {
|
|
|
465
467
|
if (remoteDeviceInfoSigned.publicKey !== remoteDeviceInfo.publicKey) {
|
|
466
468
|
throw new Error("Device info signing key does not match claimed identity key");
|
|
467
469
|
}
|
|
470
|
+
if (remoteDeviceInfo.userId === this.localDevice.userId &&
|
|
471
|
+
remoteDeviceInfo.publicKey !== this.localDevice.publicKey) {
|
|
472
|
+
// The server will refuse this handshake. Decide here, while we still have the signed
|
|
473
|
+
// identity, whether the anchors say this device's key was rotated away.
|
|
474
|
+
await (0, key_change_1.detectIdentityRotatedAway)(this.localDevice, remoteDeviceInfo);
|
|
475
|
+
}
|
|
468
476
|
const handshake = await this.initiateHandshake(remoteAddress, remoteDeviceInfo);
|
|
469
477
|
const handshakeResponseBox = await this.emit("completeHandshake", handshake);
|
|
470
478
|
const handshakeResponse = data_1.deviceHandshakeSchema.parse(await this.localDevice.openBoxedAndSignedData(handshakeResponseBox));
|
|
479
|
+
await noteHandshakeContest(handshakeResponse.keyContest);
|
|
471
480
|
if (handshakeResponse.connectionId !== this.connectionId) {
|
|
472
481
|
throw new Error(`Invalid connectionId ${handshakeResponse.connectionId}, expected ${this.connectionId}`);
|
|
473
482
|
}
|
|
@@ -522,12 +531,22 @@ class Connection {
|
|
|
522
531
|
}
|
|
523
532
|
this.trustLevel = await this.getTrustLevel(currentRemoteDeviceInfo, true);
|
|
524
533
|
if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
|
|
525
|
-
this.
|
|
526
|
-
this.emit("reset");
|
|
534
|
+
this.resetRemote();
|
|
527
535
|
throw new Error("Untrusted connection");
|
|
528
536
|
}
|
|
529
537
|
return handshakeResponse;
|
|
530
538
|
}
|
|
539
|
+
/**
|
|
540
|
+
* Resets this side and asks the remote to do the same. The remote call is a courtesy: a
|
|
541
|
+
* peer that is not a server has no `reset` handler, and a peer we just refused may already
|
|
542
|
+
* be gone, so its failure is logged and never allowed to escape as an unhandled rejection.
|
|
543
|
+
*/
|
|
544
|
+
resetRemote() {
|
|
545
|
+
this.reset();
|
|
546
|
+
this.emit("reset").catch((err) => {
|
|
547
|
+
console.warn(`Remote reset was not acknowledged: ${err?.error ?? err?.message ?? err}`);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
531
550
|
closeLocal() {
|
|
532
551
|
this.reset();
|
|
533
552
|
this.removeAllListeners();
|
|
@@ -559,6 +578,19 @@ class Connection {
|
|
|
559
578
|
}
|
|
560
579
|
}
|
|
561
580
|
exports.Connection = Connection;
|
|
581
|
+
async function noteHandshakeContest(contest) {
|
|
582
|
+
if (!contest)
|
|
583
|
+
return;
|
|
584
|
+
const userContext = (0, user_context_singleton_1.peekUserContext)();
|
|
585
|
+
if (!userContext)
|
|
586
|
+
return;
|
|
587
|
+
try {
|
|
588
|
+
await (0, key_change_1.applyKeyContest)(userContext.userDataContext, contest);
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
console.warn("Could not apply a key contest from the handshake", err);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
562
594
|
/** Normalizes a peer address for transport and security comparisons. */
|
|
563
595
|
function normalizeAddress(address) {
|
|
564
596
|
return address.toLowerCase().replace(/\/$/, "");
|
|
@@ -7,14 +7,19 @@ const binary_peer_connection_v2_1 = require("./binary-peer-connection-v2");
|
|
|
7
7
|
const connection_1 = require("./connection");
|
|
8
8
|
const device_1 = require("./device");
|
|
9
9
|
const frame_cipher_1 = require("./frame-cipher");
|
|
10
|
+
const socket_type_1 = require("./socket.type");
|
|
10
11
|
function createTestSocket(localHandlers, remoteHandlers) {
|
|
11
12
|
const socket = {
|
|
12
13
|
id: (0, utils_1.newid)(),
|
|
13
14
|
emit(eventName, args, callback) {
|
|
14
|
-
remoteHandlers.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
15
|
+
const matching = remoteHandlers.filter(({ eventName: _eventName }) => eventName === _eventName);
|
|
16
|
+
if (matching.length === 0) {
|
|
17
|
+
// Same answer the real transports give for an event nobody handles.
|
|
18
|
+
callback?.(`No handler registered for event: ${eventName}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
matching.forEach(async ({ handler }) => {
|
|
22
|
+
handler(args, callback);
|
|
18
23
|
});
|
|
19
24
|
},
|
|
20
25
|
on(eventName, handler) {
|
|
@@ -254,6 +259,30 @@ describe(connection_1.Connection, () => {
|
|
|
254
259
|
const result = await clientConnection.doHandshake(serverAddress).catch((err) => String(err));
|
|
255
260
|
expect(result).toMatch(/Inconsistent public keys/);
|
|
256
261
|
});
|
|
262
|
+
it("refuses an untrusted client without leaving an unhandled rejection behind", async () => {
|
|
263
|
+
// The server tells the client to reset as a courtesy. A client that is not itself a
|
|
264
|
+
// server has no `reset` handler, so that call fails; the failure must not escape as an
|
|
265
|
+
// unhandled rejection, which would take down a host with no process-level handler
|
|
266
|
+
// (peers-services) every time a device presents a retired key.
|
|
267
|
+
const serverAddress = "http://localhost";
|
|
268
|
+
const { clientSocket, serverSocket } = createTestSocketPair();
|
|
269
|
+
const clientConnection = new connection_1.Connection(clientSocket, new device_1.Device());
|
|
270
|
+
const serverConnection = new connection_1.Connection(serverSocket, new device_1.Device(), [serverAddress], () => Promise.resolve(socket_type_1.TrustLevel.Untrusted));
|
|
271
|
+
const unhandled = jest.fn();
|
|
272
|
+
process.on("unhandledRejection", unhandled);
|
|
273
|
+
try {
|
|
274
|
+
const result = await clientConnection
|
|
275
|
+
.doHandshake(serverAddress)
|
|
276
|
+
.catch((err) => err?.error ?? String(err));
|
|
277
|
+
expect(result).toMatch(/Untrusted connection/);
|
|
278
|
+
await (0, utils_1.sleep)(20);
|
|
279
|
+
expect(unhandled).not.toHaveBeenCalled();
|
|
280
|
+
expect(serverConnection.verified).toBe(false);
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
process.off("unhandledRejection", unhandled);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
257
286
|
it("should handle RPC calls with large arguments and responses", async () => {
|
|
258
287
|
const { clientSocket, serverSocket } = createTestSocketPair();
|
|
259
288
|
const clientDevice = new device_1.Device();
|
package/dist/device/device.d.ts
CHANGED
|
@@ -73,13 +73,13 @@ export declare class Device implements IDeviceInfo {
|
|
|
73
73
|
* Builds and signs the client half of a connection handshake.
|
|
74
74
|
* @param frameCrypto Include to advertise frame-cipher support with this connection's ephemeral key.
|
|
75
75
|
*/
|
|
76
|
-
getHandshake(connectionId: string, serverAddress: string, frameCrypto?: IHandshakeFrameCrypto): ISignedObject<IDeviceHandshake>;
|
|
76
|
+
getHandshake(connectionId: string, serverAddress: string, frameCrypto?: IHandshakeFrameCrypto, keyContest?: IDeviceHandshake["keyContest"]): ISignedObject<IDeviceHandshake>;
|
|
77
77
|
/**
|
|
78
78
|
* Validates a client handshake (signature, claimed key, connection ID) and
|
|
79
79
|
* builds the unsigned server response. `Connection` signs and boxes it.
|
|
80
80
|
* @param frameCrypto Include to accept the client's frame-cipher offer with this side's ephemeral key.
|
|
81
81
|
*/
|
|
82
|
-
handshakeResponse(remoteHandshake: ISignedObject<IDeviceHandshake>, connectionId: string, thisServerAddress: string, frameCrypto?: IHandshakeFrameCrypto): IDeviceHandshake;
|
|
82
|
+
handshakeResponse(remoteHandshake: ISignedObject<IDeviceHandshake>, connectionId: string, thisServerAddress: string, frameCrypto?: IHandshakeFrameCrypto, keyContest?: IDeviceHandshake["keyContest"]): IDeviceHandshake;
|
|
83
83
|
private deviceInfoSigned;
|
|
84
84
|
/** Returns cached signed identity information, refreshing after display metadata changes. */
|
|
85
85
|
getDeviceInfo(): ISignedObject<IDeviceInfo>;
|
package/dist/device/device.js
CHANGED
|
@@ -135,7 +135,7 @@ class Device {
|
|
|
135
135
|
* Builds and signs the client half of a connection handshake.
|
|
136
136
|
* @param frameCrypto Include to advertise frame-cipher support with this connection's ephemeral key.
|
|
137
137
|
*/
|
|
138
|
-
getHandshake(connectionId, serverAddress, frameCrypto) {
|
|
138
|
+
getHandshake(connectionId, serverAddress, frameCrypto, keyContest) {
|
|
139
139
|
try {
|
|
140
140
|
const localDeviceInfo = data_1.deviceHandshakeSchema.parse({
|
|
141
141
|
connectionId,
|
|
@@ -148,6 +148,7 @@ class Device {
|
|
|
148
148
|
serverAddress,
|
|
149
149
|
timestamp: Date.now(),
|
|
150
150
|
frameCrypto,
|
|
151
|
+
...(keyContest ? { keyContest } : {}),
|
|
151
152
|
});
|
|
152
153
|
const localDeviceInfoSigned = this.signObjectWithSecretKey(localDeviceInfo);
|
|
153
154
|
return localDeviceInfoSigned;
|
|
@@ -161,7 +162,7 @@ class Device {
|
|
|
161
162
|
* builds the unsigned server response. `Connection` signs and boxes it.
|
|
162
163
|
* @param frameCrypto Include to accept the client's frame-cipher offer with this side's ephemeral key.
|
|
163
164
|
*/
|
|
164
|
-
handshakeResponse(remoteHandshake, connectionId, thisServerAddress, frameCrypto) {
|
|
165
|
+
handshakeResponse(remoteHandshake, connectionId, thisServerAddress, frameCrypto, keyContest) {
|
|
165
166
|
try {
|
|
166
167
|
const deviceInfo = data_1.deviceHandshakeSchema.parse((0, keys_1.openSignedObject)(remoteHandshake));
|
|
167
168
|
if (remoteHandshake.publicKey !== deviceInfo.publicKey) {
|
|
@@ -181,6 +182,7 @@ class Device {
|
|
|
181
182
|
serverAddress: thisServerAddress,
|
|
182
183
|
timestamp: Date.now(),
|
|
183
184
|
frameCrypto,
|
|
185
|
+
...(keyContest ? { keyContest } : {}),
|
|
184
186
|
});
|
|
185
187
|
return handshakeResponse;
|
|
186
188
|
}
|
|
@@ -5,5 +5,6 @@ import { TrustLevel } from "./socket.type";
|
|
|
5
5
|
*
|
|
6
6
|
* Known user keys are authoritative even for a newly observed device: a claimed identity
|
|
7
7
|
* whose signing or box key differs from the stored user record is rejected as untrusted.
|
|
8
|
+
* Accepting a different key waits for the anchors on record to confirm it (identity anchors).
|
|
8
9
|
*/
|
|
9
10
|
export declare function getTrustLevelFn(me: Pick<IDeviceInfo, "userId" | "publicKey" | "publicBoxKey">, serverUrl?: string): (deviceInfo: IDeviceInfo, _registerNew?: boolean) => Promise<TrustLevel>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getTrustLevelFn = getTrustLevelFn;
|
|
4
4
|
const context_1 = require("../context");
|
|
5
5
|
const data_1 = require("../data");
|
|
6
|
+
const key_change_1 = require("../identity/key-change");
|
|
6
7
|
const socket_type_1 = require("./socket.type");
|
|
7
8
|
function applyReportedDeviceName(device, deviceInfo) {
|
|
8
9
|
if (deviceInfo.deviceName) {
|
|
@@ -18,11 +19,18 @@ function boundStoredDeviceNames(device) {
|
|
|
18
19
|
*
|
|
19
20
|
* Known user keys are authoritative even for a newly observed device: a claimed identity
|
|
20
21
|
* whose signing or box key differs from the stored user record is rejected as untrusted.
|
|
22
|
+
* Accepting a different key waits for the anchors on record to confirm it (identity anchors).
|
|
21
23
|
*/
|
|
22
24
|
function getTrustLevelFn(me, serverUrl) {
|
|
23
25
|
return async function getTrustLevel(deviceInfo, _registerNew) {
|
|
24
26
|
const userContext = await (0, context_1.getUserContext)();
|
|
25
27
|
const userDataContext = userContext.userDataContext;
|
|
28
|
+
if (deviceInfo.userId === me.userId && deviceInfo.publicKey !== me.publicKey) {
|
|
29
|
+
// Same user, different key. Anchors decide whether this device was rotated away.
|
|
30
|
+
// Either way the connection is refused: this device does not hold the key it was shown.
|
|
31
|
+
await (0, key_change_1.detectIdentityRotatedAway)(me, deviceInfo);
|
|
32
|
+
return socket_type_1.TrustLevel.Untrusted;
|
|
33
|
+
}
|
|
26
34
|
// if this is my own device it is trusted
|
|
27
35
|
if (deviceInfo.userId === me.userId &&
|
|
28
36
|
deviceInfo.publicKey === me.publicKey &&
|
|
@@ -85,13 +93,29 @@ function getTrustLevelFn(me, serverUrl) {
|
|
|
85
93
|
const effectiveUser = user ?? groupUser;
|
|
86
94
|
if (effectiveUser &&
|
|
87
95
|
(deviceInfo.userId !== effectiveUser.userId ||
|
|
88
|
-
deviceInfo.publicKey !== effectiveUser.publicKey ||
|
|
89
|
-
deviceInfo.publicBoxKey !== effectiveUser.publicBoxKey ||
|
|
90
96
|
(device ? deviceInfo.userId !== device.userId : false))) {
|
|
91
|
-
// deviceInfo does not align with local info about the user or device, return Untrusted
|
|
92
|
-
// TODO check if user has changed their public keys
|
|
93
97
|
return socket_type_1.TrustLevel.Untrusted;
|
|
94
98
|
}
|
|
99
|
+
if (effectiveUser &&
|
|
100
|
+
(deviceInfo.publicKey !== effectiveUser.publicKey ||
|
|
101
|
+
deviceInfo.publicBoxKey !== effectiveUser.publicBoxKey)) {
|
|
102
|
+
// A different box key for the key on record is a mismatch. A different signing key is
|
|
103
|
+
// accepted only when the anchors on record confirm a rotation.
|
|
104
|
+
if (deviceInfo.publicKey === effectiveUser.publicKey) {
|
|
105
|
+
return socket_type_1.TrustLevel.Untrusted;
|
|
106
|
+
}
|
|
107
|
+
const decision = await (0, key_change_1.evaluatePresentedKey)({
|
|
108
|
+
dataContext: userDataContext,
|
|
109
|
+
userId: deviceInfo.userId,
|
|
110
|
+
presentedKey: deviceInfo.publicKey,
|
|
111
|
+
presentedBoxKey: deviceInfo.publicBoxKey,
|
|
112
|
+
record: user ?? groupUser ?? undefined,
|
|
113
|
+
});
|
|
114
|
+
if (!decision.accepted) {
|
|
115
|
+
return socket_type_1.TrustLevel.Untrusted;
|
|
116
|
+
}
|
|
117
|
+
user = (await (0, data_1.Users)(userDataContext).get(deviceInfo.userId)) ?? user;
|
|
118
|
+
}
|
|
95
119
|
if (userTrustLevel?.trustLevel &&
|
|
96
120
|
userTrustLevel.trustLevel >= socket_type_1.TrustLevel.Trusted &&
|
|
97
121
|
device?.trustLevel &&
|
|
@@ -11,8 +11,13 @@ jest.mock("../data", () => ({
|
|
|
11
11
|
Users: jest.fn(),
|
|
12
12
|
UserTrustLevels: jest.fn(),
|
|
13
13
|
}));
|
|
14
|
+
jest.mock("../identity/key-change", () => ({
|
|
15
|
+
evaluatePresentedKey: jest.fn(async () => ({ accepted: false, state: "pending" })),
|
|
16
|
+
detectIdentityRotatedAway: jest.fn(async () => undefined),
|
|
17
|
+
}));
|
|
14
18
|
const context_1 = require("../context");
|
|
15
19
|
const data_1 = require("../data");
|
|
20
|
+
const key_change_1 = require("../identity/key-change");
|
|
16
21
|
const mockGetUserContext = context_1.getUserContext;
|
|
17
22
|
const mockDevices = data_1.Devices;
|
|
18
23
|
const mockUsers = data_1.Users;
|
|
@@ -587,4 +592,94 @@ describe("getTrustLevelFn", () => {
|
|
|
587
592
|
expect(mockUserTrustLevels).toHaveBeenCalled();
|
|
588
593
|
});
|
|
589
594
|
});
|
|
595
|
+
describe("anchor confirmation", () => {
|
|
596
|
+
it("accepts a different key when the anchors confirm a rotation", async () => {
|
|
597
|
+
const me = makeTestIdentity();
|
|
598
|
+
const other = makeTestIdentity();
|
|
599
|
+
const existingUser = {
|
|
600
|
+
userId: other.userId,
|
|
601
|
+
publicKey: `stored_${(0, utils_1.newid)()}`,
|
|
602
|
+
publicBoxKey: `stored_box_${(0, utils_1.newid)()}`,
|
|
603
|
+
name: "Alice",
|
|
604
|
+
};
|
|
605
|
+
const { usersTable } = setupMocks(null, existingUser, null);
|
|
606
|
+
usersTable.get.mockResolvedValueOnce(existingUser).mockResolvedValueOnce({
|
|
607
|
+
...existingUser,
|
|
608
|
+
publicKey: other.publicKey,
|
|
609
|
+
publicBoxKey: other.publicBoxKey,
|
|
610
|
+
});
|
|
611
|
+
key_change_1.evaluatePresentedKey.mockResolvedValueOnce({
|
|
612
|
+
accepted: true,
|
|
613
|
+
state: "confirmed-rotation",
|
|
614
|
+
});
|
|
615
|
+
const getTrustLevel = (0, get_trust_level_fn_1.getTrustLevelFn)(me);
|
|
616
|
+
const result = await getTrustLevel(other);
|
|
617
|
+
expect(key_change_1.evaluatePresentedKey).toHaveBeenCalledWith(expect.objectContaining({
|
|
618
|
+
userId: other.userId,
|
|
619
|
+
presentedKey: other.publicKey,
|
|
620
|
+
}));
|
|
621
|
+
expect(result).toBe(socket_type_1.TrustLevel.NewDevice);
|
|
622
|
+
});
|
|
623
|
+
it("writes nothing for a contact's key the anchors have not confirmed", async () => {
|
|
624
|
+
// Pending, vetoed, and a recovery still in its delay all land here: the handshake is
|
|
625
|
+
// refused and neither a device record nor a user stub is created for the presenter.
|
|
626
|
+
const me = makeTestIdentity();
|
|
627
|
+
const other = makeTestIdentity();
|
|
628
|
+
const existingUser = {
|
|
629
|
+
userId: other.userId,
|
|
630
|
+
publicKey: `stored_${(0, utils_1.newid)()}`,
|
|
631
|
+
publicBoxKey: `stored_box_${(0, utils_1.newid)()}`,
|
|
632
|
+
name: "Alice",
|
|
633
|
+
};
|
|
634
|
+
for (const state of ["pending", "vetoed", "recovery-pending"]) {
|
|
635
|
+
const { usersTable, devTable } = setupMocks(null, existingUser, null);
|
|
636
|
+
key_change_1.evaluatePresentedKey.mockResolvedValueOnce({ accepted: false, state });
|
|
637
|
+
const result = await (0, get_trust_level_fn_1.getTrustLevelFn)(me)(other);
|
|
638
|
+
expect(result).toBe(socket_type_1.TrustLevel.Untrusted);
|
|
639
|
+
expect(usersTable.save).not.toHaveBeenCalled();
|
|
640
|
+
expect(devTable.save).not.toHaveBeenCalled();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
it("judges a group-only contact's new key against the group row and copies nothing into the personal table", async () => {
|
|
644
|
+
const me = makeTestIdentity();
|
|
645
|
+
const other = makeTestIdentity();
|
|
646
|
+
const groupUser = {
|
|
647
|
+
userId: other.userId,
|
|
648
|
+
publicKey: `stored_${(0, utils_1.newid)()}`,
|
|
649
|
+
publicBoxKey: `stored_box_${(0, utils_1.newid)()}`,
|
|
650
|
+
name: "Alice",
|
|
651
|
+
};
|
|
652
|
+
const { usersTable, devTable } = setupMocks(null, null, null);
|
|
653
|
+
const groupUsersTable = makeUsersTable(groupUser);
|
|
654
|
+
const groupTrustTable = makeTrustTable({ trustLevel: socket_type_1.TrustLevel.Known });
|
|
655
|
+
const groupContext = { dataContextId: "group" };
|
|
656
|
+
mockGetUserContext.mockResolvedValue({
|
|
657
|
+
userDataContext: {},
|
|
658
|
+
groupDataContexts: new Map([["group", groupContext]]),
|
|
659
|
+
});
|
|
660
|
+
mockUsers.mockImplementation(((context) => context === groupContext ? groupUsersTable : usersTable));
|
|
661
|
+
mockUserTrustLevels.mockImplementation(((context) => context === groupContext ? groupTrustTable : makeTrustTable(null)));
|
|
662
|
+
key_change_1.evaluatePresentedKey.mockResolvedValueOnce({
|
|
663
|
+
accepted: true,
|
|
664
|
+
state: "confirmed-rotation",
|
|
665
|
+
});
|
|
666
|
+
const result = await (0, get_trust_level_fn_1.getTrustLevelFn)(me)(other);
|
|
667
|
+
expect(key_change_1.evaluatePresentedKey).toHaveBeenCalledWith(expect.objectContaining({ userId: other.userId, record: groupUser }));
|
|
668
|
+
// The group row is the only record; it is not copied into the personal table.
|
|
669
|
+
expect(usersTable.save).not.toHaveBeenCalled();
|
|
670
|
+
expect(groupUsersTable.save).not.toHaveBeenCalled();
|
|
671
|
+
expect(devTable.save).toHaveBeenCalledTimes(1);
|
|
672
|
+
expect(result).toBe(socket_type_1.TrustLevel.NewDevice);
|
|
673
|
+
});
|
|
674
|
+
it("treats another device of this user on a different key as untrusted", async () => {
|
|
675
|
+
const me = makeTestIdentity();
|
|
676
|
+
const otherDevice = { ...me, deviceId: (0, utils_1.newid)(), publicKey: `other_${(0, utils_1.newid)()}` };
|
|
677
|
+
setupMocks();
|
|
678
|
+
const getTrustLevel = (0, get_trust_level_fn_1.getTrustLevelFn)(me);
|
|
679
|
+
const result = await getTrustLevel(otherDevice);
|
|
680
|
+
expect(result).toBe(socket_type_1.TrustLevel.Untrusted);
|
|
681
|
+
expect(key_change_1.detectIdentityRotatedAway).toHaveBeenCalledWith(me, expect.objectContaining({ userId: me.userId, publicKey: otherDevice.publicKey }));
|
|
682
|
+
expect(mockUsers).not.toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
});
|
|
590
685
|
});
|