@peers-app/peers-sdk 0.23.13 → 0.24.0
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/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +4 -1
- package/dist/contracts/system/index.d.ts +2 -0
- package/dist/contracts/system/index.js +5 -1
- package/dist/contracts/system/invites-contract.d.ts +121 -0
- package/dist/contracts/system/invites-contract.js +150 -0
- package/dist/data/create-group.d.ts +24 -0
- package/dist/data/create-group.js +61 -0
- package/dist/data/group-secret.d.ts +10 -0
- package/dist/data/group-secret.js +26 -0
- package/dist/data/group-share.d.ts +0 -20
- package/dist/data/group-share.js +0 -35
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/invites.d.ts +350 -0
- package/dist/data/invites.js +99 -0
- package/dist/data/users.js +4 -0
- package/dist/device-pairing/constants.d.ts +27 -9
- package/dist/device-pairing/constants.js +28 -10
- package/dist/device-pairing/cpace.d.ts +46 -0
- package/dist/device-pairing/cpace.js +173 -0
- package/dist/device-pairing/cpace.test.js +116 -0
- package/dist/device-pairing/crypto.d.ts +40 -15
- package/dist/device-pairing/crypto.js +74 -33
- package/dist/device-pairing/device-pairing.test.js +156 -119
- package/dist/device-pairing/index.d.ts +3 -1
- package/dist/device-pairing/index.js +3 -1
- package/dist/device-pairing/pairing-code.d.ts +27 -0
- package/dist/device-pairing/pairing-code.js +84 -0
- package/dist/device-pairing/pgp-word-list.d.ts +14 -0
- package/dist/device-pairing/pgp-word-list.js +531 -0
- package/dist/device-pairing/signaling.d.ts +17 -6
- package/dist/device-pairing/signaling.js +31 -8
- package/dist/device-pairing/transcript.d.ts +3 -3
- package/dist/device-pairing/transcript.js +6 -13
- package/dist/device-pairing/types.d.ts +870 -224
- package/dist/device-pairing/types.js +186 -108
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/invites/data-box-schema.d.ts +15 -0
- package/dist/invites/data-box-schema.js +15 -0
- package/dist/invites/group-approval.d.ts +43 -0
- package/dist/invites/group-approval.js +155 -0
- package/dist/invites/group-approval.test.js +221 -0
- package/dist/invites/index.d.ts +5 -0
- package/dist/{group-invite → invites}/index.js +5 -3
- package/dist/invites/invite-epoch.d.ts +18 -0
- package/dist/invites/invite-epoch.js +35 -0
- package/dist/invites/invite-messages.d.ts +1354 -0
- package/dist/invites/invite-messages.js +145 -0
- package/dist/invites/invite-token.d.ts +334 -0
- package/dist/invites/invite-token.js +178 -0
- package/dist/invites/invite-token.test.d.ts +1 -0
- package/dist/invites/invite-token.test.js +123 -0
- package/dist/rpc-types.d.ts +5 -4
- package/dist/rpc-types.js +2 -1
- package/dist/system-ids.d.ts +22 -0
- package/dist/system-ids.js +23 -1
- package/dist/types/peer-device.d.ts +15 -1
- package/dist/types/peer-device.js +15 -1
- package/package.json +1 -1
- package/dist/device-pairing/invitation.d.ts +0 -13
- package/dist/device-pairing/invitation.js +0 -85
- package/dist/group-invite/group-invite.d.ts +0 -53
- package/dist/group-invite/group-invite.js +0 -231
- package/dist/group-invite/group-invite.pvars.d.ts +0 -94
- package/dist/group-invite/group-invite.pvars.js +0 -139
- package/dist/group-invite/group-invite.types.d.ts +0 -124
- package/dist/group-invite/group-invite.types.js +0 -8
- package/dist/group-invite/index.d.ts +0 -3
- package/dist/user-connect/connection-code.d.ts +0 -67
- package/dist/user-connect/connection-code.js +0 -176
- package/dist/user-connect/connection-code.test.js +0 -213
- package/dist/user-connect/index.d.ts +0 -3
- package/dist/user-connect/index.js +0 -19
- package/dist/user-connect/user-connect.pvars.d.ts +0 -20
- package/dist/user-connect/user-connect.pvars.js +0 -63
- package/dist/user-connect/user-connect.pvars.test.js +0 -46
- package/dist/user-connect/user-connect.types.d.ts +0 -94
- package/dist/user-connect/user-connect.types.js +0 -68
- /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
- /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cpacePrependLength = cpacePrependLength;
|
|
4
|
+
exports.cpaceLvCat = cpaceLvCat;
|
|
5
|
+
exports.cpaceGeneratorString = cpaceGeneratorString;
|
|
6
|
+
exports.cpaceElligator2 = cpaceElligator2;
|
|
7
|
+
exports.cpaceCalculateGenerator = cpaceCalculateGenerator;
|
|
8
|
+
exports.cpaceScalarMultVerify = cpaceScalarMultVerify;
|
|
9
|
+
exports.cpaceTranscript = cpaceTranscript;
|
|
10
|
+
exports.cpaceDeriveIsk = cpaceDeriveIsk;
|
|
11
|
+
exports.cpaceStart = cpaceStart;
|
|
12
|
+
const sha2_1 = require("@noble/hashes/sha2");
|
|
13
|
+
const nacl = require("tweetnacl");
|
|
14
|
+
/**
|
|
15
|
+
* CPace balanced PAKE over X25519 with SHA-512 (cipher suite `CPace255`), implemented per
|
|
16
|
+
* draft-irtf-cfrg-cpace. Both parties derive the same 64-byte intermediate session key (ISK)
|
|
17
|
+
* if and only if they used the same password, channel identifier, and session id, and saw
|
|
18
|
+
* each other's public shares unmodified. A wrong password yields an unrelated ISK; nothing
|
|
19
|
+
* about the password can be recovered from the transcript.
|
|
20
|
+
*
|
|
21
|
+
* Group operations use `tweetnacl`'s X25519 (`scalarMult`); the Elligator 2 map that turns
|
|
22
|
+
* the hashed password into a secret generator is implemented here on `bigint` field
|
|
23
|
+
* arithmetic (Curve25519, `A = 486662`, `Z = 2`). Verified against the draft's test vectors.
|
|
24
|
+
*/
|
|
25
|
+
const FIELD_PRIME = 2n ** 255n - 19n;
|
|
26
|
+
const CURVE_A = 486662n;
|
|
27
|
+
const ELLIGATOR_Z = 2n;
|
|
28
|
+
const FIELD_SIZE_BYTES = 32;
|
|
29
|
+
const HASH_BLOCK_BYTES = 128;
|
|
30
|
+
const DSI = new TextEncoder().encode("CPace255");
|
|
31
|
+
const DSI_ISK = new TextEncoder().encode("CPace255_ISK");
|
|
32
|
+
function mod(value) {
|
|
33
|
+
const result = value % FIELD_PRIME;
|
|
34
|
+
return result >= 0n ? result : result + FIELD_PRIME;
|
|
35
|
+
}
|
|
36
|
+
function modPow(base, exponent) {
|
|
37
|
+
let result = 1n;
|
|
38
|
+
let current = mod(base);
|
|
39
|
+
let remaining = exponent;
|
|
40
|
+
while (remaining > 0n) {
|
|
41
|
+
if (remaining & 1n) {
|
|
42
|
+
result = mod(result * current);
|
|
43
|
+
}
|
|
44
|
+
current = mod(current * current);
|
|
45
|
+
remaining >>= 1n;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function modInverse(value) {
|
|
50
|
+
return modPow(value, FIELD_PRIME - 2n);
|
|
51
|
+
}
|
|
52
|
+
/** LEB128 length prefix as defined by the draft's `prepend_len`. */
|
|
53
|
+
function cpacePrependLength(data) {
|
|
54
|
+
const prefix = [];
|
|
55
|
+
let length = data.length;
|
|
56
|
+
while (true) {
|
|
57
|
+
if (length < 128) {
|
|
58
|
+
prefix.push(length);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
prefix.push((length & 0x7f) + 0x80);
|
|
62
|
+
}
|
|
63
|
+
length >>= 7;
|
|
64
|
+
if (length === 0) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return concatBytes(Uint8Array.from(prefix), data);
|
|
69
|
+
}
|
|
70
|
+
/** Length-prefixed concatenation (`lv_cat`). */
|
|
71
|
+
function cpaceLvCat(...parts) {
|
|
72
|
+
return concatBytes(...parts.map((part) => cpacePrependLength(part)));
|
|
73
|
+
}
|
|
74
|
+
function concatBytes(...parts) {
|
|
75
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
76
|
+
const result = new Uint8Array(total);
|
|
77
|
+
let offset = 0;
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
result.set(part, offset);
|
|
80
|
+
offset += part.length;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/** Builds the `generator_string` that is hashed into the secret generator. */
|
|
85
|
+
function cpaceGeneratorString(password, channelId, sessionId) {
|
|
86
|
+
const zeroPadLength = Math.max(0, HASH_BLOCK_BYTES - 1 - cpacePrependLength(password).length - cpacePrependLength(DSI).length);
|
|
87
|
+
return cpaceLvCat(DSI, password, new Uint8Array(zeroPadLength), channelId, sessionId);
|
|
88
|
+
}
|
|
89
|
+
function decodeUCoordinate(bytes) {
|
|
90
|
+
const copy = Uint8Array.from(bytes);
|
|
91
|
+
copy[FIELD_SIZE_BYTES - 1] &= 0x7f;
|
|
92
|
+
let value = 0n;
|
|
93
|
+
for (let index = copy.length - 1; index >= 0; index--) {
|
|
94
|
+
value = (value << 8n) | BigInt(copy[index]);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function encodeUCoordinate(value) {
|
|
99
|
+
const bytes = new Uint8Array(FIELD_SIZE_BYTES);
|
|
100
|
+
let remaining = mod(value);
|
|
101
|
+
for (let index = 0; index < FIELD_SIZE_BYTES; index++) {
|
|
102
|
+
bytes[index] = Number(remaining & 0xffn);
|
|
103
|
+
remaining >>= 8n;
|
|
104
|
+
}
|
|
105
|
+
return bytes;
|
|
106
|
+
}
|
|
107
|
+
/** Elligator 2 map from a field element to a Curve25519 u-coordinate (RFC 9380 `map_to_curve_elligator2`). */
|
|
108
|
+
function cpaceElligator2(u) {
|
|
109
|
+
const v = mod(-CURVE_A * modInverse(1n + ELLIGATOR_Z * u * u));
|
|
110
|
+
const legendre = modPow(mod(v * v * v + CURVE_A * v * v + v), (FIELD_PRIME - 1n) / 2n);
|
|
111
|
+
const x = mod(legendre * v - (1n - legendre) * CURVE_A * modInverse(2n));
|
|
112
|
+
return encodeUCoordinate(x);
|
|
113
|
+
}
|
|
114
|
+
/** Derives the password-dependent generator `g` for one exchange. */
|
|
115
|
+
function cpaceCalculateGenerator(password, channelId, sessionId = new Uint8Array(0)) {
|
|
116
|
+
const digest = (0, sha2_1.sha512)(cpaceGeneratorString(password, channelId, sessionId));
|
|
117
|
+
return cpaceElligator2(decodeUCoordinate(digest.slice(0, FIELD_SIZE_BYTES)));
|
|
118
|
+
}
|
|
119
|
+
function isAllZero(bytes) {
|
|
120
|
+
let accumulator = 0;
|
|
121
|
+
for (const byte of bytes) {
|
|
122
|
+
accumulator |= byte;
|
|
123
|
+
}
|
|
124
|
+
return accumulator === 0;
|
|
125
|
+
}
|
|
126
|
+
/** X25519 scalar multiplication that rejects results in the small subgroup (`scalar_mult_vfy`). */
|
|
127
|
+
function cpaceScalarMultVerify(scalar, point) {
|
|
128
|
+
if (scalar.length !== FIELD_SIZE_BYTES || point.length !== FIELD_SIZE_BYTES) {
|
|
129
|
+
throw new Error("CPace shares must be 32 bytes");
|
|
130
|
+
}
|
|
131
|
+
const result = nacl.scalarMult(scalar, point);
|
|
132
|
+
if (isAllZero(result)) {
|
|
133
|
+
throw new Error("CPace received a low-order point");
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/** Initiator/responder transcript (`transcript_ir`). */
|
|
138
|
+
function cpaceTranscript(initiatorShare, initiatorAd, responderShare, responderAd) {
|
|
139
|
+
return concatBytes(cpaceLvCat(initiatorShare, initiatorAd), cpaceLvCat(responderShare, responderAd));
|
|
140
|
+
}
|
|
141
|
+
/** Final ISK derivation: `H(lv_cat(DSI_ISK, sid, K) || transcript)`. */
|
|
142
|
+
function cpaceDeriveIsk(sharedPoint, sessionId, transcript) {
|
|
143
|
+
return (0, sha2_1.sha512)(concatBytes(cpaceLvCat(DSI_ISK, sessionId, sharedPoint), transcript));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Starts one side of a CPace exchange. The returned share is safe to send in the clear.
|
|
147
|
+
* `scalar` may be injected for deterministic test vectors; production callers omit it.
|
|
148
|
+
*/
|
|
149
|
+
function cpaceStart(role, parameters, scalar = nacl.randomBytes(FIELD_SIZE_BYTES)) {
|
|
150
|
+
const sessionId = parameters.sessionId ?? new Uint8Array(0);
|
|
151
|
+
const initiatorAd = parameters.initiatorAd ?? new Uint8Array(0);
|
|
152
|
+
const responderAd = parameters.responderAd ?? new Uint8Array(0);
|
|
153
|
+
const generator = cpaceCalculateGenerator(parameters.password, parameters.channelId, sessionId);
|
|
154
|
+
const share = cpaceScalarMultVerify(scalar, generator);
|
|
155
|
+
let finished = false;
|
|
156
|
+
return {
|
|
157
|
+
share,
|
|
158
|
+
finish(remoteShare) {
|
|
159
|
+
if (finished) {
|
|
160
|
+
throw new Error("CPace session already finished");
|
|
161
|
+
}
|
|
162
|
+
finished = true;
|
|
163
|
+
const sharedPoint = cpaceScalarMultVerify(scalar, remoteShare);
|
|
164
|
+
const transcript = role === "initiator"
|
|
165
|
+
? cpaceTranscript(share, initiatorAd, remoteShare, responderAd)
|
|
166
|
+
: cpaceTranscript(remoteShare, initiatorAd, share, responderAd);
|
|
167
|
+
const isk = cpaceDeriveIsk(sharedPoint, sessionId, transcript);
|
|
168
|
+
scalar.fill(0);
|
|
169
|
+
sharedPoint.fill(0);
|
|
170
|
+
return isk;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cpace_1 = require("./cpace");
|
|
4
|
+
// Test vectors from draft-irtf-cfrg-cpace, "CPace using group X25519 and hash SHA-512".
|
|
5
|
+
const VECTOR = {
|
|
6
|
+
PRS: "50617373776F7264",
|
|
7
|
+
CI: "0B415F696E69746961746F720B425F726573706F6E646572",
|
|
8
|
+
sid: "7E4B4791D6A8EF019B936C79FB7F2C57",
|
|
9
|
+
g: "D04BF6D41F6A289632A2E929FA29BEBD51092512A7829FDDE7D314B62F05A73F",
|
|
10
|
+
ya: "21B4F4BD9E64ED355C3EB676A28EBEDAF6D8F17BDC365995B319097153044080",
|
|
11
|
+
ADa: "414461",
|
|
12
|
+
Ya: "1D13C89278CDADD826F6D8D7F887701430F8380DDC17611CDD6DC989CE0C9F32",
|
|
13
|
+
yb: "848B0779FF415F0AF4EA14DF9DD1D3C29AC41D836C7808896C4EBA19C51AC40A",
|
|
14
|
+
ADb: "414462",
|
|
15
|
+
Yb: "248CCCF6D5CDC3646F0AD593F9E6CEF4E69D4945F8372E623512ECEA32185623",
|
|
16
|
+
K: "5B067EFFBDC0B2A0E1D907B21EBB25CFEDB96A852179A847C37E43EE71322C6B",
|
|
17
|
+
ISK_IR: "6E19B875F7A561D6B3CA3DBB9EF42AC55DE3E717881018204B8922B4D5E53BB2AA82C300BEA7B65D2B671DA71922DDF6472301B79BC270ADFA8BF413285F2263",
|
|
18
|
+
};
|
|
19
|
+
// scalar_mult_vfy vectors: low-order (and non-canonical low-order) inputs abort; the rest
|
|
20
|
+
// must produce the draft's exact outputs, proving bit #255 is cleared before the ladder.
|
|
21
|
+
const VFY_SCALAR = "AF46E36BF0527C9D3B16154B82465EDD62144C0AC1FC5A18506A2244BA449AFF";
|
|
22
|
+
const LOW_ORDER_POINTS = [
|
|
23
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
24
|
+
"0100000000000000000000000000000000000000000000000000000000000000",
|
|
25
|
+
"ECFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
|
|
26
|
+
"E0EB7A7C3B41B8AE1656E3FAF19FC46ADA098DEB9C32B1FD866205165F49B800",
|
|
27
|
+
"5F9C95BCA3508C24B1D0B1559C83EF5B04445CC4581C8E86D8224EDDD09F1157",
|
|
28
|
+
"EDFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
|
|
29
|
+
"EEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F",
|
|
30
|
+
];
|
|
31
|
+
const NON_CANONICAL_POINTS = [
|
|
32
|
+
[
|
|
33
|
+
"DAFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
|
34
|
+
"D8E2C776BBACD510D09FD9278B7EDCD25FC5AE9ADFBA3B6E040E8D3B71B21806",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"DBFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
|
38
|
+
"C85C655EBE8BE44BA9C0FFDE69F2FE10194458D137F09BBFF725CE58803CDB38",
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"D9FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
|
42
|
+
"DB64DAFA9B8FDD136914E61461935FE92AA372CB056314E1231BC4EC12417456",
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"CDEB7A7C3B41B8AE1656E3FAF19FC46ADA098DEB9C32B1FD866205165F49B880",
|
|
46
|
+
"E062DCD5376D58297BE2618C7498F55BAA07D7E03184E8AADA20BCA28888BF7A",
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
"4C9C95BCA3508C24B1D0B1559C83EF5B04445CC4581C8E86D8224EDDD09F11D7",
|
|
50
|
+
"993C6AD11C4C29DA9A56F7691FD0FF8D732E49DE6250B6C2E80003FF4629A175",
|
|
51
|
+
],
|
|
52
|
+
];
|
|
53
|
+
function hex(value) {
|
|
54
|
+
return Uint8Array.from(Buffer.from(value, "hex"));
|
|
55
|
+
}
|
|
56
|
+
function toHex(value) {
|
|
57
|
+
return Buffer.from(value).toString("hex").toUpperCase();
|
|
58
|
+
}
|
|
59
|
+
describe("cpace string utilities", () => {
|
|
60
|
+
it("matches prepend_len and lv_cat vectors", () => {
|
|
61
|
+
expect(toHex((0, cpace_1.cpacePrependLength)(new Uint8Array(0)))).toBe("00");
|
|
62
|
+
expect(toHex((0, cpace_1.cpacePrependLength)(Buffer.from("1234")))).toBe("0431323334");
|
|
63
|
+
expect(toHex((0, cpace_1.cpacePrependLength)(new Uint8Array(128))).slice(0, 4)).toBe("8001");
|
|
64
|
+
expect(toHex((0, cpace_1.cpaceLvCat)(Buffer.from("1234"), Buffer.from("5"), new Uint8Array(0), Buffer.from("678")))).toBe("043132333401350003363738");
|
|
65
|
+
});
|
|
66
|
+
it("matches transcript_ir vector", () => {
|
|
67
|
+
expect(toHex((0, cpace_1.cpaceTranscript)(Buffer.from("123"), Buffer.from("PartyA"), Buffer.from("234"), Buffer.from("PartyB")))).toBe("03313233065061727479410332333406506172747942");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("cpace CPace255 vectors", () => {
|
|
71
|
+
it("derives the generator string and secret generator", () => {
|
|
72
|
+
const generatorString = (0, cpace_1.cpaceGeneratorString)(hex(VECTOR.PRS), hex(VECTOR.CI), hex(VECTOR.sid));
|
|
73
|
+
expect(generatorString).toHaveLength(170);
|
|
74
|
+
expect(toHex(generatorString).startsWith("0843506163653235350850617373776F72646D00")).toBe(true);
|
|
75
|
+
expect(toHex((0, cpace_1.cpaceCalculateGenerator)(hex(VECTOR.PRS), hex(VECTOR.CI), hex(VECTOR.sid)))).toBe(VECTOR.g);
|
|
76
|
+
});
|
|
77
|
+
it("derives both shares, the shared point, and the initiator/responder ISK", () => {
|
|
78
|
+
const parameters = {
|
|
79
|
+
password: hex(VECTOR.PRS),
|
|
80
|
+
channelId: hex(VECTOR.CI),
|
|
81
|
+
sessionId: hex(VECTOR.sid),
|
|
82
|
+
initiatorAd: hex(VECTOR.ADa),
|
|
83
|
+
responderAd: hex(VECTOR.ADb),
|
|
84
|
+
};
|
|
85
|
+
const initiator = (0, cpace_1.cpaceStart)("initiator", parameters, hex(VECTOR.ya));
|
|
86
|
+
const responder = (0, cpace_1.cpaceStart)("responder", parameters, hex(VECTOR.yb));
|
|
87
|
+
expect(toHex(initiator.share)).toBe(VECTOR.Ya);
|
|
88
|
+
expect(toHex(responder.share)).toBe(VECTOR.Yb);
|
|
89
|
+
expect(toHex((0, cpace_1.cpaceScalarMultVerify)(hex(VECTOR.ya), hex(VECTOR.Yb)))).toBe(VECTOR.K);
|
|
90
|
+
expect(toHex((0, cpace_1.cpaceDeriveIsk)(hex(VECTOR.K), hex(VECTOR.sid), (0, cpace_1.cpaceTranscript)(hex(VECTOR.Ya), hex(VECTOR.ADa), hex(VECTOR.Yb), hex(VECTOR.ADb))))).toBe(VECTOR.ISK_IR);
|
|
91
|
+
const initiatorIsk = toHex(initiator.finish(hex(VECTOR.Yb)));
|
|
92
|
+
const responderIsk = toHex(responder.finish(hex(VECTOR.Ya)));
|
|
93
|
+
expect(initiatorIsk).toBe(VECTOR.ISK_IR);
|
|
94
|
+
expect(responderIsk).toBe(VECTOR.ISK_IR);
|
|
95
|
+
});
|
|
96
|
+
it("rejects low-order points and clears bit #255 of non-canonical inputs", () => {
|
|
97
|
+
for (const point of LOW_ORDER_POINTS) {
|
|
98
|
+
expect(() => (0, cpace_1.cpaceScalarMultVerify)(hex(VFY_SCALAR), hex(point))).toThrow(/low-order/);
|
|
99
|
+
}
|
|
100
|
+
for (const [point, expected] of NON_CANONICAL_POINTS) {
|
|
101
|
+
expect(toHex((0, cpace_1.cpaceScalarMultVerify)(hex(VFY_SCALAR), hex(point)))).toBe(expected);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
it("produces unrelated keys for different passwords and identical keys for the same password", () => {
|
|
105
|
+
const shared = { channelId: Buffer.from("channel"), sessionId: new Uint8Array(0) };
|
|
106
|
+
const good = (0, cpace_1.cpaceStart)("initiator", { ...shared, password: Buffer.from("guitar-revenge") });
|
|
107
|
+
const right = (0, cpace_1.cpaceStart)("responder", { ...shared, password: Buffer.from("guitar-revenge") });
|
|
108
|
+
const wrong = (0, cpace_1.cpaceStart)("responder", { ...shared, password: Buffer.from("guitar-reward") });
|
|
109
|
+
const rightShare = Uint8Array.from(right.share);
|
|
110
|
+
const wrongShare = Uint8Array.from(wrong.share);
|
|
111
|
+
const goodShare = Uint8Array.from(good.share);
|
|
112
|
+
expect(toHex(right.finish(goodShare))).toBe(toHex(good.finish(rightShare)));
|
|
113
|
+
const second = (0, cpace_1.cpaceStart)("initiator", { ...shared, password: Buffer.from("guitar-revenge") });
|
|
114
|
+
expect(toHex(wrong.finish(Uint8Array.from(second.share)))).not.toBe(toHex(second.finish(wrongShare)));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,25 +1,50 @@
|
|
|
1
1
|
import type { IDeviceInfo } from "../data/devices";
|
|
2
2
|
import type { Device } from "../device/device";
|
|
3
|
-
import { type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
import { type PairingCode } from "./pairing-code";
|
|
4
|
+
import { type PairingConfirmation, type PairingConfirmationContents, type PairingCredentialContents, type PairingRole, type SignedPairingInstallationReceipt, type UnsignedPairingSignal } from "./types";
|
|
5
|
+
/** Symmetric keys both endpoints derive from the PAKE intermediate session key. */
|
|
6
|
+
export interface PairingSessionKeys {
|
|
7
|
+
/** Authenticates transport, SDP, and ICE signaling relayed by the service. */
|
|
8
|
+
signalingKey: string;
|
|
9
|
+
/** Authenticates the post-handshake transcript confirmation. */
|
|
10
|
+
confirmKey: string;
|
|
11
|
+
}
|
|
12
|
+
/** One endpoint's in-progress PAKE over the pairing code. */
|
|
13
|
+
export interface PairingPake {
|
|
14
|
+
/** Base64url public share to send in the `pake` signal. */
|
|
15
|
+
share: string;
|
|
16
|
+
/** Consumes the remote share and derives the session keys; throws on an invalid share. */
|
|
17
|
+
finish(remoteShare: string): PairingSessionKeys;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Starts CPace for one role. The source is the initiator and the destination the responder;
|
|
21
|
+
* the exchange is bound to the service origin and nameplate so a relayed share from another
|
|
22
|
+
* room cannot be replayed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startPairingPake(role: PairingRole, code: PairingCode, serviceOrigin: string): PairingPake;
|
|
25
|
+
/** Derives independent signaling and confirmation keys from a PAKE intermediate session key. */
|
|
26
|
+
export declare function derivePairingSessionKeys(isk: Uint8Array): PairingSessionKeys;
|
|
27
|
+
/**
|
|
28
|
+
* Derives the token a source presents (`auth.token`) when it connects to the destination's direct
|
|
29
|
+
* WebSocket pairing listener. Both endpoints compute it from the signaling key, so it never
|
|
30
|
+
* travels through the rendezvous; a socket without it is refused before any Connection exists.
|
|
31
|
+
*/
|
|
32
|
+
export declare function derivePairingWebSocketToken(signalingKey: string): string;
|
|
10
33
|
/** Computes the MAC over the canonical unsigned signaling envelope. */
|
|
11
34
|
export declare function computePairingSignalMac(signal: UnsignedPairingSignal, signalingKey: string): string;
|
|
12
35
|
/** Verifies a signaling MAC without short-circuiting on the first differing byte. */
|
|
13
36
|
export declare function isPairingSignalMacValid(signal: UnsignedPairingSignal, mac: string, signalingKey: string): boolean;
|
|
14
|
-
/**
|
|
15
|
-
export declare function
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
/** Builds the local `pairing.confirm` message authenticated with the PAKE confirmation key. */
|
|
38
|
+
export declare function createPairingConfirmation(contents: Omit<PairingConfirmationContents, "version">, confirmKey: string): PairingConfirmation;
|
|
39
|
+
/**
|
|
40
|
+
* Verifies the remote confirmation's MAC, role, transcript, and identity binding.
|
|
41
|
+
* A MAC failure means the other endpoint used a different code or saw a different transcript.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifyPairingConfirmation(value: unknown, confirmKey: string, expected: {
|
|
20
44
|
role: PairingRole;
|
|
21
|
-
|
|
22
|
-
|
|
45
|
+
transcriptHash: string;
|
|
46
|
+
device: IDeviceInfo;
|
|
47
|
+
}): PairingConfirmation;
|
|
23
48
|
/** Signs and encrypts account credentials directly to the temporary destination identity. */
|
|
24
49
|
export declare function createPairingCredentialEnvelope(sourceDevice: Device, destinationDevice: IDeviceInfo, credentials: {
|
|
25
50
|
userId: string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
exports.
|
|
3
|
+
exports.startPairingPake = startPairingPake;
|
|
4
|
+
exports.derivePairingSessionKeys = derivePairingSessionKeys;
|
|
5
|
+
exports.derivePairingWebSocketToken = derivePairingWebSocketToken;
|
|
6
6
|
exports.computePairingSignalMac = computePairingSignalMac;
|
|
7
7
|
exports.isPairingSignalMacValid = isPairingSignalMacValid;
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
8
|
+
exports.createPairingConfirmation = createPairingConfirmation;
|
|
9
|
+
exports.verifyPairingConfirmation = verifyPairingConfirmation;
|
|
10
10
|
exports.createPairingCredentialEnvelope = createPairingCredentialEnvelope;
|
|
11
11
|
exports.openPairingCredentialEnvelope = openPairingCredentialEnvelope;
|
|
12
12
|
exports.createSignedPairingInstallationReceipt = createSignedPairingInstallationReceipt;
|
|
@@ -15,6 +15,8 @@ const hmac_1 = require("@noble/hashes/hmac");
|
|
|
15
15
|
const sha2_1 = require("@noble/hashes/sha2");
|
|
16
16
|
const keys_1 = require("../keys");
|
|
17
17
|
const constants_1 = require("./constants");
|
|
18
|
+
const cpace_1 = require("./cpace");
|
|
19
|
+
const pairing_code_1 = require("./pairing-code");
|
|
18
20
|
const types_1 = require("./types");
|
|
19
21
|
const textEncoder = new TextEncoder();
|
|
20
22
|
function computeHmac(key, value) {
|
|
@@ -30,17 +32,42 @@ function equalBytes(left, right) {
|
|
|
30
32
|
}
|
|
31
33
|
return difference === 0;
|
|
32
34
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Starts CPace for one role. The source is the initiator and the destination the responder;
|
|
37
|
+
* the exchange is bound to the service origin and nameplate so a relayed share from another
|
|
38
|
+
* room cannot be replayed.
|
|
39
|
+
*/
|
|
40
|
+
function startPairingPake(role, code, serviceOrigin) {
|
|
41
|
+
const session = (0, cpace_1.cpaceStart)(role === "source" ? "initiator" : "responder", {
|
|
42
|
+
password: (0, pairing_code_1.pairingPasswordFromCode)(code),
|
|
43
|
+
channelId: (0, pairing_code_1.pairingChannelId)(serviceOrigin, code.nameplate),
|
|
44
|
+
initiatorAd: textEncoder.encode("source"),
|
|
45
|
+
responderAd: textEncoder.encode("destination"),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
share: (0, keys_1.encodeBase64)(session.share),
|
|
49
|
+
finish(remoteShare) {
|
|
50
|
+
const isk = session.finish((0, keys_1.decodeBase64)(remoteShare));
|
|
51
|
+
const keys = derivePairingSessionKeys(isk);
|
|
52
|
+
isk.fill(0);
|
|
53
|
+
return keys;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
36
56
|
}
|
|
37
|
-
/**
|
|
38
|
-
function
|
|
39
|
-
return
|
|
57
|
+
/** Derives independent signaling and confirmation keys from a PAKE intermediate session key. */
|
|
58
|
+
function derivePairingSessionKeys(isk) {
|
|
59
|
+
return {
|
|
60
|
+
signalingKey: (0, keys_1.encodeBase64)(computeHmac(isk, constants_1.DEVICE_PAIRING_SIGNALING_LABEL)),
|
|
61
|
+
confirmKey: (0, keys_1.encodeBase64)(computeHmac(isk, constants_1.DEVICE_PAIRING_CONFIRM_LABEL)),
|
|
62
|
+
};
|
|
40
63
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Derives the token a source presents (`auth.token`) when it connects to the destination's direct
|
|
66
|
+
* WebSocket pairing listener. Both endpoints compute it from the signaling key, so it never
|
|
67
|
+
* travels through the rendezvous; a socket without it is refused before any Connection exists.
|
|
68
|
+
*/
|
|
69
|
+
function derivePairingWebSocketToken(signalingKey) {
|
|
70
|
+
return (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(signalingKey), constants_1.DEVICE_PAIRING_WEBSOCKET_TOKEN_LABEL));
|
|
44
71
|
}
|
|
45
72
|
/** Computes the MAC over the canonical unsigned signaling envelope. */
|
|
46
73
|
function computePairingSignalMac(signal, signalingKey) {
|
|
@@ -56,31 +83,45 @@ function isPairingSignalMacValid(signal, mac, signalingKey) {
|
|
|
56
83
|
return false;
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
|
-
/**
|
|
60
|
-
function
|
|
61
|
-
const
|
|
86
|
+
/** Builds the local `pairing.confirm` message authenticated with the PAKE confirmation key. */
|
|
87
|
+
function createPairingConfirmation(contents, confirmKey) {
|
|
88
|
+
const parsed = types_1.pairingConfirmationContentsSchema.parse({
|
|
62
89
|
version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
...contents,
|
|
91
|
+
});
|
|
92
|
+
return types_1.pairingConfirmationSchema.parse({
|
|
93
|
+
...parsed,
|
|
94
|
+
mac: (0, keys_1.encodeBase64)(computeHmac((0, keys_1.decodeBase64)(confirmKey), (0, keys_1.stableStringify)(parsed))),
|
|
67
95
|
});
|
|
68
|
-
return types_1.signedPairingApprovalSchema.parse(device.signObjectWithSecretKey(contents));
|
|
69
96
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Verifies the remote confirmation's MAC, role, transcript, and identity binding.
|
|
99
|
+
* A MAC failure means the other endpoint used a different code or saw a different transcript.
|
|
100
|
+
*/
|
|
101
|
+
function verifyPairingConfirmation(value, confirmKey, expected) {
|
|
102
|
+
const parsed = types_1.pairingConfirmationSchema.parse(value);
|
|
103
|
+
const { mac, ...contents } = parsed;
|
|
104
|
+
const expectedMac = computeHmac((0, keys_1.decodeBase64)(confirmKey), (0, keys_1.stableStringify)(contents));
|
|
105
|
+
let macValid = false;
|
|
106
|
+
try {
|
|
107
|
+
macValid = equalBytes(expectedMac, (0, keys_1.decodeBase64)(mac));
|
|
75
108
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw new Error("Pairing approval does not match this ceremony");
|
|
109
|
+
catch {
|
|
110
|
+
macValid = false;
|
|
79
111
|
}
|
|
80
|
-
if (
|
|
81
|
-
throw new Error("Pairing
|
|
112
|
+
if (!macValid) {
|
|
113
|
+
throw new Error("Pairing code did not match");
|
|
114
|
+
}
|
|
115
|
+
if (contents.role !== expected.role || contents.transcriptHash !== expected.transcriptHash) {
|
|
116
|
+
throw new Error("Pairing confirmation does not match this ceremony");
|
|
117
|
+
}
|
|
118
|
+
if (contents.device.userId !== expected.device.userId ||
|
|
119
|
+
contents.device.deviceId !== expected.device.deviceId ||
|
|
120
|
+
contents.device.publicKey !== expected.device.publicKey ||
|
|
121
|
+
contents.device.publicBoxKey !== expected.device.publicBoxKey) {
|
|
122
|
+
throw new Error("Pairing confirmation identity mismatch");
|
|
82
123
|
}
|
|
83
|
-
return
|
|
124
|
+
return parsed;
|
|
84
125
|
}
|
|
85
126
|
/** Signs and encrypts account credentials directly to the temporary destination identity. */
|
|
86
127
|
function createPairingCredentialEnvelope(sourceDevice, destinationDevice, credentials, transcriptHash, expiresAt) {
|