raz-encrypt 2.1.0 → 2.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.
Files changed (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +448 -442
  3. package/dist/canonicalJson.d.ts +0 -15
  4. package/dist/canonicalJson.js +13 -73
  5. package/dist/canonicalJson.js.map +1 -1
  6. package/dist/core.d.ts +2 -4
  7. package/dist/core.js +1 -3
  8. package/dist/core.js.map +1 -1
  9. package/dist/crypto/classical.d.ts +0 -2
  10. package/dist/crypto/classical.js +0 -4
  11. package/dist/crypto/classical.js.map +1 -1
  12. package/dist/crypto/frameCipher.js +2 -8
  13. package/dist/crypto/frameCipher.js.map +1 -1
  14. package/dist/crypto/hybrid.d.ts +8 -95
  15. package/dist/crypto/hybrid.js +49 -374
  16. package/dist/crypto/hybrid.js.map +1 -1
  17. package/dist/crypto/nodeHybrid.d.ts +4 -6
  18. package/dist/crypto/nodeHybrid.js +2 -3
  19. package/dist/crypto/nodeHybrid.js.map +1 -1
  20. package/dist/crypto/pqc.d.ts +2 -7
  21. package/dist/crypto/pqc.js +4 -13
  22. package/dist/crypto/pqc.js.map +1 -1
  23. package/dist/crypto/ratchet.js +0 -5
  24. package/dist/crypto/ratchet.js.map +1 -1
  25. package/dist/fileCrypto.d.ts +2 -44
  26. package/dist/fileCrypto.js +28 -197
  27. package/dist/fileCrypto.js.map +1 -1
  28. package/dist/identity.d.ts +4 -69
  29. package/dist/identity.js +12 -111
  30. package/dist/identity.js.map +1 -1
  31. package/dist/index.d.ts +7 -8
  32. package/dist/index.js +4 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/store.d.ts +5 -42
  35. package/dist/store.js +22 -254
  36. package/dist/store.js.map +1 -1
  37. package/dist/trust.d.ts +2 -143
  38. package/dist/trust.js +27 -376
  39. package/dist/trust.js.map +1 -1
  40. package/package.json +69 -62
  41. package/dist/crypto/confirmedSession.d.ts +0 -47
  42. package/dist/crypto/confirmedSession.js +0 -84
  43. package/dist/crypto/confirmedSession.js.map +0 -1
  44. package/dist/crypto/frameSession.d.ts +0 -40
  45. package/dist/crypto/frameSession.js +0 -96
  46. package/dist/crypto/frameSession.js.map +0 -1
  47. package/dist/passphrase.d.ts +0 -27
  48. package/dist/passphrase.js +0 -69
  49. package/dist/passphrase.js.map +0 -1
package/dist/identity.js CHANGED
@@ -1,34 +1,9 @@
1
1
  import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { generateClassicalKeypair, isValidClassicalKeypairShape } from "./crypto/classical.js";
5
- import { signCore } from "./crypto/hybrid.js";
6
- import { decryptFileWithFormat, DecryptionFailedError, encryptToFile, upgradeLegacyFile } from "./fileCrypto.js";
7
- import { assertAcceptablePassphrase, MIN_PASSPHRASE_LENGTH, normalizePassphrase } from "./passphrase.js";
8
- import { generatePqcKemKeypair, generatePqcSignKeypair, isValidPqcKemKeypairShape, isValidPqcSignKeypairShape, } from "./crypto/pqc.js";
9
- function isHex(value) {
10
- return typeof value === "string" && value.length % 2 === 0 && /^[0-9a-f]*$/i.test(value);
11
- }
12
- function isValidKeypairShape(value) {
13
- if (typeof value !== "object" || value === null)
14
- return false;
15
- const v = value;
16
- return isHex(v.publicKey) && isHex(v.secretKey);
17
- }
18
- /**
19
- * Structural shape check for a decrypted identity file's plaintext. In
20
- * normal operation AEAD authentication already guarantees this shape, so a
21
- * malformed value here would mean a wrong/corrupted ciphertext slipped past
22
- * decryption (practically impossible without the passphrase) — this exists
23
- * so that case fails closed with DecryptionFailedError instead of crashing
24
- * on a missing/non-hex field (found by fuzzing, issue #42).
25
- */
26
- function isValidSerializedIdentityShape(data) {
27
- if (typeof data !== "object" || data === null)
28
- return false;
29
- const d = data;
30
- return typeof d.alias === "string" && isValidKeypairShape(d.classical) && isValidKeypairShape(d.pqcSign) && isValidKeypairShape(d.pqcKem);
31
- }
4
+ import { generateClassicalKeypair } from "./crypto/classical.js";
5
+ import { decryptFromFile, encryptToFile } from "./fileCrypto.js";
6
+ import { generatePqcKemKeypair, generatePqcSignKeypair } from "./crypto/pqc.js";
32
7
  function identityFilePath(storageDir) {
33
8
  return join(storageDir, "identity.enc.json");
34
9
  }
@@ -67,28 +42,20 @@ function deserialize(storageDir, data) {
67
42
  },
68
43
  };
