raz-encrypt 0.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/dist/canonicalJson.d.ts +3 -0
  4. package/dist/canonicalJson.js +25 -0
  5. package/dist/canonicalJson.js.map +1 -0
  6. package/dist/core.d.ts +23 -0
  7. package/dist/core.js +19 -0
  8. package/dist/core.js.map +1 -0
  9. package/dist/crypto/classical.d.ts +10 -0
  10. package/dist/crypto/classical.js +21 -0
  11. package/dist/crypto/classical.js.map +1 -0
  12. package/dist/crypto/frameCipher.d.ts +23 -0
  13. package/dist/crypto/frameCipher.js +58 -0
  14. package/dist/crypto/frameCipher.js.map +1 -0
  15. package/dist/crypto/hybrid.d.ts +117 -0
  16. package/dist/crypto/hybrid.js +180 -0
  17. package/dist/crypto/hybrid.js.map +1 -0
  18. package/dist/crypto/nodeHybrid.d.ts +42 -0
  19. package/dist/crypto/nodeHybrid.js +65 -0
  20. package/dist/crypto/nodeHybrid.js.map +1 -0
  21. package/dist/crypto/pqc.d.ts +19 -0
  22. package/dist/crypto/pqc.js +28 -0
  23. package/dist/crypto/pqc.js.map +1 -0
  24. package/dist/fileCrypto.d.ts +6 -0
  25. package/dist/fileCrypto.js +45 -0
  26. package/dist/fileCrypto.js.map +1 -0
  27. package/dist/fingerprint.d.ts +11 -0
  28. package/dist/fingerprint.js +18 -0
  29. package/dist/fingerprint.js.map +1 -0
  30. package/dist/identity.d.ts +42 -0
  31. package/dist/identity.js +87 -0
  32. package/dist/identity.js.map +1 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.js +7 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/store.d.ts +28 -0
  37. package/dist/store.js +62 -0
  38. package/dist/store.js.map +1 -0
  39. package/dist/trust.d.ts +41 -0
  40. package/dist/trust.js +98 -0
  41. package/dist/trust.js.map +1 -0
  42. package/package.json +48 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 razmans
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # RAZ Encrypt
2
+
3
+ A local-first npm library for **two consenting parties** to sign documents, verify each other's signatures, and privately encrypt data for each other — with no server, no database, and no third-party account. Each party keeps their own identity and audit log in a small encrypted file on their own machine.
4
+
5
+ Built around a **hybrid classical + post-quantum** cryptography design (see "Security posture" below) rather than any single algorithm.
6
+
7
+ ## How it works (the short version)
8
+
9
+ 1. Alice and Bob each call `createIdentity()` once. This generates their keys and stores them, encrypted, in a local folder.
10
+ 2. They exchange their **public cards** — just public keys, safe to send over email/Slack/whatever — and call `trustParty()` to record each other as trusted.
11
+ 3. From then on: Alice can `sign()` a document; Bob can `verify()` it against the certificate. Either of them can `encryptFor()` the other and `decryptFrom()` what they receive.
12
+
13
+ Nothing here requires the two parties' computers to ever talk to each other directly — everything is exchanged "by hand" (file, email, chat), and each side's storage is entirely local and private to them.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install raz-encrypt
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```ts
24
+ import {
25
+ createIdentity,
26
+ exportPublicIdentity,
27
+ trustParty,
28
+ sign,
29
+ verify,
30
+ } from "raz-encrypt";
31
+
32
+ // --- Alice, once ---
33
+ const alice = await createIdentity({
34
+ storageDir: "./alice-store",
35
+ passphrase: "alice's secret passphrase",
36
+ alias: "alice",
37
+ });
38
+ const aliceCard = exportPublicIdentity(alice); // send this to Bob
39
+
40
+ // --- Bob, once ---
41
+ const bob = await createIdentity({
42
+ storageDir: "./bob-store",
43
+ passphrase: "bob's secret passphrase",
44
+ alias: "bob",
45
+ });
46
+ const bobCard = exportPublicIdentity(bob); // send this to Alice
47
+
48
+ // --- Before trusting, compare fingerprints over a side channel (phone call, in person) ---
49
+ // This is what catches a spoofed or intercepted card — email/chat alone can't prove
50
+ // a card really came from the person you think it did.
51
+ import { fingerprint } from "raz-encrypt";
52
+ console.log("Alice reads this aloud:", fingerprint(aliceCard));
53
+ console.log("Bob confirms it matches what he sees:", fingerprint(bobCard));
54
+
55
+ // --- Both sides trust each other (after swapping cards AND confirming fingerprints) ---
56
+ await trustParty(alice, bobCard);
57
+ await trustParty(bob, aliceCard);
58
+
59
+ // --- Alice signs a document ---
60
+ const document = { invoiceId: "INV-1", amount: 15000 };
61
+ const certificate = await sign(alice, document);
62
+
63
+ // --- Bob verifies it (send him `document` + `certificate`) ---
64
+ const result = await verify(bob, document, certificate);
65
+ // => { valid: true }
66
+ ```
67
+
68
+ If the document is changed even slightly, or the certificate is corrupted, or the signer was never trusted, `verify()` returns `{ valid: false, reason: "..." }` instead of throwing — so a caller always gets a clear, structured answer.
69
+
70
+ ### Private data between the two parties
71
+
72
+ ```ts
73
+ import { encryptFor, decryptFrom } from "raz-encrypt";
74
+
75
+ const box = await encryptFor(alice, bobCard, {
76
+ secretPayload: "only Bob should read this",
77
+ });
78
+ const opened = await decryptFrom(bob, aliceCard, box);
79
+ ```
80
+
81
+ ## Two important things people ask when actually wiring this up
82
+
83
+ **1. `sign`/`verify` and `encryptFor`/`decryptFrom` are two separate, independent tools — not a pipeline.**
84
+
85
+ - `sign` + `verify` prove **authenticity**: "this exact document really came from Alice and wasn't altered." The document itself travels in the clear.
86
+ - `encryptFor` + `decryptFrom` provide **confidentiality**: "nobody but Bob can read this." It says nothing about who sent it.
87
+
88
+ You use whichever one (or both) fits what you're protecting against. You do **not** need to call `verify()` before you can call `decryptFrom()`, or vice versa — neither depends on the other. If you want both (a document that's both proven-authentic _and_ private), sign it, then encrypt the `{ document, certificate }` pair together — see the worked example below.
89
+
90
+ **2. A "card" (from `exportPublicIdentity`) is just a JSON object.** However Alice actually gets it to Bob — email attachment, Slack message, QR code, USB stick — Bob ends up with the same plain JSON on disk. Loading it back is nothing more than reading that file and `JSON.parse`-ing it; there's no special "import" step.
91
+
92
+ ## Full example: Bob receives something from Alice
93
+
94
+ Assume Bob has already run `createIdentity()` once (so he has his own `storageDir` + passphrase) and already has `alice-card.json` — the file Alice sent him after she ran `exportPublicIdentity()` on her side.
95
+
96
+ ```ts
97
+ import { readFile } from "node:fs/promises";
98
+ import {
99
+ loadIdentity,
100
+ decryptFrom,
101
+ verify,
102
+ type PublicCard,
103
+ type Certificate,
104
+ } from "raz-encrypt";
105
+
106
+ // Step 1 — Bob loads his own identity (needs his own passphrase, not Alice's).
107
+ const bob = await loadIdentity({
108
+ storageDir: "./bob-store",
109
+ passphrase: "bob's secret passphrase",
110
+ });
111
+
112
+ // Step 2 — Bob loads Alice's card. It's just JSON — read whatever file/message it arrived in.
113
+ const aliceCard: PublicCard = JSON.parse(
114
+ await readFile("./alice-card.json", "utf8"),
115
+ );
116
+ // (One-time only: before the first exchange, Bob must also have called
117
+ // trustParty(bob, aliceCard) once — otherwise decryptFrom/verify will refuse Alice's card.)
118
+
119
+ // --- If what Alice sent was ENCRYPTED (confidential) ---
120
+ const box = JSON.parse(await readFile("./received-box.json", "utf8"));
121
+ const opened = await decryptFrom(bob, aliceCard, box);
122
+
123
+ // --- If what Alice sent was SIGNED (a document + a certificate, sent in the clear) ---
124
+ const { document, certificate } = JSON.parse(
125
+ await readFile("./signed-doc.json", "utf8"),
126
+ ) as {
127
+ document: unknown;
128
+ certificate: Certificate;
129
+ };
130
+ const result = await verify(bob, document, certificate);
131
+ // => { valid: true } or { valid: false, reason: "..." }
132
+
133
+ // --- If Alice sent something both signed AND encrypted ---
134
+ // She encrypted { document, certificate } as one payload with encryptFor(); Bob does:
135
+ const openedSignedPayload = await decryptFrom<{
136
+ document: unknown;
137
+ certificate: Certificate;
138
+ }>(bob, aliceCard, box);
139
+ const combinedResult = await verify(
140
+ bob,
141
+ openedSignedPayload.document,
142
+ openedSignedPayload.certificate,
143
+ );
144
+ ```
145
+
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
+
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
+ ## API
162
+
163
+ | Function | What it does |
164
+ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `createIdentity({ storageDir, passphrase, alias, overwrite? })` | Generates a new identity (see "Security posture") and persists it encrypted at `storageDir`. Requires a passphrase of at least `MIN_PASSPHRASE_LENGTH` (12) characters. |
166
+ | `loadIdentity({ storageDir, passphrase })` | Loads a previously-created identity back from disk. |
167
+ | `exportPublicIdentity(identity)` | Returns the shareable public card `{ alias, classicalPublicKey, pqcSignPublicKey, pqcKemPublicKey }`. |
168
+ | `fingerprint(card)` | Short, human-comparable fingerprint of a public card (grouped hex). Compare this over a side channel before trusting a card you received over email/chat. |
169
+ | `trustParty(identity, card)` | Records another party's public card as trusted, locally. Re-trusting a previously revoked card reinstates it. |
170
+ | `untrustParty(identity, alias)` | Revokes every currently-trusted card under `alias` (e.g. after a suspected key compromise). Tombstoned, not deleted — `verify()` reports `"revoked-signer"` distinctly from `"untrusted-signer"` (never trusted). |
171
+ | `listTrustedParties(identity)` / `getTrustedParty(identity, alias)` | Read back your local trust list (revoked cards excluded). |
172
+ | `getTrustedPartiesByAlias(identity, alias)` / `getTrustRecordsByAlias(identity, alias)` | Since alias isn't unique, these return _every_ card under an alias — the latter includes revoked ones, with their `revokedAt`. |
173
+ | `listTrustRecords(identity)` | Full trust history, revoked included — for audit purposes. |
174
+ | `sign(identity, document, options?)` | Canonicalizes and hashes `document`, signs it with both keys, returns a `Certificate`. Pass `{ expiresAt }` (ISO timestamp) to make it expire; omit it for a permanent proof (the default). |
175
+ | `verify(identity, document, certificate)` | Checks the certificate's signer is trusted (and not revoked), the document hasn't changed, it hasn't expired, and both signatures check out. |
176
+ | `encryptFor(identity, counterpartCard, data)` / `decryptFrom(identity, counterpartCard, box)` | Encrypt/decrypt data meant only for one specific counterpart. Both throw `UntrustedPartyError` unless that counterpart is already in your local trust list (`trustParty`). |
177
+ | `readLog(identity)` | Reads the local, append-only audit log of everything this identity has signed/verified/encrypted/decrypted. |
178
+ | `verifyLogIntegrity(identity)` | Walks the log's hash chain and reports `{ intact: true }` or `{ intact: false, brokenAtIndex }` — detects an edited or deleted entry. |
179
+ | `canonicalize(value)` / `hashDocument(value)` | The deterministic JSON hashing used internally — exposed in case you want to compute a document hash yourself. |
180
+
181
+ ## Security posture
182
+
183
+ **Is this "military grade"?** It uses the same algorithms — ECDSA/ECDH on curve P-384, AES-256 — that banks, governments, and CNSA-1.0-compliant systems use. The math is the real thing, not a toy. What it is **not** is FIPS 140-2/3 (CMVP) validated: that's a certification of one specific software build/deployment, done by an accredited lab, and it takes years — no realistic npm library ships with that certification. Not being certified doesn't mean it's unsafe: it's the difference between a home-cooked meal made with the exact same top-shelf ingredients a certified restaurant uses, versus the restaurant's official health-inspection paperwork. The ingredients and technique are what make it safe; the certificate just proves it to a third party who requires that paperwork (e.g. a specific government/defense procurement rule). If your use case legally _requires_ FIPS-validated modules, this library's logic needs to run inside a validated HSM/crypto boundary — that's a deployment decision outside what any library's code can provide on its own.
184
+
185
+ **Is this quantum-safe?** Half of the design is built specifically for that. **ML-KEM-1024** (key exchange) and **ML-DSA-87** (signatures) are NIST's official post-quantum standards (FIPS 203 / FIPS 204, finalized 2024), chosen because classical ECC and RSA can eventually be broken by a large enough quantum computer via Shor's algorithm. We don't rely on the post-quantum algorithms _alone_, though — they're only a few years old, with far less real-world cryptanalysis behind them than P-384. So every signature and every shared secret in this library requires **both** the classical layer and the post-quantum layer to hold:
186
+
187
+ - **Signing**: every document is signed twice — once with ECDSA-P384, once with ML-DSA-87. `verify()` only returns `valid: true` if **both** signatures check out.
188
+ - **Encryption**: the AES-256-GCM key is derived (via HKDF-SHA384) from **both** an ECDH-P384 exchange and an ML-KEM-1024 encapsulation.
189
+
190
+ If one layer is ever broken — classical by a quantum computer, or the newer post-quantum standard by an as-yet-undiscovered flaw — the other still protects you.
191
+
192
+ **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
+
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.
195
+
196
+ **Two honest limitations, so you can decide if they matter for your use case:**
197
+
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.
199
+ - **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
+
201
+ ## What this library deliberately does _not_ do (v1)
202
+
203
+ - No server, no HTTP API, no database — everything is local files.
204
+ - No live network sync between the two parties' machines — public cards, documents, and certificates are exchanged "by hand" (file/email/etc.), not automatically.
205
+ - No multi-party or hierarchical signing (buyers/vendors/approval-levels/etc.) — this is a generic building block for exactly two parties trusting each other directly.
206
+ - **Node.js only, not a browser library.** The underlying `@noble/*` crypto libraries are isomorphic and would run fine in a browser, but the storage layer (`identity.enc.json`, `trust.json`, `log.jsonl`) is built on `node:fs`, which browsers don't have. There's no IndexedDB (or similar) storage adapter — if you need this in a browser, that adapter would need to be built.
207
+
208
+ ## Local storage layout
209
+
210
+ Each identity's `storageDir` contains:
211
+
212
+ ```
213
+ identity.enc.json # your keys, encrypted at rest under your passphrase (AES-256-GCM, scrypt-derived key)
214
+ trust.json # fingerprint-keyed public cards of parties you've trusted (or revoked) — not secret, just public keys
215
+ log.jsonl # hash-chained, append-only audit log: what you've signed/verified/encrypted/decrypted, and when
216
+ ```
217
+
218
+ ## Development
219
+
220
+ ```bash
221
+ npm install
222
+ npm run typecheck
223
+ npm test
224
+ npm run build
225
+ ```
@@ -0,0 +1,3 @@
1
+ export declare function canonicalize(value: unknown): string;
2
+ /** The document hash every signature is made over. */
3
+ export declare function hashDocument(value: unknown): string;
@@ -0,0 +1,25 @@
1
+ import { sha512 } from "@noble/hashes/sha2.js";
2
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
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.
7
+ */
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])]));
15
+ }
16
+ return value;
17
+ }
18
+ export function canonicalize(value) {
19
+ return JSON.stringify(sortKeysDeep(value));
20
+ }
21
+ /** The document hash every signature is made over. */
22
+ export function hashDocument(value) {
23
+ return bytesToHex(sha512(utf8ToBytes(canonicalize(value))));
24
+ }
25
+ //# sourceMappingURL=canonicalJson.js.map
@@ -0,0 +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"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Browser-safe crypto surface: everything from the hybrid classical+PQC
3
+ * layer, with zero `node:fs` dependency — nothing here touches
4
+ * identity.enc.json, trust.json, or log.jsonl.
5
+ *
6
+ * The four crypto operations take an explicit trust/log dependency instead
7
+ * of a persisted identity/trust store, so any caller (a browser, a
8
+ * different storage backend, tests) can supply its own. The top-level `"."`
9
+ * export's `sign`/`verify`/`encryptFor`/`decryptFrom` remain the batteries-
10
+ * included, fs-backed versions of the same logic — this module is the
11
+ * un-wired core they're built from, not a separate implementation.
12
+ */
13
+ export { decryptFromCore as decryptFrom, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
14
+ export type { Certificate, EncryptedBox, InitiatorSessionResult, LogDeps, SessionKeys, SignOptions, TrustDeps, TrustStatus, VerifyDeps, VerifyFailureReason, VerifyResult, } from "./crypto/hybrid.js";
15
+ export { generateClassicalKeypair, classicalSharedSecret, classicalSign, classicalVerify } from "./crypto/classical.js";
16
+ export type { ClassicalKeypair } from "./crypto/classical.js";
17
+ export { generatePqcKemKeypair, generatePqcSignKeypair, pqcDecapsulate, pqcEncapsulate, pqcSign, pqcVerify, } from "./crypto/pqc.js";
18
+ export type { PqcKemKeypair, PqcSignKeypair } from "./crypto/pqc.js";
19
+ export { canonicalize, hashDocument } from "./canonicalJson.js";
20
+ export { fingerprint } from "./fingerprint.js";
21
+ export { decryptFrame, encryptFrame, FrameDecryptionError } from "./crypto/frameCipher.js";
22
+ export type { EncryptedFrame } from "./crypto/frameCipher.js";
23
+ export type { Identity, PublicCard } from "./identity.js";
package/dist/core.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Browser-safe crypto surface: everything from the hybrid classical+PQC
3
+ * layer, with zero `node:fs` dependency — nothing here touches
4
+ * identity.enc.json, trust.json, or log.jsonl.
5
+ *
6
+ * The four crypto operations take an explicit trust/log dependency instead
7
+ * of a persisted identity/trust store, so any caller (a browser, a
8
+ * different storage backend, tests) can supply its own. The top-level `"."`
9
+ * export's `sign`/`verify`/`encryptFor`/`decryptFrom` remain the batteries-
10
+ * included, fs-backed versions of the same logic — this module is the
11
+ * un-wired core they're built from, not a separate implementation.
12
+ */
13
+ export { decryptFromCore as decryptFrom, encryptForCore as encryptFor, establishSessionInitiatorCore as establishSessionInitiator, establishSessionResponderCore as establishSessionResponder, signCore as sign, UntrustedPartyError, verifyCore as verify, } from "./crypto/hybrid.js";
14
+ export { generateClassicalKeypair, classicalSharedSecret, classicalSign, classicalVerify } from "./crypto/classical.js";
15
+ export { generatePqcKemKeypair, generatePqcSignKeypair, pqcDecapsulate, pqcEncapsulate, pqcSign, pqcVerify, } from "./crypto/pqc.js";
16
+ export { canonicalize, hashDocument } from "./canonicalJson.js";
17
+ export { fingerprint } from "./fingerprint.js";
18
+ export { decryptFrame, encryptFrame, FrameDecryptionError } from "./crypto/frameCipher.js";
19
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,eAAe,IAAI,WAAW,EAC9B,cAAc,IAAI,UAAU,EAC5B,6BAA6B,IAAI,yBAAyB,EAC1D,6BAA6B,IAAI,yBAAyB,EAC1D,QAAQ,IAAI,IAAI,EAChB,mBAAmB,EACnB,UAAU,IAAI,MAAM,GACrB,MAAM,oBAAoB,CAAC;AAe5B,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"}
@@ -0,0 +1,10 @@
1
+ export interface ClassicalKeypair {
2
+ publicKey: Uint8Array;
3
+ secretKey: Uint8Array;
4
+ }
5
+ /** ECDSA/ECDH on NIST P-384 — the classical, CNSA-1.0-eligible layer. */
6
+ export declare function generateClassicalKeypair(): ClassicalKeypair;
7
+ export declare function classicalSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array;
8
+ export declare function classicalVerify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean;
9
+ /** ECDH shared secret between our secret key and their public key. */
10
+ export declare function classicalSharedSecret(secretKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array;
@@ -0,0 +1,21 @@
1
+ import { p384 } from "@noble/curves/nist.js";
2
+ /** ECDSA/ECDH on NIST P-384 — the classical, CNSA-1.0-eligible layer. */
3
+ export function generateClassicalKeypair() {
4
+ return p384.keygen();
5
+ }
6
+ export function classicalSign(message, secretKey) {
7
+ return p384.sign(message, secretKey);
8
+ }
9
+ export function classicalVerify(signature, message, publicKey) {
10
+ try {
11
+ return p384.verify(signature, message, publicKey);
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /** ECDH shared secret between our secret key and their public key. */
18
+ export function classicalSharedSecret(secretKey, theirPublicKey) {
19
+ return p384.getSharedSecret(secretKey, theirPublicKey);
20
+ }
21
+ //# sourceMappingURL=classical.js.map
@@ -0,0 +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"}
@@ -0,0 +1,23 @@
1
+ export declare class FrameDecryptionError extends Error {
2
+ constructor();
3
+ }
4
+ export interface EncryptedFrame {
5
+ /** The nonce input, transmitted alongside the ciphertext — see Q9 in the session design: explicit, not implicit. */
6
+ counter: number;
7
+ /** Hex-encoded AES-256-GCM output (ciphertext with the auth tag appended). */
8
+ ciphertext: string;
9
+ }
10
+ /**
11
+ * Encrypts one frame of `plaintext` under `key` (one of the two directional
12
+ * keys from establishSessionInitiatorCore/establishSessionResponderCore) at
13
+ * `counter`. The caller owns counter bookkeeping: increment it once per
14
+ * frame sent under this key, one counter sequence per direction, and never
15
+ * reuse a value.
16
+ */
17
+ export declare function encryptFrame(key: Uint8Array, counter: number, plaintext: Uint8Array): EncryptedFrame;
18
+ /**
19
+ * 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.
22
+ */
23
+ export declare function decryptFrame(key: Uint8Array, frame: EncryptedFrame): Uint8Array;
@@ -0,0 +1,58 @@
1
+ import { gcm } from "@noble/ciphers/aes.js";
2
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
3
+ // The per-frame AEAD used to wrap every message sent under a session key
4
+ // established by establishSessionInitiatorCore/establishSessionResponderCore
5
+ // (see hybrid.ts) — one directional key, reused across many frames, so the
6
+ // nonce can never repeat under that key or AES-GCM's security fails. Nonces
7
+ // here are the caller-supplied `counter`, never random and never derived
8
+ // implicitly from delivery order: the caller (one per direction) increments
9
+ // it once per frame and MUST NEVER reuse a counter value under the same key
10
+ // — reusing one is a silent security failure, not something this module can
11
+ // detect, since a stateless encrypt call has no memory of counters it's
12
+ // already used.
13
+ const NONCE_LENGTH = 12;
14
+ const COUNTER_BYTES = 8;
15
+ export class FrameDecryptionError extends Error {
16
+ constructor() {
17
+ super("Failed to decrypt frame: wrong key, wrong counter, or the frame was tampered with in transit.");
18
+ this.name = "FrameDecryptionError";
19
+ }
20
+ }
21
+ /** 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
+ function counterToNonce(counter) {
23
+ if (!Number.isInteger(counter) || counter < 0) {
24
+ throw new Error(`Frame counter must be a non-negative integer, got ${counter}.`);
25
+ }
26
+ const nonce = new Uint8Array(NONCE_LENGTH);
27
+ let value = BigInt(counter);
28
+ for (let i = NONCE_LENGTH - 1; i >= NONCE_LENGTH - COUNTER_BYTES; i--) {
29
+ nonce[i] = Number(value & 0xffn);
30
+ value >>= 8n;
31
+ }
32
+ return nonce;
33
+ }
34
+ /**
35
+ * Encrypts one frame of `plaintext` under `key` (one of the two directional
36
+ * keys from establishSessionInitiatorCore/establishSessionResponderCore) at
37
+ * `counter`. The caller owns counter bookkeeping: increment it once per
38
+ * frame sent under this key, one counter sequence per direction, and never
39
+ * reuse a value.
40
+ */
41
+ export function encryptFrame(key, counter, plaintext) {
42
+ const ciphertext = gcm(key, counterToNonce(counter)).encrypt(plaintext);
43
+ return { counter, ciphertext: bytesToHex(ciphertext) };
44
+ }
45
+ /**
46
+ * 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.
49
+ */
50
+ export function decryptFrame(key, frame) {
51
+ try {
52
+ return gcm(key, counterToNonce(frame.counter)).decrypt(hexToBytes(frame.ciphertext));
53
+ }
54
+ catch {
55
+ throw new FrameDecryptionError();
56
+ }
57
+ }
58
+ //# sourceMappingURL=frameCipher.js.map
@@ -0,0 +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"}
@@ -0,0 +1,117 @@
1
+ import type { Identity, PublicCard } from "../identity.js";
2
+ import type { LogEntry } from "../store.js";
3
+ declare const ALGORITHMS: {
4
+ readonly classical: "ECDSA-P384";
5
+ readonly pqc: "ML-DSA-87";
6
+ };
7
+ export declare class UntrustedPartyError extends Error {
8
+ constructor(alias: string);
9
+ }
10
+ export interface Certificate {
11
+ documentHash: string;
12
+ classicalSignature: string;
13
+ pqcSignature: string;
14
+ algorithms: typeof ALGORITHMS;
15
+ signer: {
16
+ alias: string;
17
+ classicalPublicKey: string;
18
+ pqcSignPublicKey: string;
19
+ };
20
+ signedAt: string;
21
+ /** Absent = permanent proof, valid indefinitely (the default, unchanged behavior). */
22
+ expiresAt?: string;
23
+ }
24
+ export interface SignOptions {
25
+ /** If set, verify() rejects this certificate as "expired" once this ISO timestamp has passed. */
26
+ expiresAt?: string;
27
+ }
28
+ export type VerifyFailureReason = "untrusted-signer" | "revoked-signer" | "hash-mismatch" | "classical-signature-mismatch" | "pqc-signature-mismatch" | "expired";
29
+ export type VerifyResult = {
30
+ valid: true;
31
+ } | {
32
+ valid: false;
33
+ reason: VerifyFailureReason;
34
+ };
35
+ export type TrustStatus = "trusted" | "revoked" | "untrusted";
36
+ /** Injectable audit-log hook, shared by every *Core function. Omit to log nothing. */
37
+ export interface LogDeps {
38
+ onLog?: (entry: Omit<LogEntry, "at">) => void | Promise<void>;
39
+ }
40
+ /**
41
+ * verifyCore's trust dependency. Matches only the fields a Certificate's
42
+ * signer actually carries (no KEM key — signing never uses it), same as
43
+ * verify()'s own matching always did.
44
+ */
45
+ export interface VerifyDeps extends LogDeps {
46
+ checkTrust: (signer: {
47
+ alias: string;
48
+ classicalPublicKey: string;
49
+ pqcSignPublicKey: string;
50
+ }) => Promise<TrustStatus>;
51
+ }
52
+ /** encryptForCore/decryptFromCore's trust dependency — matches the full PublicCard, KEM key included. */
53
+ export interface TrustDeps extends LogDeps {
54
+ checkTrust: (card: PublicCard) => Promise<TrustStatus>;
55
+ }
56
+ /**
57
+ * Signs `document` with both the classical (ECDSA-P384) and post-quantum
58
+ * (ML-DSA-87) keys. Permanent by default; pass `options.expiresAt` to make
59
+ * verify() reject it once that time has passed. Takes no trust dependency —
60
+ * signing never checks anyone else's trust status, only your own keys.
61
+ */
62
+ export declare function signCore(identity: Identity, document: unknown, options?: SignOptions, deps?: LogDeps): Promise<Certificate>;
63
+ /**
64
+ * Verifies `certificate` over `document` using an injected trust check
65
+ * (`deps.checkTrust`) instead of a hard-wired trust store. Requires BOTH the
66
+ * classical and post-quantum signatures to check out — either one failing
67
+ * fails the whole verification.
68
+ */
69
+ export declare function verifyCore(identity: Identity, document: unknown, certificate: Certificate, deps: VerifyDeps): Promise<VerifyResult>;
70
+ export interface EncryptedBox {
71
+ kemCipherText: string;
72
+ nonce: string;
73
+ ciphertext: string;
74
+ }
75
+ /**
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".
80
+ */
81
+ export declare function encryptForCore(identity: Identity, counterpart: PublicCard, data: unknown, deps: TrustDeps): Promise<EncryptedBox>;
82
+ /**
83
+ * Decrypts a box produced by `encryptFor`. `counterpart` is the sender's
84
+ * 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.
87
+ */
88
+ export declare function decryptFromCore<T = unknown>(identity: Identity, counterpart: PublicCard, box: EncryptedBox, deps: TrustDeps): Promise<T>;
89
+ export interface SessionKeys {
90
+ /** AES-256-GCM key for frames sent by the initiator (the side that encapsulated), decrypted by the responder. */
91
+ initiatorToResponder: Uint8Array;
92
+ /** AES-256-GCM key for frames sent by the responder (the side that decapsulated), decrypted by the initiator. */
93
+ responderToInitiator: Uint8Array;
94
+ }
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. */
97
+ kemCipherText: string;
98
+ keys: SessionKeys;
99
+ }
100
+ /**
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.
108
+ */
109
+ export declare function establishSessionInitiatorCore(identity: Identity, counterpart: PublicCard): InitiatorSessionResult;
110
+ /**
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.
115
+ */
116
+ export declare function establishSessionResponderCore(identity: Identity, counterpart: PublicCard, kemCipherText: string): SessionKeys;
117
+ export {};