raz-encrypt 0.1.0 → 0.3.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/README.md CHANGED
@@ -145,19 +145,6 @@ const combinedResult = await verify(
145
145
 
146
146
  **So, to directly answer "do I need to run verify first?"** — no. `verify()` is only relevant when Alice actually signed something; `decryptFrom()` is only relevant when Alice actually encrypted something. Run whichever one matches what you received, in either order, or both if she did both.
147
147
 
148
- ## Hands-on demo
149
-
150
- The `demo/` folder is a runnable version of the quickstart above, playing Alice and Bob as two separate local identities:
151
-
152
- ```bash
153
- npm run demo:setup # creates both identities, swaps public cards, records trust
154
- npm run demo:sign # Alice signs demo/sample-document.json
155
- npm run demo:verify # Bob verifies it -> { valid: true }
156
- npm run demo:tamper # Bob verifies a modified copy -> { valid: false, reason: "hash-mismatch" }
157
- npm run demo:verify # run again fresh -> proves identity/trust really persisted to disk, not just in memory
158
- npm run demo:clean # removes the generated local stores/cards so you can start over
159
- ```
160
-
161
148
  ## API
162
149
 
163
150
  | Function | What it does |
@@ -191,11 +178,11 @@ If one layer is ever broken — classical by a quantum computer, or the newer po
191
178
 
192
179
  **Bottom line:** this is genuinely strong, real-world-grade cryptography built from carefully audited, widely used components (the [`@noble/*`](https://github.com/paulmillr/noble-post-quantum) libraries) — not a marketing label. It just isn't a government-certified module, and true safety also depends on this specific implementation being built and tested carefully, which is exactly what the test suite's "try to break it" cases (tampered documents, corrupted individual signatures) are for.
193
180
 
194
- **What `sign`/`verify` guarantees, precisely.** Every field that affects a verification outcome — the document hash *and* the optional `expiresAt` — is part of the signed payload, not just `documentHash` alone. That matters because certificates travel over the exact same untrusted channels (email, chat) this library assumes for everything else: without that, an intermediary with no secret key at all could delete or rewrite `expiresAt` in transit and flip a `verify()` result, which would quietly defeat the whole point of setting an expiry.
181
+ **What `sign`/`verify` guarantees, precisely.** Every field that affects a verification outcome — the document hash _and_ the optional `expiresAt` — is part of the signed payload, not just `documentHash` alone. That matters because certificates travel over the exact same untrusted channels (email, chat) this library assumes for everything else: without that, an intermediary with no secret key at all could delete or rewrite `expiresAt` in transit and flip a `verify()` result, which would quietly defeat the whole point of setting an expiry.
195
182
 
196
183
  **Two honest limitations, so you can decide if they matter for your use case:**
197
184
 
198
- - **No forward secrecy.** `encryptFor`/`decryptFrom` derive their key from *long-term* keys on both sides: a static ECDH-P384 exchange (deterministic for a given pair of identities) combined with an ML-KEM-1024 encapsulation whose secret still requires the recipient's long-term KEM secret key to decapsulate. Both of those secret keys live in the same `identity.enc.json`. This means that if someone ever recovers your passphrase and that file, they can decrypt **every box anyone ever sent you**, not just future ones — there's no per-message ephemeral key rotation (the kind Signal-style ratcheting provides). Rotate identities (new `storageDir`, `trustParty()` again) if you need to bound the blast radius of a future key compromise.
185
+ - **No forward secrecy.** `encryptFor`/`decryptFrom` derive their key from _long-term_ keys on both sides: a static ECDH-P384 exchange (deterministic for a given pair of identities) combined with an ML-KEM-1024 encapsulation whose secret still requires the recipient's long-term KEM secret key to decapsulate. Both of those secret keys live in the same `identity.enc.json`. This means that if someone ever recovers your passphrase and that file, they can decrypt **every box anyone ever sent you**, not just future ones — there's no per-message ephemeral key rotation (the kind Signal-style ratcheting provides). Rotate identities (new `storageDir`, `trustParty()` again) if you need to bound the blast radius of a future key compromise.
199
186
  - **No replay protection.** An `EncryptedBox` decrypts identically no matter how many times, or how much later, it's replayed to the same recipient — there's no session id, counter, or timestamp binding it to a single delivery. Fine for "send this document once"; if your use case cares about replay (e.g. treating receipt of a box as a one-time authorization), add your own nonce/timestamp inside the encrypted payload and check it on the receiving end.
200
187
 
201
188
  ## What this library deliberately does _not_ do (v1)
@@ -13,11 +13,20 @@ export interface EncryptedFrame {
13
13
  * `counter`. The caller owns counter bookkeeping: increment it once per
14
14
  * frame sent under this key, one counter sequence per direction, and never
15
15
  * reuse a value.
16
+ *
17
+ * `aad` is optional Associated Authenticated Data: extra context (e.g.
18
+ * direction, frame/message type) bound into the AEAD tag but not encrypted.
19
+ * It is never transmitted as part of EncryptedFrame — the receiver must
20
+ * reconstruct the identical bytes out of band (from the frame's own
21
+ * metadata, e.g. its direction and type) and pass them to decryptFrame.
22
+ * Omitting it behaves exactly as before this parameter existed.
16
23
  */
17
- export declare function encryptFrame(key: Uint8Array, counter: number, plaintext: Uint8Array): EncryptedFrame;
24
+ export declare function encryptFrame(key: Uint8Array, counter: number, plaintext: Uint8Array, aad?: Uint8Array): EncryptedFrame;
18
25
  /**
19
26
  * Decrypts a frame produced by encryptFrame. Fails closed — throws
20
- * FrameDecryptionError — on a tampered ciphertext, a mismatched counter, or
21
- * the wrong directional key, rather than ever returning corrupted plaintext.
27
+ * FrameDecryptionError — on a tampered ciphertext, a mismatched counter, the
28
+ * wrong directional key, or an `aad` that doesn't match what the sender used
29
+ * (including one side supplying `aad` and the other omitting it), rather
30
+ * than ever returning corrupted plaintext.
22
31
  */
23
- export declare function decryptFrame(key: Uint8Array, frame: EncryptedFrame): Uint8Array;
32
+ export declare function decryptFrame(key: Uint8Array, frame: EncryptedFrame, aad?: Uint8Array): Uint8Array;
@@ -37,19 +37,28 @@ function counterToNonce(counter) {
37
37
  * `counter`. The caller owns counter bookkeeping: increment it once per
38
38
  * frame sent under this key, one counter sequence per direction, and never
39
39
  * reuse a value.
40
+ *
41
+ * `aad` is optional Associated Authenticated Data: extra context (e.g.
42
+ * direction, frame/message type) bound into the AEAD tag but not encrypted.
43
+ * It is never transmitted as part of EncryptedFrame — the receiver must
44
+ * reconstruct the identical bytes out of band (from the frame's own
45
+ * metadata, e.g. its direction and type) and pass them to decryptFrame.
46
+ * Omitting it behaves exactly as before this parameter existed.
40
47
  */
41
- export function encryptFrame(key, counter, plaintext) {
42
- const ciphertext = gcm(key, counterToNonce(counter)).encrypt(plaintext);
48
+ export function encryptFrame(key, counter, plaintext, aad) {
49
+ const ciphertext = gcm(key, counterToNonce(counter), aad).encrypt(plaintext);
43
50
  return { counter, ciphertext: bytesToHex(ciphertext) };
44
51
  }
45
52
  /**
46
53
  * Decrypts a frame produced by encryptFrame. Fails closed — throws
47
- * FrameDecryptionError — on a tampered ciphertext, a mismatched counter, or
48
- * the wrong directional key, rather than ever returning corrupted plaintext.
54
+ * FrameDecryptionError — on a tampered ciphertext, a mismatched counter, the
55
+ * wrong directional key, or an `aad` that doesn't match what the sender used
56
+ * (including one side supplying `aad` and the other omitting it), rather
57
+ * than ever returning corrupted plaintext.
49
58
  */
50
- export function decryptFrame(key, frame) {
59
+ export function decryptFrame(key, frame, aad) {
51
60
  try {
52
- return gcm(key, counterToNonce(frame.counter)).decrypt(hexToBytes(frame.ciphertext));
61
+ return gcm(key, counterToNonce(frame.counter), aad).decrypt(hexToBytes(frame.ciphertext));
53
62
  }
54
63
  catch {
55
64
  throw new FrameDecryptionError();
@@ -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;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,OAAe,EAAE,SAAqB;IAClF,MAAM,UAAU,GAAG,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAe,EAAE,KAAqB;IACjE,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IACvF,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,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "raz-encrypt",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local-first, hybrid classical+post-quantum signing and verification for two consenting parties. No server, no database required.",
5
5
  "license": "MIT",
6
6
  "type": "module",