raz-encrypt 0.3.0 → 2.1.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 (50) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +442 -212
  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 +6 -2
  7. package/dist/core.js +4 -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 +186 -25
  21. package/dist/crypto/hybrid.js +512 -54
  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.d.ts +68 -0
  30. package/dist/crypto/ratchet.js +94 -0
  31. package/dist/crypto/ratchet.js.map +1 -0
  32. package/dist/fileCrypto.d.ts +44 -2
  33. package/dist/fileCrypto.js +197 -28
  34. package/dist/fileCrypto.js.map +1 -1
  35. package/dist/identity.d.ts +69 -4
  36. package/dist/identity.js +111 -12
  37. package/dist/identity.js.map +1 -1
  38. package/dist/index.d.ts +8 -7
  39. package/dist/index.js +4 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/passphrase.d.ts +27 -0
  42. package/dist/passphrase.js +69 -0
  43. package/dist/passphrase.js.map +1 -0
  44. package/dist/store.d.ts +42 -5
  45. package/dist/store.js +254 -22
  46. package/dist/store.js.map +1 -1
  47. package/dist/trust.d.ts +143 -2
  48. package/dist/trust.js +376 -27
  49. package/dist/trust.js.map +1 -1
  50. package/package.json +62 -48
@@ -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";
29
54
  export type VerifyResult = {
30
55
  valid: true;
31
56
  } | {
@@ -68,50 +93,186 @@ 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
- /** AES-256-GCM key for frames sent by the initiator (the side that encapsulated), decrypted by the responder. */
148
+ /** AES-256-GCM key for frames sent by the initiator, decrypted by the responder. */
91
149
  initiatorToResponder: Uint8Array;
92
- /** AES-256-GCM key for frames sent by the responder (the side that decapsulated), decrypted by the initiator. */
150
+ /** AES-256-GCM key for frames sent by the responder, decrypted by the initiator. */
93
151
  responderToInitiator: Uint8Array;
94
152
  }
