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
@@ -7,6 +7,23 @@ declare const ALGORITHMS: {
7
7
  export declare class UntrustedPartyError extends Error {
8
8
  constructor(alias: string);
9
9
  }
10
+ /** Thrown by decryptFromCore for a structurally-malformed EncryptedBox or a failed AEAD decryption — both mean "this box is unusable", so one error class covers both causes. */
11
+ export declare class DecryptionError extends Error {
12
+ constructor(message: string);
13
+ }
14
+ /**
15
+ * True only for the exact canonical format `new Date().toISOString()` itself
16
+ * produces — the one format this codebase ever actually writes. A round-trip
17
+ * check (not a regex, not bare `Date.parse`) catches wrong formats, non-ISO
18
+ * strings `Date.parse` would otherwise accept (implementation-defined beyond
19
+ * strict ISO-8601), AND calendar-overflow values some engines silently roll
20
+ * forward (e.g. a Feb 30th) — all in one comparison: any mismatch, whatever
21
+ * the cause, fails the round-trip (ADR 0010). Exported so other modules that
22
+ * define their own timestamp fields (e.g. trust.ts's rotation/revocation
23
+ * documents) can hold them to the same strict standard instead of each
24
+ * re-deriving their own looser check.
25
+ */
26
+ export declare function isValidIsoInstant(value: string): boolean;
10
27
  export interface Certificate {
11
28
  documentHash: string;
12
29
  classicalSignature: string;
@@ -18,6 +35,14 @@ export interface Certificate {
18
35
  pqcSignPublicKey: string;
19
36
  };
20
37
  signedAt: string;
38
+ /**
39
+ * Certificate format. 2 (what sign() writes) means the signatures also cover
40
+ * `signer.alias` and `signedAt`; absent means a legacy certificate whose
41
+ * signatures cover only the document hash and expiry, so those two fields
42
+ * are unauthenticated there. A v2 certificate stripped of this field does
43
+ * not become a valid legacy one: its signatures are over the v2 message.
44
+ */
45
+ version?: 2;
21
46
  /** Absent = permanent proof, valid indefinitely (the default, unchanged behavior). */
22
47
  expiresAt?: string;
23
48
  }
@@ -25,7 +50,7 @@ export interface SignOptions {
25
50
  /** If set, verify() rejects this certificate as "expired" once this ISO timestamp has passed. */
26
51
  expiresAt?: string;
27
52
  }
28
- export type VerifyFailureReason = "untrusted-signer" | "revoked-signer" | "hash-mismatch" | "classical-signature-mismatch" | "pqc-signature-mismatch" | "expired";
53
+ export type VerifyFailureReason = "untrusted-signer" | "revoked-signer" | "hash-mismatch" | "classical-signature-mismatch" | "pqc-signature-mismatch" | "expired" | "malformed-certificate" | "fingerprint-mismatch";
29
54
  export type VerifyResult = {
30
55
  valid: true;
31
56
  } | {
@@ -68,24 +93,57 @@ export declare function signCore(identity: Identity, document: unknown, options?
68
93
  */
69
94
  export declare function verifyCore(identity: Identity, document: unknown, certificate: Certificate, deps: VerifyDeps): Promise<VerifyResult>;
70
95
  export interface EncryptedBox {
96
+ /** Always 2. Version 1 boxes (1.x, unsigned, no context binding) are rejected. */
97
+ version: 2;
98
+ /** 16 random bytes, hex. Unique per box; use it to detect replays. */
99
+ id: string;
100
+ /** When the sender sealed the box (ISO-8601, covered by the signature). */
101
+ signedAt: string;
71
102
  kemCipherText: string;
72
103
  nonce: string;
73
104
  ciphertext: string;
105
+ /** The sender's dual (ECDSA-P384 + ML-DSA-87) signature over everything above and both party fingerprints. */
106
+ signature: {
107
+ classicalSignature: string;
108
+ pqcSignature: string;
109
+ };
110
+ }
111
+ /** Thrown by decryptFromCore when the caller's `isReplay` hook reports the (otherwise valid) box was already seen. */
112
+ export declare class ReplayedBoxError extends Error {
113
+ constructor(id: string);
114
+ }
115
+ /** What the replay hook is told about a box that has already passed signature verification. */
116
+ export interface BoxReplayInfo {
117
+ id: string;
118
+ signedAt: string;
119
+ senderFingerprint: string;
120
+ }
121
+ /** decryptFromCore's dependencies: trust, log, and an optional replay check (return true to reject the box). */
122
+ export interface DecryptDeps extends TrustDeps {
123
+ isReplay?: (info: BoxReplayInfo) => boolean | Promise<boolean>;
74
124
  }
75
125
  /**
76
- * Encrypts `data` for `counterpart` using a symmetric key derived from BOTH
77
- * an ECDH-P384 exchange and an ML-KEM-1024 encapsulation (HKDF-SHA384
78
- * combines the two secrets), then AES-256-GCM. Throws UntrustedPartyError
79
- * unless `deps.checkTrust(counterpart)` reports "trusted".
126
+ * Encrypts `data` for `counterpart` (box v2, ADR 0018). The key comes from
127
+ * ECDH-P384 and an ML-KEM-1024 encapsulation, combined by HKDF-SHA384 with a
128
+ * salt that binds both parties and every public value in the box. AES-256-GCM
129
+ * authenticates the same context as AAD, and the sender dual-signs the box so
130
+ * it cannot be forged by the recipient or anyone else. Throws
131
+ * UntrustedPartyError unless `deps.checkTrust(counterpart)` reports "trusted".
132
+ *
133
+ * There is still NO forward secrecy: the ECDH half uses long-term keys. Use a
134
+ * session handshake for anything that needs it.
80
135
  */
81
136
  export declare function encryptForCore(identity: Identity, counterpart: PublicCard, data: unknown, deps: TrustDeps): Promise<EncryptedBox>;
82
137
  /**
83
138
  * Decrypts a box produced by `encryptFor`. `counterpart` is the sender's
84
139
  * public card. Throws UntrustedPartyError unless `deps.checkTrust(counterpart)`
85
- * reports "trusted" — this stops a caller from accidentally decrypting "as
86
- * if" a message came from someone they never actually trusted.
140
+ * reports "trusted". The sender's signature and the recipient binding are
141
+ * verified BEFORE any decapsulation or decryption; a v1 box, a box sealed for
142
+ * someone else, or a box not signed by `counterpart` is a DecryptionError. If
143
+ * `deps.isReplay` is given it is consulted only after the signature checks
144
+ * out, and a true result throws ReplayedBoxError.
87
145
  */
88
- export declare function decryptFromCore<T = unknown>(identity: Identity, counterpart: PublicCard, box: EncryptedBox, deps: TrustDeps): Promise<T>;
146
+ export declare function decryptFromCore<T = unknown>(identity: Identity, counterpart: PublicCard, box: EncryptedBox, deps: DecryptDeps): Promise<T>;
89
147
  export interface SessionKeys {
90
148
  /** AES-256-GCM key for frames sent by the initiator, decrypted by the responder. */
91
149
  initiatorToResponder: Uint8Array;
@@ -154,6 +212,35 @@ export interface ResponderSessionResult {
154
212
  export declare class HandshakeVerificationError extends Error {
155
213
  constructor(message: string);
156
214
  }
215
+ /** Thrown for a structurally-malformed handshake message (non-hex fields) — distinct from HandshakeVerificationError, which means the shape was fine but the signature didn't check out. */
216
+ export declare class MalformedHandshakeMessageError extends Error {
217
+ constructor(message: string);
218
+ }
219
+ /** The four handshake artifacts a confirmation tag is computed over — exactly what both sides hold once the handshake completes. */
220
+ export interface HandshakeTranscript {
221
+ initiatorCard: PublicCard;
222
+ responderCard: PublicCard;
223
+ responderMessage: ResponderHandshakeMessage;
224
+ initiatorMessage: InitiatorHandshakeMessage;
225
+ }
226
+ export type HandshakeRole = "initiator" | "responder";
227
+ /**
228
+ * The tag to send the counterpart as proof you derived the same secrets:
229
+ * HMAC-SHA384, keyed by the handshake's `confirmationKey`, over the full
230
+ * transcript and YOUR role (hex, 96 characters). Folding the role in means
231
+ * your own tag can never be replayed back to you as the counterpart's.
232
+ * Send it only after the handshake call returned, and treat the connection
233
+ * as unauthenticated until `verifyKeyConfirmation` succeeds on the tag you
234
+ * receive.
235
+ */
236
+ export declare function makeKeyConfirmation(confirmationKey: Uint8Array, role: HandshakeRole, transcript: HandshakeTranscript): string;
237
+ /**
238
+ * Checks the tag received from the counterpart. Pass the COUNTERPART's role
239
+ * (an initiator verifies "responder"). Returns false — never throws — for a
240
+ * wrong tag, a malformed one (any non-string, odd length, non-hex, wrong
241
+ * length, uppercase) or an unusable key/role, and compares in constant time.
242
+ */
243
+ export declare function verifyKeyConfirmation(confirmationKey: Uint8Array, counterpartRole: HandshakeRole, transcript: HandshakeTranscript, tag: unknown): boolean;
157
244
  /**
158
245
  * First step of the handshake, run by the connection's responder: generates
159
246
  * one fresh, single-use ECDH-P384 keypair and one fresh, single-use