@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- 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.js +311 -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 +594 -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/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- 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/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- 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 -362
- package/lib/mail.js +981 -962
- 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/body-parser.js +1170 -1091
- 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 +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -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 +116 -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 -91
- 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 +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- 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 -319
- 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/credential-hash.js
CHANGED
|
@@ -1,322 +1,322 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* b.credentialHash — envelope-versioned credential hashing.
|
|
4
|
-
*
|
|
5
|
-
* Stores a verifiable digest of a credential (API key secret, shared
|
|
6
|
-
* bearer token, etc.) as a base64-encoded envelope:
|
|
7
|
-
*
|
|
8
|
-
* byte 0: 0xC1 (CREDENTIAL_MAGIC)
|
|
9
|
-
* byte 1: <algorithm ID>
|
|
10
|
-
* bytes 2..N: algorithm-specific payload
|
|
11
|
-
*
|
|
12
|
-
* The verify path dispatches on byte 1, so old credentials remain
|
|
13
|
-
* verifiable regardless of what ACTIVE.CRED_HASH points at today.
|
|
14
|
-
* When a new algorithm becomes the framework default, existing rows
|
|
15
|
-
* surface via `needsRehash()` and the next successful verify rotates
|
|
16
|
-
* them transparently — same pattern as `b.auth.password.needsRehash`.
|
|
17
|
-
*
|
|
18
|
-
* var env = await b.credentialHash.hash(secretBytes);
|
|
19
|
-
* // → "wQEx..." (base64)
|
|
20
|
-
*
|
|
21
|
-
* var ok = await b.credentialHash.verify(secretBytes, env);
|
|
22
|
-
* // → true / false
|
|
23
|
-
*
|
|
24
|
-
* var info = b.credentialHash.inspect(env);
|
|
25
|
-
* // → { algoId, algoName, payloadBytes }
|
|
26
|
-
*
|
|
27
|
-
* if (b.credentialHash.needsRehash(env)) {
|
|
28
|
-
* await db.update({ credentialHash: await b.credentialHash.hash(secretBytes) });
|
|
29
|
-
* }
|
|
30
|
-
*
|
|
31
|
-
* Active algorithm: SHAKE256 (0x01). Suitable for high-entropy random
|
|
32
|
-
* secrets (≥ 128 bits) — fast verify (microseconds), brute-force
|
|
33
|
-
* infeasible at the entropy level the framework generates. SHAKE256
|
|
34
|
-
* is an XOF: the envelope payload length drives the digest size, so
|
|
35
|
-
* a future operator can request a 96-byte (or 32-byte) digest without
|
|
36
|
-
* a primitive change — the same algorithm ID covers all output sizes.
|
|
37
|
-
* Operators with low-entropy or operator-supplied secrets should pin
|
|
38
|
-
* Argon2id (0x02) per-registry: `hash(s, { algo: "argon2id" })`.
|
|
39
|
-
*
|
|
40
|
-
* Why SHAKE256 over SHA3-512 as the active:
|
|
41
|
-
* - SHAKE256 is an extensible-output function (XOF). The envelope
|
|
42
|
-
* payload's actual byte length tells the verify path how many
|
|
43
|
-
* bytes to recompute. Changing digest size = no algo rotation.
|
|
44
|
-
* - Same family as the framework KDF (`crypto.kdf`), so one
|
|
45
|
-
* primitive does double duty.
|
|
46
|
-
* - SHA-3 family fixed-size mode locks the byte count at 64 — the
|
|
47
|
-
* moment we want a different size, we'd have to rotate algos.
|
|
48
|
-
*
|
|
49
|
-
* Why not Argon2id by default for api-key:
|
|
50
|
-
* - Argon2id at framework defaults costs ~250ms per verify call.
|
|
51
|
-
* For request-path verification that's a real latency hit.
|
|
52
|
-
* SHAKE256 is microseconds.
|
|
53
|
-
* - For ≥128-bit random secrets, the memory-hard property buys
|
|
54
|
-
* nothing — brute force is infeasible regardless of hash.
|
|
55
|
-
*
|
|
56
|
-
* Why the envelope still matters with SHAKE256 as active:
|
|
57
|
-
* - Algorithm agility — when SHA-3 family ever shows weakness, or
|
|
58
|
-
* a stronger XOF lands, ACTIVE.CRED_HASH rotates with no need
|
|
59
|
-
* to re-issue every credential. Old rows verify under their
|
|
60
|
-
* stored algo byte; new rows use the active.
|
|
61
|
-
* - Transparent rehash via needsRehash() drains old algos at the
|
|
62
|
-
* pace of organic verify traffic.
|
|
63
|
-
*
|
|
64
|
-
* Validation policy:
|
|
65
|
-
*
|
|
66
|
-
* - hash() opts (algo, params) → throw at call site
|
|
67
|
-
* - hash() secret type / length → throw at call site
|
|
68
|
-
* - verify() envelope shape unparsable → return false (tolerant read)
|
|
69
|
-
* - verify() unknown algo ID → return false (tolerant read)
|
|
70
|
-
* - inspect() bad envelope → return null (tolerant read)
|
|
71
|
-
*/
|
|
72
|
-
|
|
73
|
-
var crypto = require("./crypto");
|
|
74
|
-
var C = require("./constants");
|
|
75
|
-
var lazyRequire = require("./lazy-require");
|
|
76
|
-
var { FrameworkError } = require("./framework-error");
|
|
77
|
-
|
|
78
|
-
var observability = lazyRequire(function () { return require("./observability"); });
|
|
79
|
-
|
|
80
|
-
function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
|
|
81
|
-
|
|
82
|
-
// Default SHAKE256 output size for newly-issued credentials. 128
|
|
83
|
-
// bytes (1024 bits) is double the SHAKE256 capacity (512 bits, which
|
|
84
|
-
// caps collision-resistance); the extra bytes don't add to that
|
|
85
|
-
// guarantee but cost nothing — base64 expansion is ~172 chars in a
|
|
86
|
-
// TEXT column, verify is sub-microsecond, and a longer digest hurts
|
|
87
|
-
// no foreseeable attack while protecting against any unforeseen
|
|
88
|
-
// partial-collision attack class. Per the framework's modernity
|
|
89
|
-
// posture (highest practical bar where cost is negligible), default
|
|
90
|
-
// to the larger size; operators pass `{ params: { length: 64 } }`
|
|
91
|
-
// to opt into the SHA3-512-comparable byte count if they need it.
|
|
92
|
-
var SHAKE256_DEFAULT_LENGTH = C.BYTES.bytes(128);
|
|
93
|
-
// Minimum payload length both hash() and verify() enforce. 16 bytes
|
|
94
|
-
// (128 bits) is the floor below which the digest's collision space
|
|
95
|
-
// becomes brute-forceable. The asymmetry between hash() (rejected < 16
|
|
96
|
-
// at config time) and verify() (silently accepted any length) let a
|
|
97
|
-
// truncated stored envelope verify with trivial work; both ends now
|
|
98
|
-
// refuse short payloads symmetrically.
|
|
99
|
-
var SHAKE256_MIN_LENGTH = C.BYTES.bytes(16);
|
|
100
|
-
|
|
101
|
-
function _shake256(secret, length) {
|
|
102
|
-
// crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
|
|
103
|
-
// exact primitive we need — the framework's KDF and credential-hash
|
|
104
|
-
// share one underlying XOF.
|
|
105
|
-
return crypto.kdf(secret, length);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// auth/password is required lazily because it imports the (large)
|
|
110
|
-
// argon2 vendor; loading it for SHA3-only callers is wasted work.
|
|
111
|
-
var passwordPrimitive = lazyRequire(function () { return require("./auth/password"); });
|
|
112
|
-
|
|
113
|
-
class CredentialHashError extends FrameworkError {
|
|
114
|
-
constructor(message, code) {
|
|
115
|
-
super(message, code || "credential-hash/invalid");
|
|
116
|
-
this.name = "CredentialHashError";
|
|
117
|
-
this.isCredentialHashError = true;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
var ALGOS = Object.freeze({
|
|
122
|
-
SHAKE256: "shake256",
|
|
123
|
-
ARGON2ID: "argon2id",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
var DEFAULTS = Object.freeze({
|
|
127
|
-
algo: ALGOS.SHAKE256,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// Map between human-readable algo names (used in opts) and the wire
|
|
131
|
-
// algorithm IDs (used in the envelope byte). The envelope byte is
|
|
132
|
-
// what's persisted; the string is what operators read in code.
|
|
133
|
-
var NAME_TO_ID = Object.freeze({
|
|
134
|
-
"shake256": C.CRED_HASH_IDS.SHAKE256,
|
|
135
|
-
"argon2id": C.CRED_HASH_IDS.ARGON2ID,
|
|
136
|
-
});
|
|
137
|
-
var ID_TO_NAME = Object.freeze({
|
|
138
|
-
0x01: "shake256",
|
|
139
|
-
0x02: "argon2id",
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ---- Call-site validation (throw on bad input) ----
|
|
143
|
-
|
|
144
|
-
function _validateSecret(secret) {
|
|
145
|
-
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
146
|
-
throw new CredentialHashError(
|
|
147
|
-
"credentialHash: secret must be a string or Buffer, got " + typeof secret,
|
|
148
|
-
"credential-hash/bad-secret");
|
|
149
|
-
}
|
|
150
|
-
var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
|
|
151
|
-
if (len === 0) {
|
|
152
|
-
throw new CredentialHashError(
|
|
153
|
-
"credentialHash: secret must be non-empty",
|
|
154
|
-
"credential-hash/bad-secret");
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function _validateOpts(opts) {
|
|
159
|
-
if (!opts) return;
|
|
160
|
-
if (opts.algo !== undefined && typeof opts.algo !== "string") {
|
|
161
|
-
throw new CredentialHashError(
|
|
162
|
-
"credentialHash: algo must be a string ('sha3-512' or 'argon2id')",
|
|
163
|
-
"credential-hash/bad-opt");
|
|
164
|
-
}
|
|
165
|
-
if (opts.algo !== undefined && !Object.prototype.hasOwnProperty.call(NAME_TO_ID, opts.algo)) {
|
|
166
|
-
throw new CredentialHashError(
|
|
167
|
-
"credentialHash: unknown algo '" + opts.algo + "', expected one of " +
|
|
168
|
-
JSON.stringify(Object.keys(NAME_TO_ID)),
|
|
169
|
-
"credential-hash/bad-opt");
|
|
170
|
-
}
|
|
171
|
-
if (opts.params !== undefined && (typeof opts.params !== "object" || Array.isArray(opts.params))) {
|
|
172
|
-
throw new CredentialHashError(
|
|
173
|
-
"credentialHash: params must be a plain object",
|
|
174
|
-
"credential-hash/bad-opt");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ---- Envelope (de)serialization ----
|
|
179
|
-
|
|
180
|
-
function _envelope(algoId, payload) {
|
|
181
|
-
var head = Buffer.from([C.CREDENTIAL_MAGIC, algoId]);
|
|
182
|
-
return Buffer.concat([head, payload]).toString("base64");
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function _decodeEnvelope(env) {
|
|
186
|
-
if (typeof env !== "string" || env.length === 0) return null;
|
|
187
|
-
var buf;
|
|
188
|
-
try { buf = Buffer.from(env, "base64"); }
|
|
189
|
-
catch (_e) { return null; }
|
|
190
|
-
if (buf.length < 2) return null;
|
|
191
|
-
if (buf[0] !== C.CREDENTIAL_MAGIC) return null;
|
|
192
|
-
var algoId = buf[1];
|
|
193
|
-
if (!Object.prototype.hasOwnProperty.call(ID_TO_NAME, algoId)) return null;
|
|
194
|
-
return { algoId: algoId, payload: buf.slice(2) };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ---- Public surface ----
|
|
198
|
-
|
|
199
|
-
async function hash(secret, opts) {
|
|
200
|
-
_validateSecret(secret);
|
|
201
|
-
_validateOpts(opts);
|
|
202
|
-
var algoName = (opts && opts.algo) || DEFAULTS.algo;
|
|
203
|
-
var algoId = NAME_TO_ID[algoName];
|
|
204
|
-
|
|
205
|
-
if (algoId === C.CRED_HASH_IDS.SHAKE256) {
|
|
206
|
-
var length = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
|
|
207
|
-
if (typeof length !== "number" || !isFinite(length) ||
|
|
208
|
-
length < SHAKE256_MIN_LENGTH || Math.floor(length) !== length) {
|
|
209
|
-
throw new CredentialHashError(
|
|
210
|
-
"credentialHash.hash: SHAKE256 length must be an integer >= " +
|
|
211
|
-
SHAKE256_MIN_LENGTH + ", got " + JSON.stringify(length),
|
|
212
|
-
"credential-hash/bad-opt");
|
|
213
|
-
}
|
|
214
|
-
var env = _envelope(algoId, _shake256(secret, length));
|
|
215
|
-
_emitEvent("credentialHash.hash", 1, { algo: algoName });
|
|
216
|
-
return env;
|
|
217
|
-
}
|
|
218
|
-
if (algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
219
|
-
var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
|
|
220
|
-
var phc = await passwordPrimitive().hash(plain, opts && opts.params);
|
|
221
|
-
var argonEnv = _envelope(algoId, Buffer.from(phc, "utf8"));
|
|
222
|
-
_emitEvent("credentialHash.hash", 1, { algo: algoName });
|
|
223
|
-
return argonEnv;
|
|
224
|
-
}
|
|
225
|
-
// Unreachable — _validateOpts rejects unknown algos.
|
|
226
|
-
throw new CredentialHashError(
|
|
227
|
-
"credentialHash.hash: unsupported algo id 0x" + algoId.toString(0x10),
|
|
228
|
-
"credential-hash/unsupported");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function verify(secret, envelope) {
|
|
232
|
-
// Tolerant read: any malformed envelope → false. Lets operators write
|
|
233
|
-
// if (!await ch.verify(s, row.hash)) return res.status(401);
|
|
234
|
-
// without try/catch ceremony. We still reject obvious caller bugs
|
|
235
|
-
// (non-string-or-Buffer secret) loudly because that signals broken
|
|
236
|
-
// request plumbing rather than a bad credential.
|
|
237
|
-
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
238
|
-
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-secret-type" });
|
|
239
|
-
return false;
|
|
240
|
-
}
|
|
241
|
-
var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
|
|
242
|
-
if (len === 0) {
|
|
243
|
-
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "empty-secret" });
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
var decoded = _decodeEnvelope(envelope);
|
|
248
|
-
if (!decoded) {
|
|
249
|
-
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-envelope" });
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
var algoName = ID_TO_NAME[decoded.algoId];
|
|
253
|
-
|
|
254
|
-
if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
|
|
255
|
-
// Enforce the same minimum payload length that hash() enforces (16
|
|
256
|
-
// bytes / 128 bits). Without this, a storage bug or attacker
|
|
257
|
-
// tampering that truncates the stored envelope to just a few bytes
|
|
258
|
-
// produces a hash with catastrophically reduced collision space —
|
|
259
|
-
// a 1-byte payload has only 256 possible values, brute-forceable
|
|
260
|
-
// in microseconds. The asymmetry between hash() (rejects < 16) and
|
|
261
|
-
// verify() (accepted anything) was the silent gap; both ends now
|
|
262
|
-
// refuse weak digests symmetrically.
|
|
263
|
-
if (decoded.payload.length < SHAKE256_MIN_LENGTH) {
|
|
264
|
-
_emitEvent("credentialHash.verify", 1,
|
|
265
|
-
{ outcome: "failure", reason: "payload-too-short", algo: algoName });
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
var expected = _shake256(secret, decoded.payload.length);
|
|
269
|
-
var ok = crypto.timingSafeEqual(expected, decoded.payload);
|
|
270
|
-
_emitEvent("credentialHash.verify", 1,
|
|
271
|
-
{ outcome: ok ? "success" : "failure", algo: algoName });
|
|
272
|
-
return ok;
|
|
273
|
-
}
|
|
274
|
-
if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
275
|
-
var phc = decoded.payload.toString("utf8");
|
|
276
|
-
var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
|
|
277
|
-
var argOk = false;
|
|
278
|
-
try { argOk = await passwordPrimitive().verify(phc, plain); }
|
|
279
|
-
catch (_e) { argOk = false; }
|
|
280
|
-
_emitEvent("credentialHash.verify", 1,
|
|
281
|
-
{ outcome: argOk ? "success" : "failure", algo: algoName });
|
|
282
|
-
return argOk;
|
|
283
|
-
}
|
|
284
|
-
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "unknown-algo" });
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function inspect(envelope) {
|
|
289
|
-
var decoded = _decodeEnvelope(envelope);
|
|
290
|
-
if (!decoded) return null;
|
|
291
|
-
return {
|
|
292
|
-
algoId: decoded.algoId,
|
|
293
|
-
algoName: ID_TO_NAME[decoded.algoId],
|
|
294
|
-
payloadBytes: decoded.payload.length,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function needsRehash(envelope, opts) {
|
|
299
|
-
var decoded = _decodeEnvelope(envelope);
|
|
300
|
-
if (!decoded) return true; // unrecognized → migrate aggressively
|
|
301
|
-
var targetAlgoName = (opts && opts.algo) || DEFAULTS.algo;
|
|
302
|
-
var targetId = NAME_TO_ID[targetAlgoName] || C.ACTIVE.CRED_HASH;
|
|
303
|
-
if (decoded.algoId !== targetId) return true;
|
|
304
|
-
if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
305
|
-
// Defer the parameter-lag check to the password primitive's
|
|
306
|
-
// own needsRehash so the threshold stays in one place.
|
|
307
|
-
var phc = decoded.payload.toString("utf8");
|
|
308
|
-
try { return passwordPrimitive().needsRehash(phc, opts && opts.params); }
|
|
309
|
-
catch (_e) { return true; }
|
|
310
|
-
}
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
module.exports = {
|
|
315
|
-
hash: hash,
|
|
316
|
-
verify: verify,
|
|
317
|
-
inspect: inspect,
|
|
318
|
-
needsRehash: needsRehash,
|
|
319
|
-
ALGOS: ALGOS,
|
|
320
|
-
DEFAULTS: DEFAULTS,
|
|
321
|
-
CredentialHashError: CredentialHashError,
|
|
322
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.credentialHash — envelope-versioned credential hashing.
|
|
4
|
+
*
|
|
5
|
+
* Stores a verifiable digest of a credential (API key secret, shared
|
|
6
|
+
* bearer token, etc.) as a base64-encoded envelope:
|
|
7
|
+
*
|
|
8
|
+
* byte 0: 0xC1 (CREDENTIAL_MAGIC)
|
|
9
|
+
* byte 1: <algorithm ID>
|
|
10
|
+
* bytes 2..N: algorithm-specific payload
|
|
11
|
+
*
|
|
12
|
+
* The verify path dispatches on byte 1, so old credentials remain
|
|
13
|
+
* verifiable regardless of what ACTIVE.CRED_HASH points at today.
|
|
14
|
+
* When a new algorithm becomes the framework default, existing rows
|
|
15
|
+
* surface via `needsRehash()` and the next successful verify rotates
|
|
16
|
+
* them transparently — same pattern as `b.auth.password.needsRehash`.
|
|
17
|
+
*
|
|
18
|
+
* var env = await b.credentialHash.hash(secretBytes);
|
|
19
|
+
* // → "wQEx..." (base64)
|
|
20
|
+
*
|
|
21
|
+
* var ok = await b.credentialHash.verify(secretBytes, env);
|
|
22
|
+
* // → true / false
|
|
23
|
+
*
|
|
24
|
+
* var info = b.credentialHash.inspect(env);
|
|
25
|
+
* // → { algoId, algoName, payloadBytes }
|
|
26
|
+
*
|
|
27
|
+
* if (b.credentialHash.needsRehash(env)) {
|
|
28
|
+
* await db.update({ credentialHash: await b.credentialHash.hash(secretBytes) });
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* Active algorithm: SHAKE256 (0x01). Suitable for high-entropy random
|
|
32
|
+
* secrets (≥ 128 bits) — fast verify (microseconds), brute-force
|
|
33
|
+
* infeasible at the entropy level the framework generates. SHAKE256
|
|
34
|
+
* is an XOF: the envelope payload length drives the digest size, so
|
|
35
|
+
* a future operator can request a 96-byte (or 32-byte) digest without
|
|
36
|
+
* a primitive change — the same algorithm ID covers all output sizes.
|
|
37
|
+
* Operators with low-entropy or operator-supplied secrets should pin
|
|
38
|
+
* Argon2id (0x02) per-registry: `hash(s, { algo: "argon2id" })`.
|
|
39
|
+
*
|
|
40
|
+
* Why SHAKE256 over SHA3-512 as the active:
|
|
41
|
+
* - SHAKE256 is an extensible-output function (XOF). The envelope
|
|
42
|
+
* payload's actual byte length tells the verify path how many
|
|
43
|
+
* bytes to recompute. Changing digest size = no algo rotation.
|
|
44
|
+
* - Same family as the framework KDF (`crypto.kdf`), so one
|
|
45
|
+
* primitive does double duty.
|
|
46
|
+
* - SHA-3 family fixed-size mode locks the byte count at 64 — the
|
|
47
|
+
* moment we want a different size, we'd have to rotate algos.
|
|
48
|
+
*
|
|
49
|
+
* Why not Argon2id by default for api-key:
|
|
50
|
+
* - Argon2id at framework defaults costs ~250ms per verify call.
|
|
51
|
+
* For request-path verification that's a real latency hit.
|
|
52
|
+
* SHAKE256 is microseconds.
|
|
53
|
+
* - For ≥128-bit random secrets, the memory-hard property buys
|
|
54
|
+
* nothing — brute force is infeasible regardless of hash.
|
|
55
|
+
*
|
|
56
|
+
* Why the envelope still matters with SHAKE256 as active:
|
|
57
|
+
* - Algorithm agility — when SHA-3 family ever shows weakness, or
|
|
58
|
+
* a stronger XOF lands, ACTIVE.CRED_HASH rotates with no need
|
|
59
|
+
* to re-issue every credential. Old rows verify under their
|
|
60
|
+
* stored algo byte; new rows use the active.
|
|
61
|
+
* - Transparent rehash via needsRehash() drains old algos at the
|
|
62
|
+
* pace of organic verify traffic.
|
|
63
|
+
*
|
|
64
|
+
* Validation policy:
|
|
65
|
+
*
|
|
66
|
+
* - hash() opts (algo, params) → throw at call site
|
|
67
|
+
* - hash() secret type / length → throw at call site
|
|
68
|
+
* - verify() envelope shape unparsable → return false (tolerant read)
|
|
69
|
+
* - verify() unknown algo ID → return false (tolerant read)
|
|
70
|
+
* - inspect() bad envelope → return null (tolerant read)
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
var crypto = require("./crypto");
|
|
74
|
+
var C = require("./constants");
|
|
75
|
+
var lazyRequire = require("./lazy-require");
|
|
76
|
+
var { FrameworkError } = require("./framework-error");
|
|
77
|
+
|
|
78
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
79
|
+
|
|
80
|
+
function _emitEvent(n, v, l) { observability().safeEvent(n, v, l || {}); }
|
|
81
|
+
|
|
82
|
+
// Default SHAKE256 output size for newly-issued credentials. 128
|
|
83
|
+
// bytes (1024 bits) is double the SHAKE256 capacity (512 bits, which
|
|
84
|
+
// caps collision-resistance); the extra bytes don't add to that
|
|
85
|
+
// guarantee but cost nothing — base64 expansion is ~172 chars in a
|
|
86
|
+
// TEXT column, verify is sub-microsecond, and a longer digest hurts
|
|
87
|
+
// no foreseeable attack while protecting against any unforeseen
|
|
88
|
+
// partial-collision attack class. Per the framework's modernity
|
|
89
|
+
// posture (highest practical bar where cost is negligible), default
|
|
90
|
+
// to the larger size; operators pass `{ params: { length: 64 } }`
|
|
91
|
+
// to opt into the SHA3-512-comparable byte count if they need it.
|
|
92
|
+
var SHAKE256_DEFAULT_LENGTH = C.BYTES.bytes(128);
|
|
93
|
+
// Minimum payload length both hash() and verify() enforce. 16 bytes
|
|
94
|
+
// (128 bits) is the floor below which the digest's collision space
|
|
95
|
+
// becomes brute-forceable. The asymmetry between hash() (rejected < 16
|
|
96
|
+
// at config time) and verify() (silently accepted any length) let a
|
|
97
|
+
// truncated stored envelope verify with trivial work; both ends now
|
|
98
|
+
// refuse short payloads symmetrically.
|
|
99
|
+
var SHAKE256_MIN_LENGTH = C.BYTES.bytes(16);
|
|
100
|
+
|
|
101
|
+
function _shake256(secret, length) {
|
|
102
|
+
// crypto.kdf wraps SHAKE256 with arbitrary output length. That's the
|
|
103
|
+
// exact primitive we need — the framework's KDF and credential-hash
|
|
104
|
+
// share one underlying XOF.
|
|
105
|
+
return crypto.kdf(secret, length);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
// auth/password is required lazily because it imports the (large)
|
|
110
|
+
// argon2 vendor; loading it for SHA3-only callers is wasted work.
|
|
111
|
+
var passwordPrimitive = lazyRequire(function () { return require("./auth/password"); });
|
|
112
|
+
|
|
113
|
+
class CredentialHashError extends FrameworkError {
|
|
114
|
+
constructor(message, code) {
|
|
115
|
+
super(message, code || "credential-hash/invalid");
|
|
116
|
+
this.name = "CredentialHashError";
|
|
117
|
+
this.isCredentialHashError = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var ALGOS = Object.freeze({
|
|
122
|
+
SHAKE256: "shake256",
|
|
123
|
+
ARGON2ID: "argon2id",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
var DEFAULTS = Object.freeze({
|
|
127
|
+
algo: ALGOS.SHAKE256,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Map between human-readable algo names (used in opts) and the wire
|
|
131
|
+
// algorithm IDs (used in the envelope byte). The envelope byte is
|
|
132
|
+
// what's persisted; the string is what operators read in code.
|
|
133
|
+
var NAME_TO_ID = Object.freeze({
|
|
134
|
+
"shake256": C.CRED_HASH_IDS.SHAKE256,
|
|
135
|
+
"argon2id": C.CRED_HASH_IDS.ARGON2ID,
|
|
136
|
+
});
|
|
137
|
+
var ID_TO_NAME = Object.freeze({
|
|
138
|
+
0x01: "shake256",
|
|
139
|
+
0x02: "argon2id",
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---- Call-site validation (throw on bad input) ----
|
|
143
|
+
|
|
144
|
+
function _validateSecret(secret) {
|
|
145
|
+
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
146
|
+
throw new CredentialHashError(
|
|
147
|
+
"credentialHash: secret must be a string or Buffer, got " + typeof secret,
|
|
148
|
+
"credential-hash/bad-secret");
|
|
149
|
+
}
|
|
150
|
+
var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
|
|
151
|
+
if (len === 0) {
|
|
152
|
+
throw new CredentialHashError(
|
|
153
|
+
"credentialHash: secret must be non-empty",
|
|
154
|
+
"credential-hash/bad-secret");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function _validateOpts(opts) {
|
|
159
|
+
if (!opts) return;
|
|
160
|
+
if (opts.algo !== undefined && typeof opts.algo !== "string") {
|
|
161
|
+
throw new CredentialHashError(
|
|
162
|
+
"credentialHash: algo must be a string ('sha3-512' or 'argon2id')",
|
|
163
|
+
"credential-hash/bad-opt");
|
|
164
|
+
}
|
|
165
|
+
if (opts.algo !== undefined && !Object.prototype.hasOwnProperty.call(NAME_TO_ID, opts.algo)) {
|
|
166
|
+
throw new CredentialHashError(
|
|
167
|
+
"credentialHash: unknown algo '" + opts.algo + "', expected one of " +
|
|
168
|
+
JSON.stringify(Object.keys(NAME_TO_ID)),
|
|
169
|
+
"credential-hash/bad-opt");
|
|
170
|
+
}
|
|
171
|
+
if (opts.params !== undefined && (typeof opts.params !== "object" || Array.isArray(opts.params))) {
|
|
172
|
+
throw new CredentialHashError(
|
|
173
|
+
"credentialHash: params must be a plain object",
|
|
174
|
+
"credential-hash/bad-opt");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- Envelope (de)serialization ----
|
|
179
|
+
|
|
180
|
+
function _envelope(algoId, payload) {
|
|
181
|
+
var head = Buffer.from([C.CREDENTIAL_MAGIC, algoId]);
|
|
182
|
+
return Buffer.concat([head, payload]).toString("base64");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _decodeEnvelope(env) {
|
|
186
|
+
if (typeof env !== "string" || env.length === 0) return null;
|
|
187
|
+
var buf;
|
|
188
|
+
try { buf = Buffer.from(env, "base64"); }
|
|
189
|
+
catch (_e) { return null; }
|
|
190
|
+
if (buf.length < 2) return null;
|
|
191
|
+
if (buf[0] !== C.CREDENTIAL_MAGIC) return null;
|
|
192
|
+
var algoId = buf[1];
|
|
193
|
+
if (!Object.prototype.hasOwnProperty.call(ID_TO_NAME, algoId)) return null;
|
|
194
|
+
return { algoId: algoId, payload: buf.slice(2) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- Public surface ----
|
|
198
|
+
|
|
199
|
+
async function hash(secret, opts) {
|
|
200
|
+
_validateSecret(secret);
|
|
201
|
+
_validateOpts(opts);
|
|
202
|
+
var algoName = (opts && opts.algo) || DEFAULTS.algo;
|
|
203
|
+
var algoId = NAME_TO_ID[algoName];
|
|
204
|
+
|
|
205
|
+
if (algoId === C.CRED_HASH_IDS.SHAKE256) {
|
|
206
|
+
var length = (opts && opts.params && opts.params.length) || SHAKE256_DEFAULT_LENGTH;
|
|
207
|
+
if (typeof length !== "number" || !isFinite(length) ||
|
|
208
|
+
length < SHAKE256_MIN_LENGTH || Math.floor(length) !== length) {
|
|
209
|
+
throw new CredentialHashError(
|
|
210
|
+
"credentialHash.hash: SHAKE256 length must be an integer >= " +
|
|
211
|
+
SHAKE256_MIN_LENGTH + ", got " + JSON.stringify(length),
|
|
212
|
+
"credential-hash/bad-opt");
|
|
213
|
+
}
|
|
214
|
+
var env = _envelope(algoId, _shake256(secret, length));
|
|
215
|
+
_emitEvent("credentialHash.hash", 1, { algo: algoName });
|
|
216
|
+
return env;
|
|
217
|
+
}
|
|
218
|
+
if (algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
219
|
+
var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
|
|
220
|
+
var phc = await passwordPrimitive().hash(plain, opts && opts.params);
|
|
221
|
+
var argonEnv = _envelope(algoId, Buffer.from(phc, "utf8"));
|
|
222
|
+
_emitEvent("credentialHash.hash", 1, { algo: algoName });
|
|
223
|
+
return argonEnv;
|
|
224
|
+
}
|
|
225
|
+
// Unreachable — _validateOpts rejects unknown algos.
|
|
226
|
+
throw new CredentialHashError(
|
|
227
|
+
"credentialHash.hash: unsupported algo id 0x" + algoId.toString(0x10),
|
|
228
|
+
"credential-hash/unsupported");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function verify(secret, envelope) {
|
|
232
|
+
// Tolerant read: any malformed envelope → false. Lets operators write
|
|
233
|
+
// if (!await ch.verify(s, row.hash)) return res.status(401);
|
|
234
|
+
// without try/catch ceremony. We still reject obvious caller bugs
|
|
235
|
+
// (non-string-or-Buffer secret) loudly because that signals broken
|
|
236
|
+
// request plumbing rather than a bad credential.
|
|
237
|
+
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
238
|
+
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-secret-type" });
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
var len = Buffer.isBuffer(secret) ? secret.length : Buffer.byteLength(secret, "utf8");
|
|
242
|
+
if (len === 0) {
|
|
243
|
+
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "empty-secret" });
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
var decoded = _decodeEnvelope(envelope);
|
|
248
|
+
if (!decoded) {
|
|
249
|
+
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "bad-envelope" });
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
var algoName = ID_TO_NAME[decoded.algoId];
|
|
253
|
+
|
|
254
|
+
if (decoded.algoId === C.CRED_HASH_IDS.SHAKE256) {
|
|
255
|
+
// Enforce the same minimum payload length that hash() enforces (16
|
|
256
|
+
// bytes / 128 bits). Without this, a storage bug or attacker
|
|
257
|
+
// tampering that truncates the stored envelope to just a few bytes
|
|
258
|
+
// produces a hash with catastrophically reduced collision space —
|
|
259
|
+
// a 1-byte payload has only 256 possible values, brute-forceable
|
|
260
|
+
// in microseconds. The asymmetry between hash() (rejects < 16) and
|
|
261
|
+
// verify() (accepted anything) was the silent gap; both ends now
|
|
262
|
+
// refuse weak digests symmetrically.
|
|
263
|
+
if (decoded.payload.length < SHAKE256_MIN_LENGTH) {
|
|
264
|
+
_emitEvent("credentialHash.verify", 1,
|
|
265
|
+
{ outcome: "failure", reason: "payload-too-short", algo: algoName });
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
var expected = _shake256(secret, decoded.payload.length);
|
|
269
|
+
var ok = crypto.timingSafeEqual(expected, decoded.payload);
|
|
270
|
+
_emitEvent("credentialHash.verify", 1,
|
|
271
|
+
{ outcome: ok ? "success" : "failure", algo: algoName });
|
|
272
|
+
return ok;
|
|
273
|
+
}
|
|
274
|
+
if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
275
|
+
var phc = decoded.payload.toString("utf8");
|
|
276
|
+
var plain = Buffer.isBuffer(secret) ? secret.toString("utf8") : secret;
|
|
277
|
+
var argOk = false;
|
|
278
|
+
try { argOk = await passwordPrimitive().verify(phc, plain); }
|
|
279
|
+
catch (_e) { argOk = false; }
|
|
280
|
+
_emitEvent("credentialHash.verify", 1,
|
|
281
|
+
{ outcome: argOk ? "success" : "failure", algo: algoName });
|
|
282
|
+
return argOk;
|
|
283
|
+
}
|
|
284
|
+
_emitEvent("credentialHash.verify", 1, { outcome: "failure", reason: "unknown-algo" });
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function inspect(envelope) {
|
|
289
|
+
var decoded = _decodeEnvelope(envelope);
|
|
290
|
+
if (!decoded) return null;
|
|
291
|
+
return {
|
|
292
|
+
algoId: decoded.algoId,
|
|
293
|
+
algoName: ID_TO_NAME[decoded.algoId],
|
|
294
|
+
payloadBytes: decoded.payload.length,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function needsRehash(envelope, opts) {
|
|
299
|
+
var decoded = _decodeEnvelope(envelope);
|
|
300
|
+
if (!decoded) return true; // unrecognized → migrate aggressively
|
|
301
|
+
var targetAlgoName = (opts && opts.algo) || DEFAULTS.algo;
|
|
302
|
+
var targetId = NAME_TO_ID[targetAlgoName] || C.ACTIVE.CRED_HASH;
|
|
303
|
+
if (decoded.algoId !== targetId) return true;
|
|
304
|
+
if (decoded.algoId === C.CRED_HASH_IDS.ARGON2ID) {
|
|
305
|
+
// Defer the parameter-lag check to the password primitive's
|
|
306
|
+
// own needsRehash so the threshold stays in one place.
|
|
307
|
+
var phc = decoded.payload.toString("utf8");
|
|
308
|
+
try { return passwordPrimitive().needsRehash(phc, opts && opts.params); }
|
|
309
|
+
catch (_e) { return true; }
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = {
|
|
315
|
+
hash: hash,
|
|
316
|
+
verify: verify,
|
|
317
|
+
inspect: inspect,
|
|
318
|
+
needsRehash: needsRehash,
|
|
319
|
+
ALGOS: ALGOS,
|
|
320
|
+
DEFAULTS: DEFAULTS,
|
|
321
|
+
CredentialHashError: CredentialHashError,
|
|
322
|
+
};
|