69
44
  }
70
- export { MIN_PASSPHRASE_LENGTH };
45
+ export const MIN_PASSPHRASE_LENGTH = 12;
71
46
  /**
72
47
  * Generates a new hybrid classical+post-quantum identity (one ECDSA/ECDH-P384
73
48
  * keypair, one ML-DSA-87 signing keypair, one ML-KEM-1024 key-exchange
74
49
  * keypair) and persists it encrypted-at-rest under `storageDir`.
75
50
  *
76
- * The passphrase is normalized to Unicode NFKC, must be at least
77
- * MIN_PASSPHRASE_LENGTH characters (counted as code points after
78
- * normalization), must not be a single repeated character, and must not be
79
- * on a small blocklist of very common passphrases (ADR 0017). These checks
80
- * only apply here, not to loadIdentity, so an identity created before they
81
- * existed can still be loaded with its original passphrase.
82
- *
83
- * `options.checkPassphrase`, if supplied, runs after all of the above pass
84
- * (ADR 0021) — this is where real breach-corpus checking belongs, not in
85
- * this library's own, deliberately small, blocklist.
51
+ * Requires a passphrase of at least MIN_PASSPHRASE_LENGTH characters — this
52
+ * check only applies here, not to loadIdentity, so an identity created
53
+ * before this check existed can still be loaded with its original passphrase.
86
54
  */
