@blamejs/core 0.7.18 → 0.7.20
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/CHANGELOG.md +427 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +350 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +399 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/fetch-metadata.js +129 -0
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +85 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +121 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +7 -5
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/backup/crypto.js
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* backup-crypto — passphrase-based symmetric crypto for backup files.
|
|
4
|
-
*
|
|
5
|
-
* The primitive layer used by lib/backup.js and the backup/restore
|
|
6
|
-
* workers. Argon2id key derivation + XChaCha20-Poly1305 AEAD. Salt is
|
|
7
|
-
* managed alongside the ciphertext so backups carry their KDF input
|
|
8
|
-
* with them — restoring a backup needs the bundle's salt + the
|
|
9
|
-
* operator passphrase, nothing else.
|
|
10
|
-
*
|
|
11
|
-
* var bc = b.backupCrypto;
|
|
12
|
-
*
|
|
13
|
-
* bc.ARGON2_OPTS // baseline KDF parameters
|
|
14
|
-
*
|
|
15
|
-
* await bc.deriveKey(passphrase, saltHex)
|
|
16
|
-
* → Buffer(32) // raw key bytes for AEAD
|
|
17
|
-
*
|
|
18
|
-
* await bc.encryptWithPassphrase(plain, passphrase, saltHex)
|
|
19
|
-
* → Buffer (24-byte nonce + ciphertext+tag)
|
|
20
|
-
* await bc.decryptWithPassphrase(buf, passphrase, saltHex)
|
|
21
|
-
* → Buffer (plaintext)
|
|
22
|
-
*
|
|
23
|
-
* await bc.encryptWithFreshSalt(plain, passphrase)
|
|
24
|
-
* → { encrypted: Buffer, salt: hexString }
|
|
25
|
-
*
|
|
26
|
-
* bc.checksum(buf) // sha3-512 hex; bundle integrity
|
|
27
|
-
*
|
|
28
|
-
* Argon2id parameters match lib/vault-wrap defaults — backup integrity
|
|
29
|
-
* tracks the framework's at-rest discipline. Operators with stricter
|
|
30
|
-
* requirements (longer passphrases, higher memoryCost) bypass these
|
|
31
|
-
* defaults by passing a full Argon2 opts object to deriveKey.
|
|
32
|
-
*
|
|
33
|
-
* Algorithm choices are deliberate and locked:
|
|
34
|
-
* - KDF: Argon2id (RFC 9106) — memory-hard, the only KDF the
|
|
35
|
-
* framework uses for password-derived keys
|
|
36
|
-
* - AEAD: XChaCha20-Poly1305 — 24-byte random nonce per message
|
|
37
|
-
* means we never need to track per-passphrase nonce counters
|
|
38
|
-
*
|
|
39
|
-
* Operators with non-standard cipher needs build their own primitive;
|
|
40
|
-
* this one's whole point is "do the framework's PQC-aligned crypto
|
|
41
|
-
* correctly without per-call decisions".
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
var nodeCrypto = require("node:crypto");
|
|
45
|
-
var C = require("../constants");
|
|
46
|
-
var safeBuffer = require("../safe-buffer");
|
|
47
|
-
var { xchacha20poly1305 } = require("../vendor/noble-ciphers.cjs");
|
|
48
|
-
var argon2 = require("../vendor/argon2");
|
|
49
|
-
var { FrameworkError } = require("../framework-error");
|
|
50
|
-
|
|
51
|
-
class BackupCryptoError extends FrameworkError {
|
|
52
|
-
constructor(code, message) {
|
|
53
|
-
super(message, code);
|
|
54
|
-
this.name = "BackupCryptoError";
|
|
55
|
-
this.permanent = true;
|
|
56
|
-
this.isBackupCryptoError = true;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Baseline Argon2id parameters. Identical to vault-wrap's defaults so
|
|
61
|
-
// operators see one set of numbers across the framework's password-
|
|
62
|
-
// derivation surfaces. Tuned for: ~64 MiB memory, 3 passes, 4 lanes,
|
|
63
|
-
// 32-byte raw output (matches XChaCha20-Poly1305 key length).
|
|
64
|
-
var ARGON2_OPTS = Object.freeze({
|
|
65
|
-
type: 2, // 2 = argon2id
|
|
66
|
-
memoryCost: C.BYTES.kib(64), // 64 MiB expressed in argon2's KiB units
|
|
67
|
-
timeCost: 3,
|
|
68
|
-
parallelism: 4,
|
|
69
|
-
hashLength: C.BYTES.bytes(32), // matches XChaCha20-Poly1305 key length
|
|
70
|
-
raw: true,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
var SALT_BYTES = C.BYTES.bytes(32); // 256 bits — comfortable margin for global uniqueness
|
|
74
|
-
var NONCE_BYTES = C.BYTES.bytes(24); // XChaCha20 nonce length
|
|
75
|
-
|
|
76
|
-
function checksum(buf) {
|
|
77
|
-
if (!Buffer.isBuffer(buf) && typeof buf !== "string") {
|
|
78
|
-
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
79
|
-
"checksum: argument must be a Buffer or string");
|
|
80
|
-
}
|
|
81
|
-
return nodeCrypto.createHash("sha3-512").update(buf).digest("hex");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function _validateSaltHex(saltHex) {
|
|
85
|
-
if (!safeBuffer.isHex(saltHex) || saltHex.length % 2 !== 0) {
|
|
86
|
-
throw new BackupCryptoError("backup-crypto/bad-salt",
|
|
87
|
-
"saltHex must be a non-empty hex string with even length");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function _validatePassphrase(p) {
|
|
92
|
-
if (!Buffer.isBuffer(p) && typeof p !== "string") {
|
|
93
|
-
throw new BackupCryptoError("backup-crypto/bad-passphrase",
|
|
94
|
-
"passphrase must be a Buffer or string");
|
|
95
|
-
}
|
|
96
|
-
if (Buffer.isBuffer(p) ? p.length === 0 : p.length === 0) {
|
|
97
|
-
throw new BackupCryptoError("backup-crypto/bad-passphrase",
|
|
98
|
-
"passphrase must be non-empty");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function deriveKey(passphrase, saltHex, opts) {
|
|
103
|
-
_validatePassphrase(passphrase);
|
|
104
|
-
_validateSaltHex(saltHex);
|
|
105
|
-
var argonOpts = Object.assign({}, ARGON2_OPTS, opts || {}, {
|
|
106
|
-
salt: Buffer.from(saltHex, "hex"),
|
|
107
|
-
});
|
|
108
|
-
var hash = await argon2.hash(passphrase, argonOpts);
|
|
109
|
-
// Argon2 raw mode returns a Buffer of hashLength bytes. Defensive
|
|
110
|
-
// length check — better to fail loudly than ship a short key into
|
|
111
|
-
// XChaCha20 and trip a later cryptic error.
|
|
112
|
-
if (!Buffer.isBuffer(hash) || hash.length !== ARGON2_OPTS.hashLength) {
|
|
113
|
-
throw new BackupCryptoError("backup-crypto/derive-failed",
|
|
114
|
-
"argon2 hash returned unexpected output (expected " + ARGON2_OPTS.hashLength +
|
|
115
|
-
"-byte Buffer, got " + (hash && hash.length) + ")");
|
|
116
|
-
}
|
|
117
|
-
return hash;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function encryptWithPassphrase(plaintext, passphrase, saltHex) {
|
|
121
|
-
if (!Buffer.isBuffer(plaintext) && typeof plaintext !== "string") {
|
|
122
|
-
throw new BackupCryptoError("backup-crypto/bad-plaintext",
|
|
123
|
-
"encryptWithPassphrase: plaintext must be a Buffer or string");
|
|
124
|
-
}
|
|
125
|
-
var plainBuf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, "utf8");
|
|
126
|
-
var key = await deriveKey(passphrase, saltHex);
|
|
127
|
-
var nonce = nodeCrypto.randomBytes(NONCE_BYTES);
|
|
128
|
-
var ct = xchacha20poly1305(new Uint8Array(key), nonce).encrypt(new Uint8Array(plainBuf));
|
|
129
|
-
return Buffer.concat([nonce, Buffer.from(ct)]);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
|
|
133
|
-
if (!Buffer.isBuffer(encrypted)) {
|
|
134
|
-
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
135
|
-
"decryptWithPassphrase: encrypted must be a Buffer");
|
|
136
|
-
}
|
|
137
|
-
if (encrypted.length <= NONCE_BYTES) {
|
|
138
|
-
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
139
|
-
"decryptWithPassphrase: encrypted buffer is too short to contain nonce + tag");
|
|
140
|
-
}
|
|
141
|
-
var key = await deriveKey(passphrase, saltHex);
|
|
142
|
-
var nonce = encrypted.subarray(0, NONCE_BYTES);
|
|
143
|
-
var ct = encrypted.subarray(NONCE_BYTES);
|
|
144
|
-
var plain;
|
|
145
|
-
try {
|
|
146
|
-
plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce))
|
|
147
|
-
.decrypt(new Uint8Array(ct));
|
|
148
|
-
} catch (e) {
|
|
149
|
-
throw new BackupCryptoError("backup-crypto/decrypt-failed",
|
|
150
|
-
"XChaCha20-Poly1305 decryption failed (wrong passphrase or tampered ciphertext): " +
|
|
151
|
-
((e && e.message) || String(e)));
|
|
152
|
-
}
|
|
153
|
-
return Buffer.from(plain);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Convenience for the common "encrypt this with a fresh salt" pattern.
|
|
157
|
-
// Returns the salt as hex so callers can store it alongside the
|
|
158
|
-
// ciphertext in the bundle manifest.
|
|
159
|
-
async function encryptWithFreshSalt(plaintext, passphrase) {
|
|
160
|
-
var salt = nodeCrypto.randomBytes(SALT_BYTES);
|
|
161
|
-
var saltHex = salt.toString("hex");
|
|
162
|
-
var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex);
|
|
163
|
-
return { encrypted: encrypted, salt: saltHex };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
module.exports = {
|
|
167
|
-
deriveKey: deriveKey,
|
|
168
|
-
encryptWithPassphrase: encryptWithPassphrase,
|
|
169
|
-
decryptWithPassphrase: decryptWithPassphrase,
|
|
170
|
-
encryptWithFreshSalt: encryptWithFreshSalt,
|
|
171
|
-
checksum: checksum,
|
|
172
|
-
ARGON2_OPTS: ARGON2_OPTS,
|
|
173
|
-
SALT_BYTES: SALT_BYTES,
|
|
174
|
-
NONCE_BYTES: NONCE_BYTES,
|
|
175
|
-
BackupCryptoError: BackupCryptoError,
|
|
176
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* backup-crypto — passphrase-based symmetric crypto for backup files.
|
|
4
|
+
*
|
|
5
|
+
* The primitive layer used by lib/backup.js and the backup/restore
|
|
6
|
+
* workers. Argon2id key derivation + XChaCha20-Poly1305 AEAD. Salt is
|
|
7
|
+
* managed alongside the ciphertext so backups carry their KDF input
|
|
8
|
+
* with them — restoring a backup needs the bundle's salt + the
|
|
9
|
+
* operator passphrase, nothing else.
|
|
10
|
+
*
|
|
11
|
+
* var bc = b.backupCrypto;
|
|
12
|
+
*
|
|
13
|
+
* bc.ARGON2_OPTS // baseline KDF parameters
|
|
14
|
+
*
|
|
15
|
+
* await bc.deriveKey(passphrase, saltHex)
|
|
16
|
+
* → Buffer(32) // raw key bytes for AEAD
|
|
17
|
+
*
|
|
18
|
+
* await bc.encryptWithPassphrase(plain, passphrase, saltHex)
|
|
19
|
+
* → Buffer (24-byte nonce + ciphertext+tag)
|
|
20
|
+
* await bc.decryptWithPassphrase(buf, passphrase, saltHex)
|
|
21
|
+
* → Buffer (plaintext)
|
|
22
|
+
*
|
|
23
|
+
* await bc.encryptWithFreshSalt(plain, passphrase)
|
|
24
|
+
* → { encrypted: Buffer, salt: hexString }
|
|
25
|
+
*
|
|
26
|
+
* bc.checksum(buf) // sha3-512 hex; bundle integrity
|
|
27
|
+
*
|
|
28
|
+
* Argon2id parameters match lib/vault-wrap defaults — backup integrity
|
|
29
|
+
* tracks the framework's at-rest discipline. Operators with stricter
|
|
30
|
+
* requirements (longer passphrases, higher memoryCost) bypass these
|
|
31
|
+
* defaults by passing a full Argon2 opts object to deriveKey.
|
|
32
|
+
*
|
|
33
|
+
* Algorithm choices are deliberate and locked:
|
|
34
|
+
* - KDF: Argon2id (RFC 9106) — memory-hard, the only KDF the
|
|
35
|
+
* framework uses for password-derived keys
|
|
36
|
+
* - AEAD: XChaCha20-Poly1305 — 24-byte random nonce per message
|
|
37
|
+
* means we never need to track per-passphrase nonce counters
|
|
38
|
+
*
|
|
39
|
+
* Operators with non-standard cipher needs build their own primitive;
|
|
40
|
+
* this one's whole point is "do the framework's PQC-aligned crypto
|
|
41
|
+
* correctly without per-call decisions".
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var nodeCrypto = require("node:crypto");
|
|
45
|
+
var C = require("../constants");
|
|
46
|
+
var safeBuffer = require("../safe-buffer");
|
|
47
|
+
var { xchacha20poly1305 } = require("../vendor/noble-ciphers.cjs");
|
|
48
|
+
var argon2 = require("../vendor/argon2");
|
|
49
|
+
var { FrameworkError } = require("../framework-error");
|
|
50
|
+
|
|
51
|
+
class BackupCryptoError extends FrameworkError {
|
|
52
|
+
constructor(code, message) {
|
|
53
|
+
super(message, code);
|
|
54
|
+
this.name = "BackupCryptoError";
|
|
55
|
+
this.permanent = true;
|
|
56
|
+
this.isBackupCryptoError = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Baseline Argon2id parameters. Identical to vault-wrap's defaults so
|
|
61
|
+
// operators see one set of numbers across the framework's password-
|
|
62
|
+
// derivation surfaces. Tuned for: ~64 MiB memory, 3 passes, 4 lanes,
|
|
63
|
+
// 32-byte raw output (matches XChaCha20-Poly1305 key length).
|
|
64
|
+
var ARGON2_OPTS = Object.freeze({
|
|
65
|
+
type: 2, // 2 = argon2id
|
|
66
|
+
memoryCost: C.BYTES.kib(64), // 64 MiB expressed in argon2's KiB units
|
|
67
|
+
timeCost: 3,
|
|
68
|
+
parallelism: 4,
|
|
69
|
+
hashLength: C.BYTES.bytes(32), // matches XChaCha20-Poly1305 key length
|
|
70
|
+
raw: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
var SALT_BYTES = C.BYTES.bytes(32); // 256 bits — comfortable margin for global uniqueness
|
|
74
|
+
var NONCE_BYTES = C.BYTES.bytes(24); // XChaCha20 nonce length
|
|
75
|
+
|
|
76
|
+
function checksum(buf) {
|
|
77
|
+
if (!Buffer.isBuffer(buf) && typeof buf !== "string") {
|
|
78
|
+
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
79
|
+
"checksum: argument must be a Buffer or string");
|
|
80
|
+
}
|
|
81
|
+
return nodeCrypto.createHash("sha3-512").update(buf).digest("hex");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _validateSaltHex(saltHex) {
|
|
85
|
+
if (!safeBuffer.isHex(saltHex) || saltHex.length % 2 !== 0) {
|
|
86
|
+
throw new BackupCryptoError("backup-crypto/bad-salt",
|
|
87
|
+
"saltHex must be a non-empty hex string with even length");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _validatePassphrase(p) {
|
|
92
|
+
if (!Buffer.isBuffer(p) && typeof p !== "string") {
|
|
93
|
+
throw new BackupCryptoError("backup-crypto/bad-passphrase",
|
|
94
|
+
"passphrase must be a Buffer or string");
|
|
95
|
+
}
|
|
96
|
+
if (Buffer.isBuffer(p) ? p.length === 0 : p.length === 0) {
|
|
97
|
+
throw new BackupCryptoError("backup-crypto/bad-passphrase",
|
|
98
|
+
"passphrase must be non-empty");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function deriveKey(passphrase, saltHex, opts) {
|
|
103
|
+
_validatePassphrase(passphrase);
|
|
104
|
+
_validateSaltHex(saltHex);
|
|
105
|
+
var argonOpts = Object.assign({}, ARGON2_OPTS, opts || {}, {
|
|
106
|
+
salt: Buffer.from(saltHex, "hex"),
|
|
107
|
+
});
|
|
108
|
+
var hash = await argon2.hash(passphrase, argonOpts);
|
|
109
|
+
// Argon2 raw mode returns a Buffer of hashLength bytes. Defensive
|
|
110
|
+
// length check — better to fail loudly than ship a short key into
|
|
111
|
+
// XChaCha20 and trip a later cryptic error.
|
|
112
|
+
if (!Buffer.isBuffer(hash) || hash.length !== ARGON2_OPTS.hashLength) {
|
|
113
|
+
throw new BackupCryptoError("backup-crypto/derive-failed",
|
|
114
|
+
"argon2 hash returned unexpected output (expected " + ARGON2_OPTS.hashLength +
|
|
115
|
+
"-byte Buffer, got " + (hash && hash.length) + ")");
|
|
116
|
+
}
|
|
117
|
+
return hash;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function encryptWithPassphrase(plaintext, passphrase, saltHex) {
|
|
121
|
+
if (!Buffer.isBuffer(plaintext) && typeof plaintext !== "string") {
|
|
122
|
+
throw new BackupCryptoError("backup-crypto/bad-plaintext",
|
|
123
|
+
"encryptWithPassphrase: plaintext must be a Buffer or string");
|
|
124
|
+
}
|
|
125
|
+
var plainBuf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, "utf8");
|
|
126
|
+
var key = await deriveKey(passphrase, saltHex);
|
|
127
|
+
var nonce = nodeCrypto.randomBytes(NONCE_BYTES);
|
|
128
|
+
var ct = xchacha20poly1305(new Uint8Array(key), nonce).encrypt(new Uint8Array(plainBuf));
|
|
129
|
+
return Buffer.concat([nonce, Buffer.from(ct)]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function decryptWithPassphrase(encrypted, passphrase, saltHex) {
|
|
133
|
+
if (!Buffer.isBuffer(encrypted)) {
|
|
134
|
+
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
135
|
+
"decryptWithPassphrase: encrypted must be a Buffer");
|
|
136
|
+
}
|
|
137
|
+
if (encrypted.length <= NONCE_BYTES) {
|
|
138
|
+
throw new BackupCryptoError("backup-crypto/bad-input",
|
|
139
|
+
"decryptWithPassphrase: encrypted buffer is too short to contain nonce + tag");
|
|
140
|
+
}
|
|
141
|
+
var key = await deriveKey(passphrase, saltHex);
|
|
142
|
+
var nonce = encrypted.subarray(0, NONCE_BYTES);
|
|
143
|
+
var ct = encrypted.subarray(NONCE_BYTES);
|
|
144
|
+
var plain;
|
|
145
|
+
try {
|
|
146
|
+
plain = xchacha20poly1305(new Uint8Array(key), new Uint8Array(nonce))
|
|
147
|
+
.decrypt(new Uint8Array(ct));
|
|
148
|
+
} catch (e) {
|
|
149
|
+
throw new BackupCryptoError("backup-crypto/decrypt-failed",
|
|
150
|
+
"XChaCha20-Poly1305 decryption failed (wrong passphrase or tampered ciphertext): " +
|
|
151
|
+
((e && e.message) || String(e)));
|
|
152
|
+
}
|
|
153
|
+
return Buffer.from(plain);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Convenience for the common "encrypt this with a fresh salt" pattern.
|
|
157
|
+
// Returns the salt as hex so callers can store it alongside the
|
|
158
|
+
// ciphertext in the bundle manifest.
|
|
159
|
+
async function encryptWithFreshSalt(plaintext, passphrase) {
|
|
160
|
+
var salt = nodeCrypto.randomBytes(SALT_BYTES);
|
|
161
|
+
var saltHex = salt.toString("hex");
|
|
162
|
+
var encrypted = await encryptWithPassphrase(plaintext, passphrase, saltHex);
|
|
163
|
+
return { encrypted: encrypted, salt: saltHex };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
deriveKey: deriveKey,
|
|
168
|
+
encryptWithPassphrase: encryptWithPassphrase,
|
|
169
|
+
decryptWithPassphrase: decryptWithPassphrase,
|
|
170
|
+
encryptWithFreshSalt: encryptWithFreshSalt,
|
|
171
|
+
checksum: checksum,
|
|
172
|
+
ARGON2_OPTS: ARGON2_OPTS,
|
|
173
|
+
SALT_BYTES: SALT_BYTES,
|
|
174
|
+
NONCE_BYTES: NONCE_BYTES,
|
|
175
|
+
BackupCryptoError: BackupCryptoError,
|
|
176
|
+
};
|