95
- export interface InitiatorSessionResult {
96
- /** Send this to the responder — the only message this handshake requires; the ECDH half needs no message since both sides derive it locally from already-exchanged Public Cards. */
153
+ /** The dual classical+PQ signature over a handshake message's own signed ephemeral payload (see `handshakeSignedMessage`). */
154
+ export interface HandshakeSignature {
155
+ classicalSignature: string;
156
+ pqcSignature: string;
157
+ }
158
+ /**
159
+ * The responder's half of the handshake: one fresh ECDH-P384 public key,
160
+ * one fresh ML-KEM-1024 public key, and a per-handshake nonce, dual-signed
161
+ * with the responder's long-term classical+PQ signing keys.
162
+ */
163
+ export interface ResponderHandshakeMessage {
164
+ ephemeralClassicalPublicKey: string;
165
+ ephemeralKemPublicKey: string;
166
+ nonce: string;
167
+ signature: HandshakeSignature;
168
+ }
169
+ /**
170
+ * The initiator's reply: its own fresh ECDH-P384 public key and nonce
171
+ * (dual-signed), plus the ML-KEM ciphertext encapsulated toward the
172
+ * responder's ephemeral KEM public key (never the responder's long-term
173
+ * one).
174
+ */
175
+ export interface InitiatorHandshakeMessage {
176
+ ephemeralClassicalPublicKey: string;
177
+ nonce: string;
178
+ signature: HandshakeSignature;
97
179
  kemCipherText: string;
180
+ }
181
+ /**
182
+ * The responder's ephemeral secrets plus everything needed to reconstruct
183
+ * its own signed message for the transcript hash later. Returned to the
184
+ * caller only to be threaded straight into `establishSessionResponderCore`
185
+ * for THIS handshake attempt — never persisted, logged, or attached to
186
+ * Identity, and discarded (left for GC) once that call returns.
187
+ */
188
+ export interface ResponderHandshakeSecrets {
189
+ ephemeralClassicalSecretKey: Uint8Array;
190
+ ephemeralKemSecretKey: Uint8Array;
191
+ message: ResponderHandshakeMessage;
192
+ }
193
+ export interface ResponderHandshakeInit {
194
+ message: ResponderHandshakeMessage;
195
+ ephemeral: ResponderHandshakeSecrets;
196
+ }
197
+ export interface InitiatorSessionResult {
198
+ /** Send this to the responder to complete the handshake. */
199
+ message: InitiatorHandshakeMessage;
200
+ keys: SessionKeys;
201
+ /** HKDF output #3 — proves both sides derived the same combined secret before any chat/file-transfer traffic is trusted. Using it (e.g. as a MAC key over the transcript) is the caller's concern. */
202
+ confirmationKey: Uint8Array;
203
+ /** HKDF output #4 (additive, issue #29) — seeds a two-party ratchet (see ./ratchet.ts) built on top of this handshake. Not a replacement for `keys`/`confirmationKey`, and never used to encrypt a frame directly. */
204
+ rootKey: Uint8Array;
205
+ }
206
+ export interface ResponderSessionResult {
98
207
  keys: SessionKeys;
208
+ confirmationKey: Uint8Array;
209
+ /** See InitiatorSessionResult.rootKey. */
210
+ rootKey: Uint8Array;
99
211
  }
212
+ export declare class HandshakeVerificationError extends Error {
213
+ constructor(message: string);
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;
244
+ /**
245
+ * First step of the handshake, run by the connection's responder: generates
246
+ * one fresh, single-use ECDH-P384 keypair and one fresh, single-use
247
+ * ML-KEM-1024 keypair, a per-handshake nonce, and dual-signs all three with
248
+ * the responder's long-term classical+PQ signing keys. `message` is sent to
249
+ * the initiator (it can ride alongside the responder's PublicCard); `ephemeral`
250
+ * must be held in memory only and passed straight into
251
+ * `establishSessionResponderCore` once the initiator's reply arrives, then
252
+ * discarded.
253
+ */
254
+ export declare function createResponderHandshakeCore(identity: Identity): ResponderHandshakeInit;
100
255
  /**
101
- * Establishes a session as the connection's initiator (the side that
102
- * created the WebRTC offer, in raz-encrypt-agnostic terms: whichever side
103
- * owns the single ML-KEM encapsulation this handshake performs). Computes
104
- * the ECDH-P384 secret with `counterpart` and one ML-KEM-1024 encapsulation
105
- * toward their KEM public key, then HKDF-splits the combined secret into two
106
- * directional AES-256-GCM keys. No trust dependency: establishing a session
107
- * never checks anyone's trust status, same as signCore.
256
+ * Establishes a session as the connection's initiator, given the
257
+ * responder's `ResponderHandshakeMessage`. Verifies the responder's dual
258
+ * signature (throws `HandshakeVerificationError` on failure), generates its
259
+ * own fresh, single-use ECDH-P384 keypair and nonce, dual-signs them,
260
+ * encapsulates toward the responder's ephemeral KEM public key, and derives
261
+ * the Session Keys plus a key-confirmation key from the combined ephemeral
262
+ * ECDH + KEM + long-term ECDH secret, salted by a hash of the full
263
+ * handshake transcript. The returned `message` is sent to the responder to
264
+ * complete the handshake. No trust dependency: establishing a session never
265
+ * checks anyone's trust status, same as signCore.
108
266
  */
109
- export declare function establishSessionInitiatorCore(identity: Identity, counterpart: PublicCard): InitiatorSessionResult;
267
+ export declare function establishSessionInitiatorCore(identity: Identity, counterpart: PublicCard, responderMessage: ResponderHandshakeMessage): InitiatorSessionResult;
110
268
  /**
111
- * Establishes a session as the connection's responder: computes the same
112
- * ECDH-P384 secret, decapsulates the initiator's `kemCipherText` with our
113
- * own KEM secret key, and derives the same two directional keys the
114
- * initiator derived.
269
+ * Completes the handshake as the connection's responder, given the
270
+ * `ephemeral` secrets returned by `createResponderHandshakeCore` and the
271
+ * initiator's `InitiatorHandshakeMessage`. Verifies the initiator's dual
272
+ * signature (throws `HandshakeVerificationError` on failure), decapsulates
273
+ * the KEM ciphertext with the ephemeral KEM secret key, computes the
274
+ * ephemeral ECDH secret, and derives the same Session Keys and
275
+ * confirmation key the initiator derived.
115
276
  */
116
- export declare function establishSessionResponderCore(identity: Identity, counterpart: PublicCard, kemCipherText: string): SessionKeys;
277
+ export declare function establishSessionResponderCore(identity: Identity, counterpart: PublicCard, ephemeral: ResponderHandshakeSecrets, initiatorMessage: InitiatorHandshakeMessage): ResponderSessionResult;
117
278
  export {};