87
55
  export async function createIdentity(options) {
88
- const passphrase = normalizePassphrase(options.passphrase);
89
- assertAcceptablePassphrase(passphrase);
90
- if (options.checkPassphrase)
91
- await options.checkPassphrase(passphrase);
56
+ if (options.passphrase.length < MIN_PASSPHRASE_LENGTH) {
57
+ throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LENGTH} characters long.`);
58
+ }
92
59
  const filePath = identityFilePath(options.storageDir);
93
60
  if (!options.overwrite && existsSync(filePath)) {
94
61
  throw new Error(`An identity already exists at ${filePath}. Pass { overwrite: true } to replace it.`);
@@ -100,67 +67,14 @@ export async function createIdentity(options) {
100
67
  pqcSign: generatePqcSignKeypair(),
101
68
  pqcKem: generatePqcKemKeypair(),
102
69
  };
103
- await encryptToFile(filePath, passphrase, serialize(identity));
70
+ await encryptToFile(filePath, options.passphrase, serialize(identity));
104
71
  return identity;
105
72
  }
106
73
  /** Loads a previously-created identity back from disk. */
107
74
  export async function loadIdentity(options) {
108
- const filePath = identityFilePath(options.storageDir);
109
- const normalized = normalizePassphrase(options.passphrase);
110
- // Normalized form first; the raw form only matters for a file written
111
- // before normalization existed under a passphrase NFKC would change.
112
- const candidates = normalized === options.passphrase ? [normalized] : [normalized, options.passphrase];
113
- const { data, legacy, passphraseIndex } = await decryptFileWithFormat(filePath, candidates);
114
- if (!isValidSerializedIdentityShape(data))
115
- throw new DecryptionFailedError();
116
- if (legacy || passphraseIndex > 0) {
117
- // Upgrade a pre-versioned file (and/or re-key one made under an
118
- // un-normalized passphrase) to the current format under the normalized
119
- // passphrase now that the passphrase is proven correct. Best effort: a
120
- // read-only directory or a full disk must not stop the identity from
121
- // loading.
122
- try {
123
- await upgradeLegacyFile(filePath, normalized, data);
124
- }
125
- catch {
126
- // The original file is untouched (see upgradeLegacyFile); try again on the next load.
127
- }
128
- }
75
+ const data = await decryptFromFile(identityFilePath(options.storageDir), options.passphrase);
129
76
  return deserialize(options.storageDir, data);
130
77
  }
131
- /**
132
- * Builds an Identity from a caller-supplied KeyStore instead of a
133
- * passphrase-encrypted file — `identity.enc.json` and a passphrase are
134
- * never part of the picture. The returned key material's shape (byte
135
- * lengths) is validated before trusting it, so a buggy KeyStore fails
136
- * clearly here rather than with a cryptic error the first time a signing
137
- * call happens to touch the bad bytes. `storageDir` is still required: the
138
- * trust store and audit log are a separate concern from where secret keys
139
- * live, unaffected by this function.
140
- */
141
- export async function loadIdentityFromKeyStore(options) {
142
- const { classical, pqcSign, pqcKem } = await options.keyStore.load();
143
- if (!isValidClassicalKeypairShape(classical))
144
- throw new Error("KeyStore returned a classical keypair with the wrong byte length(s) for P-384.");
145
- if (!isValidPqcSignKeypairShape(pqcSign))
146
- throw new Error("KeyStore returned a pqcSign keypair with the wrong byte length(s) for ML-DSA-87.");
147
- if (!isValidPqcKemKeypairShape(pqcKem))
148
- throw new Error("KeyStore returned a pqcKem keypair with the wrong byte length(s) for ML-KEM-1024.");
149
- return { alias: options.alias, storageDir: options.storageDir, classical, pqcSign, pqcKem };
150
- }
151
- /**
152
- * Best-effort wipe of the secret key bytes held by `identity`: overwrites
153
- * them with zeros so they do not linger in this process's memory after you
154
- * are done. The identity is unusable afterwards. Limits: JavaScript gives no
155
- * guarantee about copies the engine or GC already made, and the hex strings
156
- * produced while saving or loading an identity are immutable and cannot be
157
- * wiped. This narrows the exposure window; it is not a secure-erase.
158
- */
159
- export function zeroizeIdentity(identity) {
160
- identity.classical.secretKey.fill(0);
161
- identity.pqcSign.secretKey.fill(0);
162
- identity.pqcKem.secretKey.fill(0);
163
- }
164
78
  /** The shareable public half of an identity — hand this to the other party out of band. */
165
79
  export function exportPublicIdentity(identity) {
166
80
  return {
@@ -170,17 +84,4 @@ export function exportPublicIdentity(identity) {
170
84
  pqcKemPublicKey: bytesToHex(identity.pqcKem.publicKey),
171
85
  };
172
86
  }
173
- /**
174
- * A self-signed, optionally time-limited version of `exportPublicIdentity`'s
175
- * card (ADR 0023/issue #37): the identity signs its own card, so a peer can
176
- * verify it wasn't tampered with and — if `options.expiresAt` was set —
177
- * reject it once expired, enforcing a cryptoperiod the bare card has no way
178
- * to carry (it has no signature at all). `exportPublicIdentity` itself is
179
- * unchanged; this is a separate, opt-in function precisely so every
180
- * existing caller of the bare card is unaffected. Verify and trust a
181
- * received one with `trustSignedCard`, not `trustParty`.
182
- */
183
- export async function exportSignedCard(identity, options = {}) {
184
- return signCore(identity, exportPublicIdentity(identity), options);
185
- }
186
87
  //# sourceMappingURL=identity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  export { canonicalize, hashDocument } from "./canonicalJson.js";
2
2
  export { fingerprint } from "./fingerprint.js";
3
- export { decryptFrom, DecryptionError, encryptFor, ReplayedBoxError, sign, UntrustedPartyError, verify } from "./crypto/nodeHybrid.js";
4
- export type { BoxReplayInfo, Certificate, EncryptedBox, VerifyFailureReason, VerifyResult } from "./crypto/nodeHybrid.js";
5
- export { createIdentity, exportPublicIdentity, exportSignedCard, loadIdentity, loadIdentityFromKeyStore, MIN_PASSPHRASE_LENGTH, zeroizeIdentity, } from "./identity.js";
6
- export type { CreateIdentityOptions, Identity, KeyStore, LoadIdentityFromKeyStoreOptions, LoadIdentityOptions, PublicCard } from "./identity.js";
7
- export type { PassphraseChecker } from "./passphrase.js";
8
- export { migrateAuditLogToEncrypted, readLog, verifyLogIntegrity } from "./store.js";
3
+ export { decryptFrom, encryptFor, sign, UntrustedPartyError, verify } from "./crypto/nodeHybrid.js";
4
+ export type { Certificate, EncryptedBox, VerifyFailureReason, VerifyResult } from "./crypto/nodeHybrid.js";
5
+ export { createIdentity, exportPublicIdentity, loadIdentity, MIN_PASSPHRASE_LENGTH } from "./identity.js";
6
+ export type { CreateIdentityOptions, Identity, LoadIdentityOptions, PublicCard } from "./identity.js";
7
+ export { readLog, verifyLogIntegrity } from "./store.js";
9
8
  export type { LogEntry, LogIntegrityResult } from "./store.js";
10
- export { applyRevocationNotice, applyRotation, beginRotation, completeRotation, createRevocationNotice, getTrustedParty, getTrustedPartiesByAlias, getTrustRecordsByAlias, listTrustedParties, listTrustRecords, migrateLegacyTrustStore, migrateTrustStoreToEncrypted, TrustStoreIntegrityError, trustParty, trustSignedCard, untrustFingerprint, untrustParty, } from "./trust.js";
11
- export type { RevocationDocument, RevocationNotice, RotationDocument, RotationStatement, TrustRecord } from "./trust.js";
9
+ export { getTrustedParty, getTrustedPartiesByAlias, getTrustRecordsByAlias, listTrustedParties, listTrustRecords, trustParty, untrustParty, } from "./trust.js";
10
+ export type { TrustRecord } from "./trust.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { canonicalize, hashDocument } from "./canonicalJson.js";
2
2
  export { fingerprint } from "./fingerprint.js";
3
- export { decryptFrom, DecryptionError, encryptFor, ReplayedBoxError, sign, UntrustedPartyError, verify } from "./crypto/nodeHybrid.js";
4
- export { createIdentity, exportPublicIdentity, exportSignedCard, loadIdentity, loadIdentityFromKeyStore, MIN_PASSPHRASE_LENGTH, zeroizeIdentity, } from "./identity.js";
5
- export { migrateAuditLogToEncrypted, readLog, verifyLogIntegrity } from "./store.js";
6
- export { applyRevocationNotice, applyRotation, beginRotation, completeRotation, createRevocationNotice, getTrustedParty, getTrustedPartiesByAlias, getTrustRecordsByAlias, listTrustedParties, listTrustRecords, migrateLegacyTrustStore, migrateTrustStoreToEncrypted, TrustStoreIntegrityError, trustParty, trustSignedCard, untrustFingerprint, untrustParty, } from "./trust.js";
3
+ export { decryptFrom, encryptFor, sign, UntrustedPartyError, verify } from "./crypto/nodeHybrid.js";
4
+ export { createIdentity, exportPublicIdentity, loadIdentity, MIN_PASSPHRASE_LENGTH } from "./identity.js";
5
+ export { readLog, verifyLogIntegrity } from "./store.js";
6
+ export { getTrustedParty, getTrustedPartiesByAlias, getTrustRecordsByAlias, listTrustedParties, listTrustRecords, trustParty, untrustParty, } from "./trust.js";
7
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,gBAAgB,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEvI,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,wBAAwB,EACxB,qBAAqB,EACrB,eAAe,GAChB,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErF,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAC5B,wBAAwB,EACxB,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,YAAY,GACb,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEpG,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE1G,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/store.d.ts CHANGED
@@ -6,60 +6,23 @@ export interface LogEntry {
6
6
  result?: string;
7
7
  at: string;
8
8
  }
9
- /**
10
- * Constant-time MAC comparison (ADR 0009/0019's chain and head MACs are both
11
- * HMAC-SHA384, so 96 lowercase hex chars). `candidate` is untrusted on-disk
12
- * data of unknown shape: a non-string, wrong-length, or non-hex value is
13
- * reported as a mismatch, never thrown, matching verifyKeyConfirmation's
14
- * contract in crypto/hybrid.ts.
15
- *
16
- * Exported (ADR 0026) so the timing-safety test can exercise this exact
17
- * compare in isolation, without the file I/O its only internal callers do.
18
- */
19
- export declare function macEquals(candidate: unknown, expected: string): boolean;
20
9
  /**
21
10
  * Appends one entry to the local, append-only audit log (public metadata
22
- * only — no secrets). Each entry is chained to the previous one's MAC
23
- * (see verifyLogIntegrity), keyed from identity's own secret material
24
- * (ADR 0009) so tampering or deleting past entries is detectable by anyone
25
- * EXCEPT someone who also holds that secret — not just theoretically
26
- * append-only, and not forgeable by file-write access alone.
11
+ * only — no secrets). Each entry is chained to the previous one's hash
12
+ * (see verifyLogIntegrity) so tampering or deleting past entries is
13
+ * detectable, not just theoretically append-only.
27
14
  */
28
15
  export declare function appendLogEntry(identity: Identity, entry: Omit<LogEntry, "at">): Promise<void>;
29
- /** Transparently decrypts any v2 (encrypted, issue #43) entries alongside plaintext v1 ones. */
30
16
  export declare function readLog(identity: Identity): Promise<LogEntry[]>;
31
17
  export type LogIntegrityResult = {
32
18
  intact: true;
33
19
  } | {
34
20
  intact: false;
35
- /** First entry whose chain link failed; for the reasons below, the number of entries actually present. */
36
21
  brokenAtIndex: number;
37
- /** Absent for an edited or deleted entry inside the chain. "truncated": entries at the end were removed. "head-missing" / "head-invalid": the head record that would show this is gone or not authentic. */
38
- reason?: "truncated" | "head-missing" | "head-invalid";
39
22
  };
40
23
  /**
41
- * Walks the MAC chain and reports whether it's intact. If an entry was
24
+ * Walks the hash chain and reports whether it's intact. If an entry was
42
25
  * edited or deleted, `brokenAtIndex` is the (0-based) index of the first
43
- * entry whose chain link no longer matches. Recomputing a matching MAC
44
- * requires identity's own secret material (see computeMac/deriveAuditLogKey)
45
- * — this is what makes forging the chain require more than filesystem
46
- * write access (ADR 0009). It also checks the authenticated head record
47
- * (ADR 0019), so deleting the most recent entries is reported too, with
48
- * `reason: "truncated"`.
49
- *
50
- * Not detectable: replacing BOTH log.jsonl and log.head.json with an older,
51
- * consistent pair, or deleting both files outright. Keep an off-box copy of
52
- * the head if that matters to you.
26
+ * entry whose chain link no longer matches.
53
27
  */
54
28
  export declare function verifyLogIntegrity(identity: Identity): Promise<LogIntegrityResult>;
55
- /**
56
- * One-time, explicit opt-in (issue #43/ADR 0027) to encrypt the audit log's
57
- * entry content, so filesystem access alone no longer reveals what actions
58
- * this identity has performed. Re-encrypts every existing entry in one pass
59
- * (there is no mixed-format file afterwards) and marks the head record so
60
- * every later `appendLogEntry` call encrypts just the one entry it writes —
61
- * never the whole log again. A no-op if the log is already encrypted.
62
- * Throws if the log fails its integrity check first: there is nothing safe
63
- * to migrate if the chain can't be trusted.
64
- */
65
- export declare function migrateAuditLogToEncrypted(identity: Identity): Promise<void>;
package/dist/store.js CHANGED
@@ -1,92 +1,14 @@
1
- import { gcm } from "@noble/ciphers/aes.js";
2
- import { equalBytes } from "@noble/ciphers/utils.js";
3
- import { hkdf } from "@noble/hashes/hkdf.js";
4
- import { hmac } from "@noble/hashes/hmac.js";
5
1
  import { sha384 } from "@noble/hashes/sha2.js";
6
- import { bytesToHex, concatBytes, hexToBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils.js";
2
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
7
3
  import { appendFile, mkdir, readFile } from "node:fs/promises";
8
- import { join, resolve } from "node:path";
4
+ import { join } from "node:path";
9
5
  import { canonicalize } from "./canonicalJson.js";
10
- import { writeFileAtomic } from "./fileCrypto.js";
11
- function isV2Entry(entry) {
12
- return entry.version === 2;
13
- }
14
- function stripChainFields(entry) {
15
- const { prevMac: _prevMac, mac: _mac, ...rest } = entry;
16
- return rest;
17
- }
18
6
  const GENESIS_HASH = "GENESIS";
19
- const AUDIT_LOG_HMAC_INFO = utf8ToBytes("raz-encrypt-audit-log-v1|hmac-key");
20
- // A separate key for the head record, so a head MAC can never be mistaken for an entry MAC (ADR 0019).
21
- const AUDIT_LOG_HEAD_INFO = utf8ToBytes("raz-encrypt-audit-log-v1|head-hmac-key");
22
- // A separate key from both MAC keys above, same reasoning (ADR 0027) — never derived from a passphrase, since appendLogEntry/readLog only ever receive an Identity.
23
- const AUDIT_LOG_ENC_INFO = utf8ToBytes("raz-encrypt-audit-log-v1|enc-key");
24
7
  function logFilePath(storageDir) {
25
8
  return join(storageDir, "log.jsonl");
26
9
  }
27
- function headFilePath(storageDir) {
28
- return join(storageDir, "log.head.json");
29
- }
30
- /**
31
- * HKDF-derived from BOTH the classical and PQC signing secrets (concatenated,
32
- * same composition pattern as hybrid.ts's deriveHybridKey) — matching this
33
- * library's hybrid posture everywhere else: compromising either secret key
34
- * alone must not be enough to forge the chain. Recomputed fresh on every
35
- * call, never persisted (ADR 0009).
36
- */
37
- function deriveAuditLogKey(identity) {
38
- return hkdf(sha384, concatBytes(identity.classical.secretKey, identity.pqcSign.secretKey), undefined, AUDIT_LOG_HMAC_INFO, 32);
39
- }
40
- /**
41
- * Keyed with deriveAuditLogKey — plain SHA-384 (ADR 0009's rejected
42
- * approach) lets anyone with filesystem write access to log.jsonl forge the
43
- * whole chain with no knowledge of the Identity's secret material at all.
44
- * HMAC means forging it requires that secret. Generic over the entry's own
45
- * shape (v1 plaintext fields, or v2's `{version,nonce,ciphertext}` — issue
46
- * #43) since `canonicalize` doesn't care which fields are present; the chain
47
- * mechanism itself never changes, only the bytes it authenticates.
48
- */
49
- function computeMac(identity, entry) {
50
- return bytesToHex(hmac(sha384, deriveAuditLogKey(identity), utf8ToBytes(canonicalize(entry))));
51
- }
52
- /** Same derivation shape as deriveAuditLogKey, under its own info label (ADR 0027). */
53
- function deriveAuditLogEncKey(identity) {
54
- return hkdf(sha384, concatBytes(identity.classical.secretKey, identity.pqcSign.secretKey), undefined, AUDIT_LOG_ENC_INFO, 32);
55
- }
56
- /** Associated data for an entry's content AEAD: binds the ciphertext to its format version and its own nonce, same pattern as fileCrypto.ts's headerAad. */
57
- function entryAad(nonce) {
58
- return utf8ToBytes(canonicalize({ version: 2, nonce }));
59
- }
60
- function encryptEntryContent(identity, content) {
61
- const nonceBytes = randomBytes(12);
62
- const nonce = bytesToHex(nonceBytes);
63
- const ciphertext = gcm(deriveAuditLogEncKey(identity), nonceBytes, entryAad(nonce)).encrypt(utf8ToBytes(canonicalize(content)));
64
- return { nonce, ciphertext: bytesToHex(ciphertext) };
65
- }
66
- /** Throws (never returns something un-decrypted) if the entry doesn't authenticate under this identity's key. */
67
- function decryptEntryContent(identity, entry) {
68
- try {
69
- const plaintext = gcm(deriveAuditLogEncKey(identity), hexToBytes(entry.nonce), entryAad(entry.nonce)).decrypt(hexToBytes(entry.ciphertext));
70
- return JSON.parse(new TextDecoder().decode(plaintext));
71
- }
72
- catch (err) {
73
- throw new Error(`Audit log entry failed to decrypt: ${err instanceof Error ? err.message : String(err)}`);
74
- }
75
- }
76
- /**
77
- * Constant-time MAC comparison (ADR 0009/0019's chain and head MACs are both
78
- * HMAC-SHA384, so 96 lowercase hex chars). `candidate` is untrusted on-disk
79
- * data of unknown shape: a non-string, wrong-length, or non-hex value is
80
- * reported as a mismatch, never thrown, matching verifyKeyConfirmation's
81
- * contract in crypto/hybrid.ts.
82
- *
83
- * Exported (ADR 0026) so the timing-safety test can exercise this exact
84
- * compare in isolation, without the file I/O its only internal callers do.
85
- */
86
- export function macEquals(candidate, expected) {
87
- if (typeof candidate !== "string" || !/^[0-9a-f]{96}$/.test(candidate))
88
- return false;
89
- return equalBytes(hexToBytes(candidate), hexToBytes(expected));
10
+ function computeHash(entry) {
11
+ return bytesToHex(sha384(utf8ToBytes(canonicalize(entry))));
90
12
  }
91
13
  async function readStoredEntries(storageDir) {
92
14
  try {
@@ -94,15 +16,7 @@ async function readStoredEntries(storageDir) {
94
16
  return raw
95
17
  .split("\n")
96
18
  .filter((line) => line.trim().length > 0)
97
- .map((line) => {
98
- const parsed = JSON.parse(line);
99
- // A corrupted line can be valid JSON that isn't an object (e.g.
100
- // `null`) — destructuring that downstream throws TypeError instead
101
- // of being reported as a broken chain link. Coerce to an empty
102
- // object so it flows safely through as a non-matching entry (found
103
- // by fuzzing, issue #42).
104
- return (typeof parsed === "object" && parsed !== null ? parsed : {});
105
- });
19
+ .map((line) => JSON.parse(line));
106
20
  }
107
21
  catch (err) {
108
22
  if (err.code === "ENOENT")
@@ -110,185 +24,39 @@ async function readStoredEntries(storageDir) {
110
24
  throw err;
111
25
  }
112
26
  }
113
- function computeHeadMac(identity, head) {
114
- const key = hkdf(sha384, concatBytes(identity.classical.secretKey, identity.pqcSign.secretKey), undefined, AUDIT_LOG_HEAD_INFO, 32);
115
- return bytesToHex(hmac(sha384, key, utf8ToBytes(canonicalize(head))));
116
- }
117
- async function readHead(identity) {
118
- let raw;
119
- try {
120
- raw = await readFile(headFilePath(identity.storageDir), "utf8");
121
- }
122
- catch (err) {
123
- if (err.code === "ENOENT")
124
- return { state: "missing" };
125
- throw err;
126
- }
127
- try {
128
- const parsed = JSON.parse(raw);
129
- const { headMac, ...rest } = parsed;
130
- if (rest.version !== 1 ||
131
- typeof rest.count !== "number" ||
132
- !Number.isSafeInteger(rest.count) ||
133
- rest.count < 0 ||
134
- typeof rest.mac !== "string" ||
135
- typeof headMac !== "string" ||
136
- (rest.encrypted !== undefined && rest.encrypted !== true)) {
137
- return { state: "invalid" };
138
- }
139
- // Only ever include `encrypted` when true, exactly as writeHead does —
140
- // otherwise a legacy head record's canonicalized bytes (and therefore
141
- // its headMac) would silently change underneath it.
142
- const head = rest.encrypted === true ? { version: 1, count: rest.count, mac: rest.mac, encrypted: true } : { version: 1, count: rest.count, mac: rest.mac };
143
- return macEquals(headMac, computeHeadMac(identity, head)) ? { state: "ok", head } : { state: "invalid" };
144
- }
145
- catch {
146
- return { state: "invalid" };
147
- }
148
- }
149
- /** Compares an already-read head against the entries on disk. Returns undefined when they agree (a head up to a few entries behind is fine: that is a crash between append and head update). */
150
- function checkHeadAgainst(read, entries) {
151
- if (read.state === "missing")
152
- return entries.length === 0 ? undefined : { reason: "head-missing", brokenAtIndex: entries.length };
153
- if (read.state === "invalid")
154
- return { reason: "head-invalid", brokenAtIndex: entries.length };
155
- const { count, mac } = read.head;
156
- if (count > entries.length)
157
- return { reason: "truncated", brokenAtIndex: entries.length };
158
- const expected = count === 0 ? GENESIS_HASH : entries[count - 1].mac;
159
- if (mac !== expected)
160
- return { reason: "head-invalid", brokenAtIndex: entries.length };
161
- return undefined;
162
- }
163
- async function checkHead(identity, entries) {
164
- return checkHeadAgainst(await readHead(identity), entries);
165
- }
166
- async function writeHead(identity, count, mac, encrypted) {
167
- const head = encrypted ? { version: 1, count, mac, encrypted: true } : { version: 1, count, mac };
168
- await writeFileAtomic(headFilePath(identity.storageDir), JSON.stringify({ ...head, headMac: computeHeadMac(identity, head) }));
169
- }
170
- const appendState = new Map();
171
- function stateFor(storageDir) {
172
- const key = resolve(storageDir);
173
- let state = appendState.get(key);
174
- if (!state) {
175
- state = { queue: Promise.resolve(), tail: null, headTrusted: true, encrypted: false };
176
- appendState.set(key, state);
177
- }
178
- return state;
179
- }
180
- async function doAppend(identity, entry, state) {
181
- const full = { ...entry, at: new Date().toISOString() };
182
- if (!state.tail) {
183
- const existing = await readStoredEntries(identity.storageDir);
184
- const head = await readHead(identity);
185
- state.tail = { mac: existing.length > 0 ? existing[existing.length - 1].mac : GENESIS_HASH, entryCount: existing.length };
186
- state.headTrusted = checkHeadAgainst(head, existing) === undefined;
187
- state.encrypted = head.state === "ok" && head.head.encrypted === true;
188
- }
189
- const content = state.encrypted ? { version: 2, ...encryptEntryContent(identity, full) } : { ...full };
190
- const withPrev = { ...content, prevMac: state.tail.mac };
191
- const mac = computeMac(identity, withPrev);
192
- const stored = { ...withPrev, mac };
193
- await mkdir(identity.storageDir, { recursive: true, mode: 0o700 });
194
- await appendFile(logFilePath(identity.storageDir), JSON.stringify(stored) + "\n", { mode: 0o600 });
195
- state.tail = { mac, entryCount: state.tail.entryCount + 1 };
196
- // Never overwrite a head that already disagreed with the log: that would
197
- // launder a truncation into a "clean" state.
198
- if (state.headTrusted)
199
- await writeHead(identity, state.tail.entryCount, mac, state.encrypted);
200
- }
201
27
  /**
202
28
  * Appends one entry to the local, append-only audit log (public metadata
203
- * only — no secrets). Each entry is chained to the previous one's MAC
204
- * (see verifyLogIntegrity), keyed from identity's own secret material
205
- * (ADR 0009) so tampering or deleting past entries is detectable by anyone
206
- * EXCEPT someone who also holds that secret — not just theoretically
207
- * append-only, and not forgeable by file-write access alone.
29
+ * only — no secrets). Each entry is chained to the previous one's hash
30
+ * (see verifyLogIntegrity) so tampering or deleting past entries is
31
+ * detectable, not just theoretically append-only.
208
32
  */
209
33
  export async function appendLogEntry(identity, entry) {
210
- const state = stateFor(identity.storageDir);
211
- // Chain onto the previous operation regardless of how it settled (a past
212
- // failure must not permanently block future appends to this storageDir),
213
- // but this call's own outcome — via `task`, awaited below — propagates to
214
- // ITS caller untouched.
215
- const task = state.queue.catch(() => { }).then(() => doAppend(identity, entry, state));
216
- state.queue = task.catch(() => { });
217
- await task;
34
+ const full = { ...entry, at: new Date().toISOString() };
35
+ const existing = await readStoredEntries(identity.storageDir);
36
+ const prevHash = existing.length > 0 ? existing[existing.length - 1].hash : GENESIS_HASH;
37
+ const withPrev = { ...full, prevHash };
38
+ const stored = { ...withPrev, hash: computeHash(withPrev) };
39
+ await mkdir(identity.storageDir, { recursive: true });
40
+ await appendFile(logFilePath(identity.storageDir), JSON.stringify(stored) + "\n", { mode: 0o600 });
218
41
  }
219
- /** Transparently decrypts any v2 (encrypted, issue #43) entries alongside plaintext v1 ones. */
220
42
  export async function readLog(identity) {
221
- return (await readStoredEntries(identity.storageDir)).map((entry) => (isV2Entry(entry) ? decryptEntryContent(identity, entry) : stripChainFields(entry)));
43
+ return (await readStoredEntries(identity.storageDir)).map(({ prevHash: _prevHash, hash: _hash, ...entry }) => entry);
222
44
  }
223
45
  /**
224
- * Walks the MAC chain and reports whether it's intact. If an entry was
46
+ * Walks the hash chain and reports whether it's intact. If an entry was
225
47
  * edited or deleted, `brokenAtIndex` is the (0-based) index of the first
226
- * entry whose chain link no longer matches. Recomputing a matching MAC
227
- * requires identity's own secret material (see computeMac/deriveAuditLogKey)
228
- * — this is what makes forging the chain require more than filesystem
229
- * write access (ADR 0009). It also checks the authenticated head record
230
- * (ADR 0019), so deleting the most recent entries is reported too, with
231
- * `reason: "truncated"`.
232
- *
233
- * Not detectable: replacing BOTH log.jsonl and log.head.json with an older,
234
- * consistent pair, or deleting both files outright. Keep an off-box copy of
235
- * the head if that matters to you.
48
+ * entry whose chain link no longer matches.
236
49
  */
237
50
  export async function verifyLogIntegrity(identity) {
238
51
  const entries = await readStoredEntries(identity.storageDir);
239
- let expectedPrevMac = GENESIS_HASH;
52
+ let expectedPrevHash = GENESIS_HASH;
240
53
  for (let i = 0; i < entries.length; i++) {
241
- const { prevMac, mac, ...rest } = entries[i];
242
- if (prevMac !== expectedPrevMac || !macEquals(mac, computeMac(identity, { ...rest, prevMac }))) {
54
+ const { prevHash, hash, ...rest } = entries[i];
55
+ if (prevHash !== expectedPrevHash || hash !== computeHash({ ...rest, prevHash })) {
243
56
  return { intact: false, brokenAtIndex: i };
244
57
  }
245
- expectedPrevMac = mac;
58
+ expectedPrevHash = hash;
246
59
  }
247
- const problem = await checkHead(identity, entries);
248
- if (problem)
249
- return { intact: false, ...problem };
250
60
  return { intact: true };
251
61
  }
252
- /**
253
- * One-time, explicit opt-in (issue #43/ADR 0027) to encrypt the audit log's
254
- * entry content, so filesystem access alone no longer reveals what actions
255
- * this identity has performed. Re-encrypts every existing entry in one pass
256
- * (there is no mixed-format file afterwards) and marks the head record so
257
- * every later `appendLogEntry` call encrypts just the one entry it writes —
258
- * never the whole log again. A no-op if the log is already encrypted.
259
- * Throws if the log fails its integrity check first: there is nothing safe
260
- * to migrate if the chain can't be trusted.
261
- */
262
- export async function migrateAuditLogToEncrypted(identity) {
263
- const state = stateFor(identity.storageDir);
264
- const task = state.queue.catch(() => { }).then(() => doMigrateToEncrypted(identity, state));
265
- state.queue = task.catch(() => { });
266
- await task;
267
- }
268
- async function doMigrateToEncrypted(identity, state) {
269
- const head = await readHead(identity);
270
- if (head.state === "ok" && head.head.encrypted === true)
271
- return; // already encrypted — true no-op
272
- const integrity = await verifyLogIntegrity(identity);
273
- if (!integrity.intact) {
274
- throw new Error("Cannot migrate: the audit log failed its integrity check. Investigate before encrypting it.");
275
- }
276
- const entries = await readStoredEntries(identity.storageDir);
277
- let prevMac = GENESIS_HASH;
278
- const rewritten = [];
279
- for (const entry of entries) {
280
- const content = isV2Entry(entry) ? decryptEntryContent(identity, entry) : stripChainFields(entry);
281
- const { nonce, ciphertext } = encryptEntryContent(identity, content);
282
- const withPrev = { version: 2, nonce, ciphertext, prevMac };
283
- const mac = computeMac(identity, withPrev);
284
- rewritten.push({ ...withPrev, mac });
285
- prevMac = mac;
286
- }
287
- await mkdir(identity.storageDir, { recursive: true, mode: 0o700 });
288
- await writeFileAtomic(logFilePath(identity.storageDir), rewritten.map((entry) => JSON.stringify(entry)).join("\n") + (rewritten.length > 0 ? "\n" : ""));
289
- await writeHead(identity, rewritten.length, prevMac, true);
290
- state.tail = { mac: prevMac, entryCount: rewritten.length };
291
- state.headTrusted = true;
292
- state.encrypted = true;
293
- }
294
62
  //# sourceMappingURL=store.js.map