raz-encrypt 2.3.0 → 2.4.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.
Files changed (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +488 -448
  3. package/dist/canonicalJson.d.ts +15 -0
  4. package/dist/canonicalJson.js +73 -13
  5. package/dist/canonicalJson.js.map +1 -1
  6. package/dist/core.d.ts +4 -2
  7. package/dist/core.js +3 -1
  8. package/dist/core.js.map +1 -1
  9. package/dist/crypto/classical.d.ts +2 -0
  10. package/dist/crypto/classical.js +4 -0
  11. package/dist/crypto/classical.js.map +1 -1
  12. package/dist/crypto/confirmedSession.d.ts +47 -0
  13. package/dist/crypto/confirmedSession.js +84 -0
  14. package/dist/crypto/confirmedSession.js.map +1 -0
  15. package/dist/crypto/frameCipher.js +8 -2
  16. package/dist/crypto/frameCipher.js.map +1 -1
  17. package/dist/crypto/frameSession.d.ts +40 -0
  18. package/dist/crypto/frameSession.js +96 -0
  19. package/dist/crypto/frameSession.js.map +1 -0
  20. package/dist/crypto/hybrid.d.ts +95 -8
  21. package/dist/crypto/hybrid.js +374 -49
  22. package/dist/crypto/hybrid.js.map +1 -1
  23. package/dist/crypto/nodeHybrid.d.ts +6 -4
  24. package/dist/crypto/nodeHybrid.js +3 -2
  25. package/dist/crypto/nodeHybrid.js.map +1 -1
  26. package/dist/crypto/pqc.d.ts +7 -2
  27. package/dist/crypto/pqc.js +13 -4
  28. package/dist/crypto/pqc.js.map +1 -1
  29. package/dist/crypto/ratchet.js +5 -0
  30. package/dist/crypto/ratchet.js.map +1 -1
  31. package/dist/fileCrypto.d.ts +44 -2
  32. package/dist/fileCrypto.js +198 -28
  33. package/dist/fileCrypto.js.map +1 -1
  34. package/dist/identity.d.ts +69 -4
  35. package/dist/identity.js +148 -13
  36. package/dist/identity.js.map +1 -1
  37. package/dist/index.d.ts +8 -7
  38. package/dist/index.js +4 -4
  39. package/dist/index.js.map +1 -1
  40. package/dist/passphrase.d.ts +27 -0
  41. package/dist/passphrase.js +69 -0
  42. package/dist/passphrase.js.map +1 -0
  43. package/dist/store.d.ts +43 -6
  44. package/dist/store.js +296 -23
  45. package/dist/store.js.map +1 -1
  46. package/dist/trust.d.ts +170 -2
  47. package/dist/trust.js +527 -33
  48. package/dist/trust.js.map +1 -1
  49. package/package.json +68 -69
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Canonical JSON per RFC 8785 (JCS): no whitespace, object keys sorted by
3
+ * UTF-16 code unit, ECMAScript number and string serialization — so the same
4
+ * logical document always hashes to the same value regardless of key order
5
+ * on the wire.
6
+ *
7
+ * Unlike a bare `JSON.stringify`, this rejects (with a `TypeError` naming the
8
+ * offending path) every value that JSON cannot represent faithfully, rather
9
+ * than silently collapsing it into something else. Two different in-memory
10
+ * documents must never canonicalize to the same bytes (ADR 0012):
11
+ * `undefined`, `NaN`, `±Infinity`, `BigInt`, functions, symbols, sparse
12
+ * arrays, circular references, lone surrogates, and anything that is not a
13
+ * plain object / array / string / finite number / boolean / null (`Map`,
14
+ * `Set`, `Date`, typed arrays, class instances, ...).
15
+ */
1
16
  export declare function canonicalize(value: unknown): string;
2
17
  /** The document hash every signature is made over. */
3
18
  export declare function hashDocument(value: unknown): string;
@@ -1,22 +1,82 @@
1
1
  import { sha512 } from "@noble/hashes/sha2.js";
2
2
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
3
3
  /**
4
- * Deterministic JSON serialization (object keys sorted recursively) so the
5
- * same logical document always hashes to the same value regardless of key
6
- * order on the wire.
4
+ * Canonical JSON per RFC 8785 (JCS): no whitespace, object keys sorted by
5
+ * UTF-16 code unit, ECMAScript number and string serialization — so the same
6
+ * logical document always hashes to the same value regardless of key order
7
+ * on the wire.
8
+ *
9
+ * Unlike a bare `JSON.stringify`, this rejects (with a `TypeError` naming the
10
+ * offending path) every value that JSON cannot represent faithfully, rather
11
+ * than silently collapsing it into something else. Two different in-memory
12
+ * documents must never canonicalize to the same bytes (ADR 0012):
13
+ * `undefined`, `NaN`, `±Infinity`, `BigInt`, functions, symbols, sparse
14
+ * arrays, circular references, lone surrogates, and anything that is not a
15
+ * plain object / array / string / finite number / boolean / null (`Map`,
16
+ * `Set`, `Date`, typed arrays, class instances, ...).
7
17
  */
8
- function sortKeysDeep(value) {
9
- if (Array.isArray(value))
10
- return value.map(sortKeysDeep);
11
- if (value !== null && typeof value === "object") {
12
- return Object.fromEntries(Object.keys(value)
13
- .sort()
14
- .map((key) => [key, sortKeysDeep(value[key])]));
18
+ export function canonicalize(value) {
19
+ return serialize(value, "$", new Set());
20
+ }
21
+ function fail(path, what) {
22
+ throw new TypeError(`canonicalize: ${what} at ${path} cannot be represented as canonical JSON`);
23
+ }
24
+ function serialize(value, path, ancestors) {
25
+ switch (typeof value) {
26
+ case "string":
27
+ return serializeString(value, path);
28
+ case "number":
29
+ if (!Number.isFinite(value))
30
+ fail(path, `non-finite number (${String(value)})`);
31
+ // ECMAScript Number::toString is exactly what RFC 8785 §3.2.2.3 mandates
32
+ // (including "-0" -> "0").
33
+ return JSON.stringify(value);
34
+ case "boolean":
35
+ return value ? "true" : "false";
36
+ case "object":
37
+ break;
38
+ default:
39
+ return fail(path, typeof value === "undefined" ? "undefined" : `${typeof value}`);
40
+ }
41
+ if (value === null)
42
+ return "null";
43
+ const obj = value;
44
+ if (ancestors.has(obj))
45
+ fail(path, "circular reference");
46
+ ancestors.add(obj);
47
+ try {
48
+ if (Array.isArray(obj)) {
49
+ const parts = [];
50
+ for (let i = 0; i < obj.length; i++) {
51
+ if (!(i in obj))
52
+ fail(`${path}[${i}]`, "hole in sparse array");
53
+ parts.push(serialize(obj[i], `${path}[${i}]`, ancestors));
54
+ }
55
+ return `[${parts.join(",")}]`;
56
+ }
57
+ const proto = Object.getPrototypeOf(obj);
58
+ if (proto !== Object.prototype && proto !== null) {
59
+ fail(path, `non-plain object (${proto.constructor?.name ?? "unknown"})`);
60
+ }
61
+ const record = obj;
62
+ // Default sort() compares UTF-16 code units, which is the RFC 8785 order.
63
+ const keys = Object.keys(record).sort();
64
+ const parts = keys.map((key) => `${serializeString(key, `${path}.${key}`)}:${serialize(record[key], `${path}.${key}`, ancestors)}`);
65
+ return `{${parts.join(",")}}`;
66
+ }
67
+ finally {
68
+ ancestors.delete(obj);
15
69
  }
16
- return value;
17
70
  }
18
- export function canonicalize(value) {
19
- return JSON.stringify(sortKeysDeep(value));
71
+ // High surrogate not followed by a low one, or a low surrogate not preceded by a high one.
72
+ const LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
73
+ function serializeString(value, path) {
74
+ // RFC 8785 requires I-JSON: strings must be well-formed UTF-16. A lone
75
+ // surrogate has no UTF-8 encoding, so two distinct strings would otherwise
76
+ // both turn into U+FFFD when hashed.
77
+ if (LONE_SURROGATE.test(value))
78
+ fail(path, "string with a lone surrogate");
79
+ return JSON.stringify(value);
20
80
  }
21
81
  /** The document hash every signature is made over. */
22
82
  export function hashDocument(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"canonicalJson.js","sourceRoot":"","sources":["../src/canonicalJson.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;GAIG;AACH,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC;aAC1C,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,YAAY,CAAE,KAAiC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAC9E,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"canonicalJson.js","sourceRoot":"","sources":["../src/canonicalJson.ts"],"names":[],"mappings":"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"}
package/dist/core.d.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  * included, fs-backed versions of the same logic — this module is the
11
11
  * un-wired core they're built from, not a separate implementation.
12
12
  */
13
- export { createResponderHandshakeCore as createResponderHandshake, decryptFromCore as decryptFrom, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, HandshakeVerificationError, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
14
- export type { Certificate, EncryptedBox, HandshakeSignature, InitiatorHandshakeMessage, InitiatorSessionResult, LogDeps, ResponderHandshakeInit, ResponderHandshakeMessage, ResponderHandshakeSecrets, ResponderSessionResult, SessionKeys, SignOptions, TrustDeps, TrustStatus, VerifyDeps, VerifyFailureReason, VerifyResult, } from "./crypto/hybrid.js";
13
+ export { createResponderHandshakeCore as createResponderHandshake, decryptFromCore as decryptFrom, DecryptionError, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, HandshakeVerificationError, makeKeyConfirmation, verifyKeyConfirmation, MalformedHandshakeMessageError, ReplayedBoxError, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
14
+ export type { BoxReplayInfo, Certificate, DecryptDeps, EncryptedBox, HandshakeRole, HandshakeSignature, HandshakeTranscript, InitiatorHandshakeMessage, InitiatorSessionResult, LogDeps, ResponderHandshakeInit, ResponderHandshakeMessage, ResponderHandshakeSecrets, ResponderSessionResult, SessionKeys, SignOptions, TrustDeps, TrustStatus, VerifyDeps, VerifyFailureReason, VerifyResult, } from "./crypto/hybrid.js";
15
15
  export { generateClassicalKeypair, classicalSharedSecret, classicalSign, classicalVerify } from "./crypto/classical.js";
16
16
  export type { ClassicalKeypair } from "./crypto/classical.js";
17
17
  export { generatePqcKemKeypair, generatePqcSignKeypair, pqcDecapsulate, pqcEncapsulate, pqcSign, pqcVerify, } from "./crypto/pqc.js";
@@ -20,6 +20,8 @@ export { canonicalize, hashDocument } from "./canonicalJson.js";
20
20
  export { fingerprint } from "./fingerprint.js";
21
21
  export { decryptFrame, encryptFrame, FrameDecryptionError } from "./crypto/frameCipher.js";
22
22
  export type { EncryptedFrame } from "./crypto/frameCipher.js";
23
+ export { createFrameChannels, FrameCounterError, FrameReceiver, FrameSender } from "./crypto/frameSession.js";
24
+ export { ConfirmedSession, UnconfirmedSessionError } from "./crypto/confirmedSession.js";
23
25
  export { CHAIN_INFO_INITIATOR_TO_RESPONDER, CHAIN_INFO_RESPONDER_TO_INITIATOR, ML_KEM_STEP_CADENCE, performEcdhStep, performKemStep, ratchetChainKey, seedChainKey, } from "./crypto/ratchet.js";
24
26
  export type { AsymmetricStepResult, ChainStep } from "./crypto/ratchet.js";
25
27
  export type { Identity, PublicCard } from "./identity.js";
package/dist/core.js CHANGED
@@ -10,11 +10,13 @@
10
10
  * included, fs-backed versions of the same logic — this module is the
11
11
  * un-wired core they're built from, not a separate implementation.
12
12
  */
13
- export { createResponderHandshakeCore as createResponderHandshake, decryptFromCore as decryptFrom, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, HandshakeVerificationError, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
13
+ export { createResponderHandshakeCore as createResponderHandshake, decryptFromCore as decryptFrom, DecryptionError, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, HandshakeVerificationError, makeKeyConfirmation, verifyKeyConfirmation, MalformedHandshakeMessageError, ReplayedBoxError, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
14
14
  export { generateClassicalKeypair, classicalSharedSecret, classicalSign, classicalVerify } from "./crypto/classical.js";
15
15
  export { generatePqcKemKeypair, generatePqcSignKeypair, pqcDecapsulate, pqcEncapsulate, pqcSign, pqcVerify, } from "./crypto/pqc.js";
16
16
  export { canonicalize, hashDocument } from "./canonicalJson.js";
17
17
  export { fingerprint } from "./fingerprint.js";
18
18
  export { decryptFrame, encryptFrame, FrameDecryptionError } from "./crypto/frameCipher.js";
19
+ export { createFrameChannels, FrameCounterError, FrameReceiver, FrameSender } from "./crypto/frameSession.js";
20
+ export { ConfirmedSession, UnconfirmedSessionError } from "./crypto/confirmedSession.js";
19
21
  export { CHAIN_INFO_INITIATOR_TO_RESPONDER, CHAIN_INFO_RESPONDER_TO_INITIATOR, ML_KEM_STEP_CADENCE, performEcdhStep, performKemStep, ratchetChainKey, seedChainKey, } from "./crypto/ratchet.js";
20
22
  //# sourceMappingURL=core.js.map
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,4BAA4B,IAAI,wBAAwB,EACxD,eAAe,IAAI,WAAW,EAC9B,cAAc,IAAI,UAAU,EAC5B,6BAA6B,IAAI,yBAAyB,EAC1D,6BAA6B,IAAI,yBAAyB,EAC1D,0BAA0B,EAC1B,QAAQ,IAAI,IAAI,EAChB,mBAAmB,EACnB,UAAU,IAAI,MAAM,GACrB,MAAM,oBAAoB,CAAC;AAqB5B,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxH,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,OAAO,EACP,SAAS,GACV,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAG3F,OAAO,EACL,iCAAiC,EACjC,iCAAiC,EACjC,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,eAAe,EACf,YAAY,GACb,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,4BAA4B,IAAI,wBAAwB,EACxD,eAAe,IAAI,WAAW,EAC9B,eAAe,EACf,cAAc,IAAI,UAAU,EAC5B,6BAA6B,IAAI,yBAAyB,EAC1D,6BAA6B,IAAI,yBAAyB,EAC1D,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,8BAA8B,EAC9B,gBAAgB,EAChB,QAAQ,IAAI,IAAI,EAChB,mBAAmB,EACnB,UAAU,IAAI,MAAM,GACrB,MAAM,oBAAoB,CAAC;AAyB5B,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxH,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,OAAO,EACP,SAAS,GACV,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE3F,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC9G,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAEzF,OAAO,EACL,iCAAiC,EACjC,iCAAiC,EACjC,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,eAAe,EACf,YAAY,GACb,MAAM,qBAAqB,CAAC"}
@@ -8,3 +8,5 @@ export declare function classicalSign(message: Uint8Array, secretKey: Uint8Array
8
8
  export declare function classicalVerify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean;
9
9
  /** ECDH shared secret between our secret key and their public key. */
10
10
  export declare function classicalSharedSecret(secretKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array;
11
+ /** True iff `keypair`'s two fields are the exact byte lengths p384's own keygen produces — a cheap sanity check for key material sourced from somewhere this library doesn't control (e.g. a caller-supplied KeyStore, issue #38). */
12
+ export declare function isValidClassicalKeypairShape(keypair: ClassicalKeypair): boolean;
@@ -18,4 +18,8 @@ export function classicalVerify(signature, message, publicKey) {
18
18
  export function classicalSharedSecret(secretKey, theirPublicKey) {
19
19
  return p384.getSharedSecret(secretKey, theirPublicKey);
20
20
  }
21
+ /** True iff `keypair`'s two fields are the exact byte lengths p384's own keygen produces — a cheap sanity check for key material sourced from somewhere this library doesn't control (e.g. a caller-supplied KeyStore, issue #38). */
22
+ export function isValidClassicalKeypairShape(keypair) {
23
+ return keypair.secretKey.length === p384.lengths.secretKey && keypair.publicKey.length === p384.lengths.publicKey;
24
+ }
21
25
  //# sourceMappingURL=classical.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"classical.js","sourceRoot":"","sources":["../../src/crypto/classical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAO7C,yEAAyE;AACzE,MAAM,UAAU,wBAAwB;IACtC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAmB,EAAE,SAAqB;IACtE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAqB,EAAE,OAAmB,EAAE,SAAqB;IAC/F,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,qBAAqB,CAAC,SAAqB,EAAE,cAA0B;IACrF,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;AACzD,CAAC"}
1
+ {"version":3,"file":"classical.js","sourceRoot":"","sources":["../../src/crypto/classical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAO7C,yEAAyE;AACzE,MAAM,UAAU,wBAAwB;IACtC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAmB,EAAE,SAAqB;IACtE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,SAAqB,EAAE,OAAmB,EAAE,SAAqB;IAC/F,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,qBAAqB,CAAC,SAAqB,EAAE,cAA0B;IACrF,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;AACzD,CAAC;AAED,sOAAsO;AACtO,MAAM,UAAU,4BAA4B,CAAC,OAAyB;IACpE,OAAO,OAAO,CAAC,SAAS,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;AACpH,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { EncryptedFrame } from "./frameCipher.js";
2
+ import { type HandshakeRole, type HandshakeTranscript, type InitiatorSessionResult, type ResponderSessionResult } from "./hybrid.js";
3
+ /**
4
+ * ConfirmedSession (issue #40/ADR 0025): a high-level gate in front of
5
+ * FrameSender/FrameReceiver that makes it hard to skip key confirmation by
6
+ * accident. `createFrameChannels` already builds a ready-to-use
7
+ * sender/receiver pair straight from SessionKeys with no confirmation gate
8
+ * at all — a caller could start sending and receiving the instant the
9
+ * handshake returns, with nothing to signal that confirmation (issue #8's
10
+ * makeKeyConfirmation/verifyKeyConfirmation) was skipped entirely.
11
+ *
12
+ * This class adds exactly one thing, no new crypto: a gate. It performs NO
13
+ * network I/O — the caller still transports the handshake messages and the
14
+ * confirmation tags over whatever channel they already use, exactly as for
15
+ * the low-level functions, which are completely unchanged and remain the
16
+ * supported API for advanced use.
17
+ */
18
+ export declare class UnconfirmedSessionError extends Error {
19
+ constructor(operation: "seal" | "open");
20
+ }
21
+ export declare class ConfirmedSession {
22
+ private readonly role;
23
+ private readonly counterpartRole;
24
+ private readonly confirmationKey;
25
+ private readonly transcript;
26
+ private readonly sender;
27
+ private readonly receiver;
28
+ private confirmed;
29
+ /** The tag to send the counterpart — computed eagerly, send it as soon as you have a channel to send it over. */
30
+ readonly ownConfirmationTag: string;
31
+ constructor(role: HandshakeRole, result: InitiatorSessionResult | ResponderSessionResult, transcript: HandshakeTranscript);
32
+ /** Whether `confirmPeer` has already succeeded. `seal`/`open` are only usable once this is true. */
33
+ get isConfirmed(): boolean;
34
+ /**
35
+ * Verifies the tag received from the counterpart. Returns `false` for a
36
+ * wrong or malformed tag — never throws, matching `verifyKeyConfirmation`'s
37
+ * own contract — so you decide what to do (retry, abort, log). Once this
38
+ * returns `true`, the gate opens permanently; nothing closes it again.
39
+ */
40
+ confirmPeer(peerTag: unknown): boolean;
41
+ /** Encrypts `plaintext` at the next counter. Throws UnconfirmedSessionError until `confirmPeer` has succeeded. */
42
+ seal(plaintext: Uint8Array, aad?: Uint8Array): EncryptedFrame;
43
+ /** Opens a received frame. Throws UnconfirmedSessionError until `confirmPeer` has succeeded — reading content from an unconfirmed counterpart is exactly the mistake key confirmation exists to prevent. */
44
+ open(frame: EncryptedFrame, aad?: Uint8Array): Uint8Array;
45
+ /** Wipes the wrapped sender/receiver's key copies and this object's own copy of the confirmation key. Unusable afterwards. */
46
+ destroy(): void;
47
+ }
@@ -0,0 +1,84 @@
1
+ import { createFrameChannels } from "./frameSession.js";
2
+ import { makeKeyConfirmation, verifyKeyConfirmation, } from "./hybrid.js";
3
+ /**
4
+ * ConfirmedSession (issue #40/ADR 0025): a high-level gate in front of
5
+ * FrameSender/FrameReceiver that makes it hard to skip key confirmation by
6
+ * accident. `createFrameChannels` already builds a ready-to-use
7
+ * sender/receiver pair straight from SessionKeys with no confirmation gate
8
+ * at all — a caller could start sending and receiving the instant the
9
+ * handshake returns, with nothing to signal that confirmation (issue #8's
10
+ * makeKeyConfirmation/verifyKeyConfirmation) was skipped entirely.
11
+ *
12
+ * This class adds exactly one thing, no new crypto: a gate. It performs NO
13
+ * network I/O — the caller still transports the handshake messages and the
14
+ * confirmation tags over whatever channel they already use, exactly as for
15
+ * the low-level functions, which are completely unchanged and remain the
16
+ * supported API for advanced use.
17
+ */
18
+ export class UnconfirmedSessionError extends Error {
19
+ constructor(operation) {
20
+ super(`Cannot ${operation}: key confirmation has not succeeded yet. Call confirmPeer(peerTag) with the tag received from the counterpart first.`);
21
+ this.name = "UnconfirmedSessionError";
22
+ }
23
+ }
24
+ export class ConfirmedSession {
25
+ role;
26
+ counterpartRole;
27
+ confirmationKey;
28
+ transcript;
29
+ sender;
30
+ receiver;
31
+ confirmed = false;
32
+ /** The tag to send the counterpart — computed eagerly, send it as soon as you have a channel to send it over. */
33
+ ownConfirmationTag;
34
+ constructor(role, result, transcript) {
35
+ this.role = role;
36
+ this.counterpartRole = role === "initiator" ? "responder" : "initiator";
37
+ // A private copy, same reason FrameSender/FrameReceiver copy their keys
38
+ // (./frameSession.js): destroy() must never reach back and mutate the
39
+ // caller's own handshake-result object.
40
+ this.confirmationKey = result.confirmationKey.slice();
41
+ this.transcript = transcript;
42
+ this.ownConfirmationTag = makeKeyConfirmation(result.confirmationKey, role, transcript);
43
+ const { sender, receiver } = createFrameChannels(result.keys, role);
44
+ this.sender = sender;
45
+ this.receiver = receiver;
46
+ }
47
+ /** Whether `confirmPeer` has already succeeded. `seal`/`open` are only usable once this is true. */
48
+ get isConfirmed() {
49
+ return this.confirmed;
50
+ }
51
+ /**
52
+ * Verifies the tag received from the counterpart. Returns `false` for a
53
+ * wrong or malformed tag — never throws, matching `verifyKeyConfirmation`'s
54
+ * own contract — so you decide what to do (retry, abort, log). Once this
55
+ * returns `true`, the gate opens permanently; nothing closes it again.
56
+ */
57
+ confirmPeer(peerTag) {
58
+ if (this.confirmed)
59
+ return true;
60
+ if (verifyKeyConfirmation(this.confirmationKey, this.counterpartRole, this.transcript, peerTag)) {
61
+ this.confirmed = true;
62
+ }
63
+ return this.confirmed;
64
+ }
65
+ /** Encrypts `plaintext` at the next counter. Throws UnconfirmedSessionError until `confirmPeer` has succeeded. */
66
+ seal(plaintext, aad) {
67
+ if (!this.confirmed)
68
+ throw new UnconfirmedSessionError("seal");
69
+ return this.sender.seal(plaintext, aad);
70
+ }
71
+ /** Opens a received frame. Throws UnconfirmedSessionError until `confirmPeer` has succeeded — reading content from an unconfirmed counterpart is exactly the mistake key confirmation exists to prevent. */
72
+ open(frame, aad) {
73
+ if (!this.confirmed)
74
+ throw new UnconfirmedSessionError("open");
75
+ return this.receiver.open(frame, aad);
76
+ }
77
+ /** Wipes the wrapped sender/receiver's key copies and this object's own copy of the confirmation key. Unusable afterwards. */
78
+ destroy() {
79
+ this.sender.destroy();
80
+ this.receiver.destroy();
81
+ this.confirmationKey.fill(0);
82
+ }
83
+ }
84
+ //# sourceMappingURL=confirmedSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirmedSession.js","sourceRoot":"","sources":["../../src/crypto/confirmedSession.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAA8B,MAAM,mBAAmB,CAAC;AAEpF,OAAO,EACL,mBAAmB,EACnB,qBAAqB,GAKtB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,SAA0B;QACpC,KAAK,CAAC,UAAU,SAAS,uHAAuH,CAAC,CAAC;QAClJ,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,gBAAgB;IACV,IAAI,CAAgB;IACpB,eAAe,CAAgB;IAC/B,eAAe,CAAa;IAC5B,UAAU,CAAsB;IAChC,MAAM,CAAc;IACpB,QAAQ,CAAgB;IACjC,SAAS,GAAG,KAAK,CAAC;IAE1B,iHAAiH;IACxG,kBAAkB,CAAS;IAEpC,YAAY,IAAmB,EAAE,MAAuD,EAAE,UAA+B;QACvH,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,eAAe,GAAG,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC;QACxE,wEAAwE;QACxE,sEAAsE;QACtE,wCAAwC;QACxC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QACtD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,kBAAkB,GAAG,mBAAmB,CAAC,MAAM,CAAC,eAAe,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;QACxF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,mBAAmB,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACpE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,oGAAoG;IACpG,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAgB;QAC1B,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,qBAAqB,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC;YAChG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,kHAAkH;IAClH,IAAI,CAAC,SAAqB,EAAE,GAAgB;QAC1C,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC1C,CAAC;IAED,4MAA4M;IAC5M,IAAI,CAAC,KAAqB,EAAE,GAAgB;QAC1C,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAC/D,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,8HAA8H;IAC9H,OAAO;QACL,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -1,5 +1,9 @@
1
1
  import { gcm } from "@noble/ciphers/aes.js";
2
2
  import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
3
+ // Prefer FrameSender/FrameReceiver (./frameSession.ts), which own the counter so
4
+ // it cannot be reused by mistake; use these raw functions only if you must manage
5
+ // counters yourself.
6
+ //
3
7
  // The per-frame AEAD used to wrap every message sent under a session key
4
8
  // established by establishSessionInitiatorCore/establishSessionResponderCore
5
9
  // (see hybrid.ts) — one directional key, reused across many frames, so the
@@ -20,8 +24,10 @@ export class FrameDecryptionError extends Error {
20
24
  }
21
25
  /** Encodes `counter` as a 96-bit nonce: 4 zero bytes + an 8-byte big-endian counter (2^64 frames is far beyond any realistic session). */
22
26
  function counterToNonce(counter) {
23
- if (!Number.isInteger(counter) || counter < 0) {
24
- throw new Error(`Frame counter must be a non-negative integer, got ${counter}.`);
27
+ // isSafeInteger, not isInteger: above 2^53 distinct counters can round to
28
+ // the same double, which would silently reuse a nonce.
29
+ if (!Number.isSafeInteger(counter) || counter < 0) {
30
+ throw new Error(`Frame counter must be a non-negative safe integer (0 to 2^53-1), got ${counter}.`);
25
31
  }
26
32
  const nonce = new Uint8Array(NONCE_LENGTH);
27
33
  let value = BigInt(counter);
@@ -1 +1 @@
1
- {"version":3,"file":"frameCipher.js","sourceRoot":"","sources":["../../src/crypto/frameCipher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEhE,yEAAyE;AACzE,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,wEAAwE;AACxE,gBAAgB;AAEhB,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C;QACE,KAAK,CAAC,+FAA+F,CAAC,CAAC;QACvG,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AASD,0IAA0I;AAC1I,SAAS,cAAc,CAAC,OAAe;IACrC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,qDAAqD,OAAO,GAAG,CAAC,CAAC;IACnF,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC,IAAI,YAAY,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACtE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QACjC,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,OAAe,EAAE,SAAqB,EAAE,GAAgB;IACpG,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,KAAqB,EAAE,GAAgB;IACnF,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,oBAAoB,EAAE,CAAC;IACnC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"frameCipher.js","sourceRoot":"","sources":["../../src/crypto/frameCipher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEhE,iFAAiF;AACjF,kFAAkF;AAClF,qBAAqB;AACrB,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,2EAA2E;AAC3E,4EAA4E;AAC5E,yEAAyE;AACzE,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,wEAAwE;AACxE,gBAAgB;AAEhB,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,aAAa,GAAG,CAAC,CAAC;AAExB,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C;QACE,KAAK,CAAC,+FAA+F,CAAC,CAAC;QACvG,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AASD,0IAA0I;AAC1I,SAAS,cAAc,CAAC,OAAe;IACrC,0EAA0E;IAC1E,uDAAuD;IACvD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,wEAAwE,OAAO,GAAG,CAAC,CAAC;IACtG,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5B,KAAK,IAAI,CAAC,GAAG,YAAY,GAAG,CAAC,EAAE,CAAC,IAAI,YAAY,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACtE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QACjC,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,OAAe,EAAE,SAAqB,EAAE,GAAgB;IACpG,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC7E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,KAAqB,EAAE,GAAgB;IACnF,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5F,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,oBAAoB,EAAE,CAAC;IACnC,CAAC;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type EncryptedFrame } from "./frameCipher.js";
2
+ import type { SessionKeys } from "./hybrid.js";
3
+ /** Thrown for counter misuse: exhausted space, a replayed or out-of-order counter, a malformed counter, or use after destroy(). */
4
+ export declare class FrameCounterError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ /**
8
+ * Seals frames under one directional key, owning the counter. Use exactly one
9
+ * sender per key, and never create a second sender for a key that has already
10
+ * been used: a new sender starts again at counter 0.
11
+ */
12
+ export declare class FrameSender {
13
+ private key;
14
+ private next;
15
+ private exhausted;
16
+ constructor(key: Uint8Array);
17
+ /** Encrypts `plaintext` at the next counter. The counter is spent before encrypting, so a failure can never lead to reuse. `aad` has the same meaning as in encryptFrame. */
18
+ seal(plaintext: Uint8Array, aad?: Uint8Array): EncryptedFrame;
19
+ /** Wipes this sender's private copy of the key. The array you passed in is not touched. */
20
+ destroy(): void;
21
+ }
22
+ /**
23
+ * Opens frames under one directional key and rejects any counter that is not
24
+ * higher than the last frame it opened. Gaps are allowed (a dropped frame is
25
+ * not an attack); repeats and reordering are not. State only moves after a
26
+ * frame authenticates, so forged frames cannot advance it.
27
+ */
28
+ export declare class FrameReceiver {
29
+ private key;
30
+ private last;
31
+ constructor(key: Uint8Array);
32
+ open(frame: EncryptedFrame, aad?: Uint8Array): Uint8Array;
33
+ /** Wipes this receiver's private copy of the key. The array you passed in is not touched. */
34
+ destroy(): void;
35
+ }
36
+ /** Pairs the right directional keys for your role so a sender can never be given the receiving key by mistake. */
37
+ export declare function createFrameChannels(keys: SessionKeys, role: "initiator" | "responder"): {
38
+ sender: FrameSender;
39
+ receiver: FrameReceiver;
40
+ };
@@ -0,0 +1,96 @@
1
+ import { decryptFrame, encryptFrame } from "./frameCipher.js";
2
+ // Stateful wrappers around encryptFrame/decryptFrame (ADR 0020, issue #35).
3
+ //
4
+ // AES-GCM breaks badly if a (key, nonce) pair is ever used twice, and the
5
+ // low-level functions take the counter from the caller, so a stray retry, a
6
+ // restored snapshot or a second code path sharing a key can reuse one without
7
+ // anyone noticing. These classes own the counter instead: the sender can only
8
+ // move forward and never repeats, the receiver only accepts a counter higher
9
+ // than the last one it successfully opened.
10
+ const KEY_LENGTH = 32;
11
+ /** Thrown for counter misuse: exhausted space, a replayed or out-of-order counter, a malformed counter, or use after destroy(). */
12
+ export class FrameCounterError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "FrameCounterError";
16
+ }
17
+ }
18
+ function copyKey(key) {
19
+ if (!(key instanceof Uint8Array) || key.length !== KEY_LENGTH) {
20
+ throw new TypeError(`Frame key must be a ${KEY_LENGTH}-byte Uint8Array.`);
21
+ }
22
+ return key.slice();
23
+ }
24
+ /**
25
+ * Seals frames under one directional key, owning the counter. Use exactly one
26
+ * sender per key, and never create a second sender for a key that has already
27
+ * been used: a new sender starts again at counter 0.
28
+ */
29
+ export class FrameSender {
30
+ key;
31
+ next = 0;
32
+ exhausted = false;
33
+ constructor(key) {
34
+ this.key = copyKey(key);
35
+ }
36
+ /** Encrypts `plaintext` at the next counter. The counter is spent before encrypting, so a failure can never lead to reuse. `aad` has the same meaning as in encryptFrame. */
37
+ seal(plaintext, aad) {
38
+ if (!this.key)
39
+ throw new FrameCounterError("This FrameSender was destroyed.");
40
+ if (this.exhausted)
41
+ throw new FrameCounterError("Frame counter space is exhausted for this key; establish a new session.");
42
+ const counter = this.next;
43
+ if (counter === Number.MAX_SAFE_INTEGER)
44
+ this.exhausted = true;
45
+ else
46
+ this.next = counter + 1;
47
+ return encryptFrame(this.key, counter, plaintext, aad);
48
+ }
49
+ /** Wipes this sender's private copy of the key. The array you passed in is not touched. */
50
+ destroy() {
51
+ this.key?.fill(0);
52
+ this.key = null;
53
+ }
54
+ }
55
+ /**
56
+ * Opens frames under one directional key and rejects any counter that is not
57
+ * higher than the last frame it opened. Gaps are allowed (a dropped frame is
58
+ * not an attack); repeats and reordering are not. State only moves after a
59
+ * frame authenticates, so forged frames cannot advance it.
60
+ */
61
+ export class FrameReceiver {
62
+ key;
63
+ last = -1;
64
+ constructor(key) {
65
+ this.key = copyKey(key);
66
+ }
67
+ open(frame, aad) {
68
+ if (!this.key)
69
+ throw new FrameCounterError("This FrameReceiver was destroyed.");
70
+ const counter = frame?.counter;
71
+ if (typeof counter !== "number" || !Number.isSafeInteger(counter) || counter < 0) {
72
+ throw new FrameCounterError("Frame counter must be a non-negative safe integer.");
73
+ }
74
+ if (counter <= this.last)
75
+ throw new FrameCounterError(`Frame counter ${counter} was already used or is out of order (last accepted: ${this.last}).`);
76
+ const plaintext = decryptFrame(this.key, frame, aad);
77
+ this.last = counter;
78
+ return plaintext;
79
+ }
80
+ /** Wipes this receiver's private copy of the key. The array you passed in is not touched. */
81
+ destroy() {
82
+ this.key?.fill(0);
83
+ this.key = null;
84
+ }
85
+ }
86
+ /** Pairs the right directional keys for your role so a sender can never be given the receiving key by mistake. */
87
+ export function createFrameChannels(keys, role) {
88
+ if (role === "initiator") {
89
+ return { sender: new FrameSender(keys.initiatorToResponder), receiver: new FrameReceiver(keys.responderToInitiator) };
90
+ }
91
+ if (role === "responder") {
92
+ return { sender: new FrameSender(keys.responderToInitiator), receiver: new FrameReceiver(keys.initiatorToResponder) };
93
+ }
94
+ throw new TypeError(`Unknown role "${String(role)}": expected "initiator" or "responder".`);
95
+ }
96
+ //# sourceMappingURL=frameSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frameSession.js","sourceRoot":"","sources":["../../src/crypto/frameSession.ts"],"names":[],"mappings":"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"}