@msdis/shield 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +140 -0
- package/README.md +106 -0
- package/dist/aead/index.d.ts +37 -0
- package/dist/aead/index.js +7 -0
- package/dist/aead/index.js.map +1 -0
- package/dist/asymmetric/index.d.ts +32 -0
- package/dist/asymmetric/index.js +6 -0
- package/dist/asymmetric/index.js.map +1 -0
- package/dist/chunk-3DQPQCAR.js +114 -0
- package/dist/chunk-3DQPQCAR.js.map +1 -0
- package/dist/chunk-3HCT6A2P.js +55 -0
- package/dist/chunk-3HCT6A2P.js.map +1 -0
- package/dist/chunk-AB2WZ7Y2.js +57 -0
- package/dist/chunk-AB2WZ7Y2.js.map +1 -0
- package/dist/chunk-BUFRR5PB.js +9 -0
- package/dist/chunk-BUFRR5PB.js.map +1 -0
- package/dist/chunk-CYIGDF63.js +30 -0
- package/dist/chunk-CYIGDF63.js.map +1 -0
- package/dist/chunk-EOXWR7DS.js +153 -0
- package/dist/chunk-EOXWR7DS.js.map +1 -0
- package/dist/chunk-FUDDBD2G.js +43 -0
- package/dist/chunk-FUDDBD2G.js.map +1 -0
- package/dist/chunk-JSKIWIEC.js +56 -0
- package/dist/chunk-JSKIWIEC.js.map +1 -0
- package/dist/chunk-JVFP2GAO.js +66 -0
- package/dist/chunk-JVFP2GAO.js.map +1 -0
- package/dist/chunk-KNCZMIZA.js +55 -0
- package/dist/chunk-KNCZMIZA.js.map +1 -0
- package/dist/chunk-MJO7IJZC.js +44 -0
- package/dist/chunk-MJO7IJZC.js.map +1 -0
- package/dist/chunk-MPWYZXW7.js +66 -0
- package/dist/chunk-MPWYZXW7.js.map +1 -0
- package/dist/chunk-OA5ARYJM.js +73 -0
- package/dist/chunk-OA5ARYJM.js.map +1 -0
- package/dist/chunk-OPHN2B3N.js +147 -0
- package/dist/chunk-OPHN2B3N.js.map +1 -0
- package/dist/chunk-RTAJJZKO.js +116 -0
- package/dist/chunk-RTAJJZKO.js.map +1 -0
- package/dist/chunk-SCHZI6YY.js +35 -0
- package/dist/chunk-SCHZI6YY.js.map +1 -0
- package/dist/chunk-T3IV7SHD.js +388 -0
- package/dist/chunk-T3IV7SHD.js.map +1 -0
- package/dist/chunk-U65A4HIY.js +133 -0
- package/dist/chunk-U65A4HIY.js.map +1 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/encoding-B-cb7Duu.d.ts +37 -0
- package/dist/errors-C79jA9vX.d.ts +65 -0
- package/dist/file-encryption/index.d.ts +135 -0
- package/dist/file-encryption/index.js +11 -0
- package/dist/file-encryption/index.js.map +1 -0
- package/dist/format-versioning/index.d.ts +37 -0
- package/dist/format-versioning/index.js +4 -0
- package/dist/format-versioning/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity/index.d.ts +46 -0
- package/dist/integrity/index.js +6 -0
- package/dist/integrity/index.js.map +1 -0
- package/dist/kdf/index.d.ts +104 -0
- package/dist/kdf/index.js +7 -0
- package/dist/kdf/index.js.map +1 -0
- package/dist/key-management/index.d.ts +69 -0
- package/dist/key-management/index.js +10 -0
- package/dist/key-management/index.js.map +1 -0
- package/dist/migrations/index.d.ts +41 -0
- package/dist/migrations/index.js +4 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/post-quantum/index.d.ts +153 -0
- package/dist/post-quantum/index.js +3 -0
- package/dist/post-quantum/index.js.map +1 -0
- package/dist/random/index.d.ts +28 -0
- package/dist/random/index.js +5 -0
- package/dist/random/index.js.map +1 -0
- package/dist/secure-memory/index.d.ts +40 -0
- package/dist/secure-memory/index.js +5 -0
- package/dist/secure-memory/index.js.map +1 -0
- package/dist/signing/index.d.ts +41 -0
- package/dist/signing/index.js +5 -0
- package/dist/signing/index.js.map +1 -0
- package/dist/totp/index.d.ts +69 -0
- package/dist/totp/index.js +4 -0
- package/dist/totp/index.js.map +1 -0
- package/dist/vault-crypto/index.d.ts +225 -0
- package/dist/vault-crypto/index.js +465 -0
- package/dist/vault-crypto/index.js.map +1 -0
- package/dist/vault-encryption/index.d.ts +40 -0
- package/dist/vault-encryption/index.js +9 -0
- package/dist/vault-encryption/index.js.map +1 -0
- package/package.json +137 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/integrity/index.ts"],"names":[],"mappings":";;;;;AAoBA,eAAsB,YAAY,IAAA,EAAuC;AACrE,EAAA,MAAM,SAAS,MAAM,MAAA,EAAO,CAAE,MAAA,CAAO,WAAW,IAAoB,CAAA;AACpE,EAAA,OAAO,IAAI,WAAW,MAAM,CAAA;AAChC;AAGA,eAAsB,aAAa,IAAA,EAAmC;AAClE,EAAA,OAAO,aAAA,CAAc,MAAM,WAAA,CAAY,IAAI,CAAC,CAAA;AAChD;AAGA,eAAsB,gBAAgB,IAAA,EAAmC;AACrE,EAAA,OAAO,gBAAA,CAAiB,MAAM,WAAA,CAAY,IAAI,CAAC,CAAA;AACnD;AAGA,eAAsB,UAAU,IAAA,EAAmC;AAC/D,EAAA,OAAO,UAAA,CAAW,MAAM,WAAA,CAAY,IAAI,CAAC,CAAA;AAC7C;AAUA,eAAsB,QAAQ,IAAA,EAAmC;AAC7D,EAAA,MAAM,SAAS,MAAM,MAAA,EAAO,CAAE,MAAA,CAAO,SAAS,IAAoB,CAAA;AAClE,EAAA,OAAO,UAAA,CAAW,IAAI,UAAA,CAAW,MAAM,CAAC,CAAA;AAC5C;AAGA,eAAsB,oBAClB,QAAA,EACA,MAAA,GAAqB,CAAC,MAAA,EAAQ,QAAQ,CAAA,EACpB;AAClB,EAAA,OAAO,QAAO,CAAE,SAAA;AAAA,IACZ,KAAA;AAAA,IACA,QAAA;AAAA,IACA,EAAE,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,SAAA,EAAU;AAAA,IAChC,KAAA;AAAA,IACA;AAAA,GACJ;AACJ;AAGA,eAAsB,iBAAA,CAAkB,KAAgB,IAAA,EAAuC;AAC3F,EAAA,MAAM,MAAM,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,EAAQ,KAAK,IAAoB,CAAA;AACjE,EAAA,OAAO,IAAI,WAAW,GAAG,CAAA;AAC7B;AAGA,eAAsB,UAAA,CAAW,UAAsB,IAAA,EAAuC;AAC1F,EAAA,MAAM,MAAM,MAAM,mBAAA,CAAoB,QAAA,EAAU,CAAC,MAAM,CAAC,CAAA;AACxD,EAAA,OAAO,iBAAA,CAAkB,KAAK,IAAI,CAAA;AACtC;AAGA,eAAsB,mBAAmB,IAAA,EAA+B;AACpE,EAAA,OAAO,YAAA,CAAa,WAAA,CAAY,IAAI,CAAC,CAAA;AACzC;AAGA,eAAsB,iBAAiB,KAAA,EAAiC;AACpE,EAAA,OAAO,kBAAA,CAAmB,IAAA,CAAK,SAAA,CAAU,KAAK,CAAC,CAAA;AACnD;AAGO,SAAS,iBAAA,CAAkB,GAAe,CAAA,EAAwB;AACrE,EAAA,IAAI,CAAA,CAAE,MAAA,KAAW,CAAA,CAAE,MAAA,EAAQ,OAAO,KAAA;AAClC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,CAAE,QAAQ,CAAA,EAAA,EAAK;AAC/B,IAAA,MAAA,IAAU,CAAA,CAAE,CAAC,CAAA,GAAK,CAAA,CAAE,CAAC,CAAA;AAAA,EACzB;AACA,EAAA,OAAO,MAAA,KAAW,CAAA;AACtB;AAGO,SAAS,uBAAA,CAAwB,GAAW,CAAA,EAAoB;AACnE,EAAA,OAAO,kBAAkB,aAAA,CAAc,CAAC,CAAA,EAAG,aAAA,CAAc,CAAC,CAAC,CAAA;AAC/D;AAMA,eAAsB,sBAAA,CAClB,MACA,cAAA,EACa;AACb,EAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,IAAI,CAAA;AACtC,EAAA,IAAI,CAAC,uBAAA,CAAwB,MAAA,EAAQ,cAAc,CAAA,EAAG;AAClD,IAAA,MAAM,IAAI,iBAAA,EAAkB;AAAA,EAChC;AACJ","file":"chunk-MPWYZXW7.js","sourcesContent":["/**\r\n * dis-integrity — hashing and verification helpers.\r\n *\r\n * Provides SHA-256 digests (base64) and constant-time comparison. Used for\r\n * file-chunk integrity, manifest roots, and any place that must verify a\r\n * payload has not been altered. Confidential payloads still rely on AEAD;\r\n * these helpers cover integrity of already-encrypted / public material.\r\n */\r\n\r\nimport { subtle } from '../core/provider.js';\r\nimport {\r\n base64ToBytes,\r\n bytesToBase64,\r\n bytesToBase64Url,\r\n bytesToHex,\r\n utf8ToBytes,\r\n} from '../core/encoding.js';\r\nimport { DisIntegrityError } from '../core/errors.js';\r\n\r\n/** SHA-256 of raw bytes, returned as a fresh `Uint8Array` digest. */\r\nexport async function sha256Bytes(data: Uint8Array): Promise<Uint8Array> {\r\n const digest = await subtle().digest('SHA-256', data as BufferSource);\r\n return new Uint8Array(digest);\r\n}\r\n\r\n/** SHA-256 of raw bytes, base64-encoded. */\r\nexport async function sha256Base64(data: Uint8Array): Promise<string> {\r\n return bytesToBase64(await sha256Bytes(data));\r\n}\r\n\r\n/** SHA-256 of raw bytes, unpadded-base64url-encoded. */\r\nexport async function sha256Base64Url(data: Uint8Array): Promise<string> {\r\n return bytesToBase64Url(await sha256Bytes(data));\r\n}\r\n\r\n/** SHA-256 of raw bytes, lower-case hex-encoded. */\r\nexport async function sha256Hex(data: Uint8Array): Promise<string> {\r\n return bytesToHex(await sha256Bytes(data));\r\n}\r\n\r\n/**\r\n * SHA-1 of raw bytes, lower-case hex-encoded.\r\n *\r\n * SHA-1 is collision-broken and MUST NOT be used for any security decision.\r\n * It is provided solely for legacy interop where a remote protocol mandates\r\n * it — specifically the HaveIBeenPwned k-anonymity range API, which keys on\r\n * the SHA-1 of the password. Callers uppercase the hex as the API requires.\r\n */\r\nexport async function sha1Hex(data: Uint8Array): Promise<string> {\r\n const digest = await subtle().digest('SHA-1', data as BufferSource);\r\n return bytesToHex(new Uint8Array(digest));\r\n}\r\n\r\n/** Imports raw bytes as an HMAC-SHA-256 key. */\r\nexport async function importHmacSha256Key(\r\n keyBytes: Uint8Array,\r\n usages: KeyUsage[] = ['sign', 'verify'],\r\n): Promise<CryptoKey> {\r\n return subtle().importKey(\r\n 'raw',\r\n keyBytes as BufferSource,\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n false,\r\n usages,\r\n );\r\n}\r\n\r\n/** Computes HMAC-SHA-256 over `data` with an already-imported key. */\r\nexport async function hmacSha256WithKey(key: CryptoKey, data: Uint8Array): Promise<Uint8Array> {\r\n const sig = await subtle().sign('HMAC', key, data as BufferSource);\r\n return new Uint8Array(sig);\r\n}\r\n\r\n/** Computes HMAC-SHA-256 over `data` with raw key bytes. */\r\nexport async function hmacSha256(keyBytes: Uint8Array, data: Uint8Array): Promise<Uint8Array> {\r\n const key = await importHmacSha256Key(keyBytes, ['sign']);\r\n return hmacSha256WithKey(key, data);\r\n}\r\n\r\n/** SHA-256 of a UTF-8 string, base64-encoded. */\r\nexport async function sha256StringBase64(data: string): Promise<string> {\r\n return sha256Base64(utf8ToBytes(data));\r\n}\r\n\r\n/** SHA-256 of the JSON serialisation of `value`, base64-encoded. */\r\nexport async function sha256JsonBase64(value: unknown): Promise<string> {\r\n return sha256StringBase64(JSON.stringify(value));\r\n}\r\n\r\n/** Constant-time equality of two byte arrays. */\r\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\r\n if (a.length !== b.length) return false;\r\n let result = 0;\r\n for (let i = 0; i < a.length; i++) {\r\n result |= a[i]! ^ b[i]!;\r\n }\r\n return result === 0;\r\n}\r\n\r\n/** Constant-time equality of two base64 strings (compared as decoded bytes). */\r\nexport function constantTimeEqualBase64(a: string, b: string): boolean {\r\n return constantTimeEqual(base64ToBytes(a), base64ToBytes(b));\r\n}\r\n\r\n/**\r\n * Verifies that `data` hashes (SHA-256) to `expectedBase64`, throwing\r\n * {@link DisIntegrityError} on mismatch. Comparison is constant-time.\r\n */\r\nexport async function verifyPayloadIntegrity(\r\n data: Uint8Array,\r\n expectedBase64: string,\r\n): Promise<void> {\r\n const actual = await sha256Base64(data);\r\n if (!constantTimeEqualBase64(actual, expectedBase64)) {\r\n throw new DisIntegrityError();\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
|
|
2
|
+
import * as OTPAuth from 'otpauth';
|
|
3
|
+
|
|
4
|
+
var TOTP_ALGORITHM = "SHA1";
|
|
5
|
+
var TOTP_DIGITS = 6;
|
|
6
|
+
var TOTP_PERIOD_SECONDS = 30;
|
|
7
|
+
var TOTP_SECRET_SIZE = 20;
|
|
8
|
+
function generateTotpSecret() {
|
|
9
|
+
return new OTPAuth.Secret({ size: TOTP_SECRET_SIZE }).base32;
|
|
10
|
+
}
|
|
11
|
+
function buildTotp(params) {
|
|
12
|
+
return new OTPAuth.TOTP({
|
|
13
|
+
issuer: params.issuer,
|
|
14
|
+
label: params.label,
|
|
15
|
+
algorithm: TOTP_ALGORITHM,
|
|
16
|
+
digits: TOTP_DIGITS,
|
|
17
|
+
period: TOTP_PERIOD_SECONDS,
|
|
18
|
+
secret: OTPAuth.Secret.fromBase32(params.secret.replace(/\s/g, ""))
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function buildTotpUri(params) {
|
|
22
|
+
if (!params.secret) {
|
|
23
|
+
throw new DisInvalidArgumentError("TOTP secret is required");
|
|
24
|
+
}
|
|
25
|
+
return buildTotp(params).toString();
|
|
26
|
+
}
|
|
27
|
+
function generateTotpCode(secret, options = {}) {
|
|
28
|
+
let parsedSecret;
|
|
29
|
+
try {
|
|
30
|
+
parsedSecret = OTPAuth.Secret.fromBase32(secret.replace(/\s/g, ""));
|
|
31
|
+
} catch {
|
|
32
|
+
throw new DisInvalidArgumentError("TOTP secret is not valid base32");
|
|
33
|
+
}
|
|
34
|
+
const totp = new OTPAuth.TOTP({
|
|
35
|
+
algorithm: options.algorithm ?? TOTP_ALGORITHM,
|
|
36
|
+
digits: options.digits ?? TOTP_DIGITS,
|
|
37
|
+
period: options.period ?? TOTP_PERIOD_SECONDS,
|
|
38
|
+
secret: parsedSecret
|
|
39
|
+
});
|
|
40
|
+
return totp.generate(
|
|
41
|
+
options.timestamp !== void 0 ? { timestamp: options.timestamp } : void 0
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function buildTotpUriWithOptions(params, options = {}) {
|
|
45
|
+
if (!params.secret) {
|
|
46
|
+
throw new DisInvalidArgumentError("TOTP secret is required");
|
|
47
|
+
}
|
|
48
|
+
return new OTPAuth.TOTP({
|
|
49
|
+
issuer: params.issuer,
|
|
50
|
+
label: params.label,
|
|
51
|
+
algorithm: options.algorithm ?? TOTP_ALGORITHM,
|
|
52
|
+
digits: options.digits ?? TOTP_DIGITS,
|
|
53
|
+
period: options.period ?? TOTP_PERIOD_SECONDS,
|
|
54
|
+
secret: OTPAuth.Secret.fromBase32(params.secret.replace(/\s/g, ""))
|
|
55
|
+
}).toString();
|
|
56
|
+
}
|
|
57
|
+
function verifyTotpCode(secret, code, window = 1) {
|
|
58
|
+
try {
|
|
59
|
+
const totp = new OTPAuth.TOTP({
|
|
60
|
+
algorithm: TOTP_ALGORITHM,
|
|
61
|
+
digits: TOTP_DIGITS,
|
|
62
|
+
period: TOTP_PERIOD_SECONDS,
|
|
63
|
+
secret: OTPAuth.Secret.fromBase32(secret.replace(/\s/g, ""))
|
|
64
|
+
});
|
|
65
|
+
return totp.validate({ token: code.replace(/\s/g, ""), window }) !== null;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { TOTP_ALGORITHM, TOTP_DIGITS, TOTP_PERIOD_SECONDS, TOTP_SECRET_SIZE, buildTotpUri, buildTotpUriWithOptions, generateTotpCode, generateTotpSecret, verifyTotpCode };
|
|
72
|
+
//# sourceMappingURL=chunk-OA5ARYJM.js.map
|
|
73
|
+
//# sourceMappingURL=chunk-OA5ARYJM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/totp/index.ts"],"names":[],"mappings":"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","file":"chunk-OA5ARYJM.js","sourcesContent":["/**\r\n * dis-totp — time-based one-time passwords (RFC 6238).\r\n *\r\n * Thin wrapper over the audited `otpauth` library so applications never embed\r\n * the OTP primitive directly. Parameters are pinned to the values Singra Vault\r\n * uses (SHA-1, 6 digits, 30-second period, 160-bit secrets) so existing\r\n * enrolled authenticators keep working unchanged.\r\n *\r\n * SHA-1 here is the standardised HMAC inside the TOTP construction (RFC 6238),\r\n * which every authenticator app implements; it is not used as a hash for any\r\n * security decision elsewhere.\r\n */\r\n\r\nimport * as OTPAuth from 'otpauth';\r\nimport { DisInvalidArgumentError } from '../core/errors.js';\r\n\r\n/** TOTP parameters, fixed to remain compatible with enrolled authenticators. */\r\nexport const TOTP_ALGORITHM = 'SHA1' as const;\r\nexport const TOTP_DIGITS = 6;\r\nexport const TOTP_PERIOD_SECONDS = 30;\r\n/** Secret size in bytes (160-bit) as produced by {@link generateTotpSecret}. */\r\nexport const TOTP_SECRET_SIZE = 20;\r\n\r\n/** HMAC algorithms RFC 6238 permits and authenticator apps implement. */\r\nexport type TotpAlgorithm = 'SHA1' | 'SHA256' | 'SHA512';\r\n\r\n/**\r\n * Parameters for third-party TOTP entries (password-manager authenticator\r\n * storage). Unlike Singra's own 2FA enrolment — which is pinned to\r\n * SHA-1 / 6 digits / 30 s — imported entries carry whatever parameters the\r\n * issuing service chose, so each knob is explicit here.\r\n */\r\nexport interface TotpCodeOptions {\r\n /** HMAC algorithm. Defaults to {@link TOTP_ALGORITHM}. */\r\n readonly algorithm?: TotpAlgorithm;\r\n /** Code length. Defaults to {@link TOTP_DIGITS}. */\r\n readonly digits?: number;\r\n /** Time step in seconds. Defaults to {@link TOTP_PERIOD_SECONDS}. */\r\n readonly period?: number;\r\n /** Epoch milliseconds to generate for. Defaults to the current time. */\r\n readonly timestamp?: number;\r\n}\r\n\r\nexport interface TotpParams {\r\n /** Issuer label shown in the authenticator app. */\r\n readonly issuer: string;\r\n /** Account label (typically the user's email). */\r\n readonly label: string;\r\n /** Base32-encoded shared secret. */\r\n readonly secret: string;\r\n}\r\n\r\n/** Generates a new base32-encoded 160-bit TOTP secret. */\r\nexport function generateTotpSecret(): string {\r\n return new OTPAuth.Secret({ size: TOTP_SECRET_SIZE }).base32;\r\n}\r\n\r\nfunction buildTotp(params: TotpParams): OTPAuth.TOTP {\r\n return new OTPAuth.TOTP({\r\n issuer: params.issuer,\r\n label: params.label,\r\n algorithm: TOTP_ALGORITHM,\r\n digits: TOTP_DIGITS,\r\n period: TOTP_PERIOD_SECONDS,\r\n secret: OTPAuth.Secret.fromBase32(params.secret.replace(/\\s/g, '')),\r\n });\r\n}\r\n\r\n/** Builds the `otpauth://` provisioning URI for a QR code. */\r\nexport function buildTotpUri(params: TotpParams): string {\r\n if (!params.secret) {\r\n throw new DisInvalidArgumentError('TOTP secret is required');\r\n }\r\n return buildTotp(params).toString();\r\n}\r\n\r\n/**\r\n * Generates the TOTP code for a base32 `secret` with explicit parameters —\r\n * the primitive behind a password manager's authenticator view. Throws\r\n * {@link DisInvalidArgumentError} on a malformed secret so callers can decide\r\n * how to surface the error.\r\n */\r\nexport function generateTotpCode(secret: string, options: TotpCodeOptions = {}): string {\r\n let parsedSecret: OTPAuth.Secret;\r\n try {\r\n parsedSecret = OTPAuth.Secret.fromBase32(secret.replace(/\\s/g, ''));\r\n } catch {\r\n throw new DisInvalidArgumentError('TOTP secret is not valid base32');\r\n }\r\n const totp = new OTPAuth.TOTP({\r\n algorithm: options.algorithm ?? TOTP_ALGORITHM,\r\n digits: options.digits ?? TOTP_DIGITS,\r\n period: options.period ?? TOTP_PERIOD_SECONDS,\r\n secret: parsedSecret,\r\n });\r\n return totp.generate(\r\n options.timestamp !== undefined ? { timestamp: options.timestamp } : undefined,\r\n );\r\n}\r\n\r\n/**\r\n * Builds the `otpauth://` provisioning URI for an entry with explicit\r\n * parameters (third-party authenticator entries). Singra's own 2FA enrolment\r\n * should keep using {@link buildTotpUri}, which pins the Singra parameter set.\r\n */\r\nexport function buildTotpUriWithOptions(\r\n params: TotpParams,\r\n options: Omit<TotpCodeOptions, 'timestamp'> = {},\r\n): string {\r\n if (!params.secret) {\r\n throw new DisInvalidArgumentError('TOTP secret is required');\r\n }\r\n return new OTPAuth.TOTP({\r\n issuer: params.issuer,\r\n label: params.label,\r\n algorithm: options.algorithm ?? TOTP_ALGORITHM,\r\n digits: options.digits ?? TOTP_DIGITS,\r\n period: options.period ?? TOTP_PERIOD_SECONDS,\r\n secret: OTPAuth.Secret.fromBase32(params.secret.replace(/\\s/g, '')),\r\n }).toString();\r\n}\r\n\r\n/**\r\n * Verifies a TOTP `code` against a base32 `secret`, allowing `window` periods\r\n * of clock drift on either side (default 1 = ±30s). Returns `true` on a match.\r\n * Malformed secrets/codes return `false` rather than throwing.\r\n */\r\nexport function verifyTotpCode(\r\n secret: string,\r\n code: string,\r\n window = 1,\r\n): boolean {\r\n try {\r\n const totp = new OTPAuth.TOTP({\r\n algorithm: TOTP_ALGORITHM,\r\n digits: TOTP_DIGITS,\r\n period: TOTP_PERIOD_SECONDS,\r\n secret: OTPAuth.Secret.fromBase32(secret.replace(/\\s/g, '')),\r\n });\r\n return totp.validate({ token: code.replace(/\\s/g, ''), window }) !== null;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { KDF_SALT_LENGTH } from './chunk-BUFRR5PB.js';
|
|
2
|
+
import { bytesToBase64, base64ToBytes } from './chunk-JSKIWIEC.js';
|
|
3
|
+
import { getCryptoProvider, subtle } from './chunk-CYIGDF63.js';
|
|
4
|
+
import { DisError, DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
|
|
5
|
+
import { argon2id } from 'hash-wasm';
|
|
6
|
+
|
|
7
|
+
var DEFAULT_KDF_PARAMS = Object.freeze({
|
|
8
|
+
1: { memory: 65536, iterations: 3, parallelism: 4, hashLength: 32 },
|
|
9
|
+
2: { memory: 131072, iterations: 3, parallelism: 4, hashLength: 32 }
|
|
10
|
+
});
|
|
11
|
+
var CURRENT_KDF_VERSION = 2;
|
|
12
|
+
function generateSalt() {
|
|
13
|
+
const salt = new Uint8Array(KDF_SALT_LENGTH);
|
|
14
|
+
getCryptoProvider().getRandomValues(salt);
|
|
15
|
+
return bytesToBase64(salt);
|
|
16
|
+
}
|
|
17
|
+
async function hkdfStrengthen(argon2Output, options) {
|
|
18
|
+
const baseKey = await subtle().importKey("raw", argon2Output, "HKDF", false, [
|
|
19
|
+
"deriveBits"
|
|
20
|
+
]);
|
|
21
|
+
const info = new TextEncoder().encode(options.info);
|
|
22
|
+
const derivedBits = await subtle().deriveBits(
|
|
23
|
+
{
|
|
24
|
+
name: "HKDF",
|
|
25
|
+
hash: "SHA-256",
|
|
26
|
+
salt: options.hkdfSalt,
|
|
27
|
+
info
|
|
28
|
+
},
|
|
29
|
+
baseKey,
|
|
30
|
+
argon2Output.length * 8
|
|
31
|
+
);
|
|
32
|
+
return new Uint8Array(derivedBits);
|
|
33
|
+
}
|
|
34
|
+
async function deriveRawKey(password, saltBase64, options = {}) {
|
|
35
|
+
const version = options.version ?? CURRENT_KDF_VERSION;
|
|
36
|
+
const registry = options.params ?? DEFAULT_KDF_PARAMS;
|
|
37
|
+
const params = registry[version];
|
|
38
|
+
if (!params) {
|
|
39
|
+
throw new DisError("UNSUPPORTED_KDF_VERSION", `Unknown KDF version: ${version}`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof password !== "string" || password.length === 0) {
|
|
42
|
+
throw new DisInvalidArgumentError("password must be a non-empty string");
|
|
43
|
+
}
|
|
44
|
+
const salt = base64ToBytes(saltBase64);
|
|
45
|
+
const result = await argon2id({
|
|
46
|
+
password,
|
|
47
|
+
salt,
|
|
48
|
+
parallelism: params.parallelism,
|
|
49
|
+
iterations: params.iterations,
|
|
50
|
+
memorySize: params.memory,
|
|
51
|
+
hashLength: params.hashLength,
|
|
52
|
+
outputType: "binary"
|
|
53
|
+
});
|
|
54
|
+
let argon2Bytes;
|
|
55
|
+
if (result instanceof Uint8Array) {
|
|
56
|
+
argon2Bytes = result;
|
|
57
|
+
} else if (result instanceof ArrayBuffer) {
|
|
58
|
+
argon2Bytes = new Uint8Array(result);
|
|
59
|
+
} else {
|
|
60
|
+
throw new DisError("KEY_DERIVATION_FAILED", "Unexpected Argon2id output type");
|
|
61
|
+
}
|
|
62
|
+
if (!options.strengthen) {
|
|
63
|
+
return argon2Bytes;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await hkdfStrengthen(argon2Bytes, options.strengthen);
|
|
67
|
+
} finally {
|
|
68
|
+
argon2Bytes.fill(0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function importAesGcmKey(keyBytes) {
|
|
72
|
+
return subtle().importKey(
|
|
73
|
+
"raw",
|
|
74
|
+
keyBytes,
|
|
75
|
+
{ name: "AES-GCM", length: 256 },
|
|
76
|
+
false,
|
|
77
|
+
["encrypt", "decrypt"]
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
async function deriveAesGcmKey(password, saltBase64, options = {}) {
|
|
81
|
+
const keyBytes = await deriveRawKey(password, saltBase64, options);
|
|
82
|
+
try {
|
|
83
|
+
return await importAesGcmKey(keyBytes);
|
|
84
|
+
} finally {
|
|
85
|
+
keyBytes.fill(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function argon2idRaw(params) {
|
|
89
|
+
if (typeof params.password !== "string" || params.password.length === 0) {
|
|
90
|
+
throw new DisInvalidArgumentError("password must be a non-empty string");
|
|
91
|
+
}
|
|
92
|
+
const result = await argon2id({
|
|
93
|
+
password: params.password,
|
|
94
|
+
salt: params.salt,
|
|
95
|
+
parallelism: params.parallelism,
|
|
96
|
+
iterations: params.iterations,
|
|
97
|
+
memorySize: params.memorySize,
|
|
98
|
+
hashLength: params.hashLength,
|
|
99
|
+
outputType: "binary"
|
|
100
|
+
});
|
|
101
|
+
return new Uint8Array(result);
|
|
102
|
+
}
|
|
103
|
+
async function deriveHkdfSha256Bits(ikm, options) {
|
|
104
|
+
const baseKey = await subtle().importKey(
|
|
105
|
+
"raw",
|
|
106
|
+
ikm,
|
|
107
|
+
{ name: "HKDF" },
|
|
108
|
+
false,
|
|
109
|
+
["deriveBits"]
|
|
110
|
+
);
|
|
111
|
+
const bits = await subtle().deriveBits(
|
|
112
|
+
{
|
|
113
|
+
name: "HKDF",
|
|
114
|
+
hash: "SHA-256",
|
|
115
|
+
salt: options.salt ?? new Uint8Array(0),
|
|
116
|
+
info: options.info
|
|
117
|
+
},
|
|
118
|
+
baseKey,
|
|
119
|
+
options.lengthBits ?? 256
|
|
120
|
+
);
|
|
121
|
+
return new Uint8Array(bits);
|
|
122
|
+
}
|
|
123
|
+
async function deriveHkdfAesGcmKey(ikm, options) {
|
|
124
|
+
const baseKey = await subtle().importKey(
|
|
125
|
+
"raw",
|
|
126
|
+
ikm,
|
|
127
|
+
"HKDF",
|
|
128
|
+
false,
|
|
129
|
+
["deriveKey"]
|
|
130
|
+
);
|
|
131
|
+
return subtle().deriveKey(
|
|
132
|
+
{
|
|
133
|
+
name: "HKDF",
|
|
134
|
+
hash: "SHA-256",
|
|
135
|
+
salt: options.salt ?? new Uint8Array(0),
|
|
136
|
+
info: options.info
|
|
137
|
+
},
|
|
138
|
+
baseKey,
|
|
139
|
+
{ name: "AES-GCM", length: 256 },
|
|
140
|
+
false,
|
|
141
|
+
options.usages ?? ["encrypt", "decrypt"]
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { CURRENT_KDF_VERSION, DEFAULT_KDF_PARAMS, argon2idRaw, deriveAesGcmKey, deriveHkdfAesGcmKey, deriveHkdfSha256Bits, deriveRawKey, generateSalt, importAesGcmKey };
|
|
146
|
+
//# sourceMappingURL=chunk-OPHN2B3N.js.map
|
|
147
|
+
//# sourceMappingURL=chunk-OPHN2B3N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/kdf/index.ts"],"names":[],"mappings":"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","file":"chunk-OPHN2B3N.js","sourcesContent":["/**\r\n * dis-kdf — password-based key derivation.\r\n *\r\n * Primitive: Argon2id (via the audited `hash-wasm` implementation), with\r\n * versioned, immutable parameter sets so accounts can be transparently\r\n * upgraded to stronger parameters over time. Optional HKDF-Expand strengthening\r\n * binds the derived key to a second factor (e.g. a device key) without\r\n * weakening the password-derived material.\r\n *\r\n * DIS does not invent a KDF. Parameters follow OWASP Argon2id guidance.\r\n */\r\n\r\nimport { argon2id } from 'hash-wasm';\r\nimport { getCryptoProvider, subtle } from '../core/provider.js';\r\nimport { base64ToBytes, bytesToBase64 } from '../core/encoding.js';\r\nimport { KDF_SALT_LENGTH } from '../core/constants.js';\r\nimport {\r\n DisError,\r\n DisInvalidArgumentError,\r\n} from '../core/errors.js';\r\n\r\n/** Argon2id parameter set. Once released, a version's params are immutable. */\r\nexport interface KdfParams {\r\n /** Memory cost in KiB. */\r\n readonly memory: number;\r\n /** Iteration (time) cost. */\r\n readonly iterations: number;\r\n /** Degree of parallelism. */\r\n readonly parallelism: number;\r\n /** Output length in bytes. */\r\n readonly hashLength: number;\r\n}\r\n\r\n/**\r\n * Default versioned parameter registry. Byte-compatible with Singra Vault:\r\n * v1: 64 MiB (original baseline)\r\n * v2: 128 MiB (current; exceeds OWASP Argon2id minimum)\r\n *\r\n * IMPORTANT: never change an existing version's parameters — only add versions.\r\n */\r\nexport const DEFAULT_KDF_PARAMS: Readonly<Record<number, KdfParams>> = Object.freeze({\r\n 1: { memory: 65536, iterations: 3, parallelism: 4, hashLength: 32 },\r\n 2: { memory: 131072, iterations: 3, parallelism: 4, hashLength: 32 },\r\n});\r\n\r\n/** The latest KDF version. New accounts derive with this version. */\r\nexport const CURRENT_KDF_VERSION = 2;\r\n\r\n/** Optional second-factor strengthening via HKDF-Expand (SHA-256). */\r\nexport interface KdfStrengthenOptions {\r\n /** Salt for HKDF (e.g. a 256-bit device key). */\r\n readonly hkdfSalt: Uint8Array;\r\n /** Domain-separation `info` string for HKDF. Caller-owned (format contract). */\r\n readonly info: string;\r\n}\r\n\r\nexport interface DeriveRawKeyOptions {\r\n /** KDF version to look up in `params`. Defaults to {@link CURRENT_KDF_VERSION}. */\r\n readonly version?: number;\r\n /** Parameter registry. Defaults to {@link DEFAULT_KDF_PARAMS}. */\r\n readonly params?: Readonly<Record<number, KdfParams>>;\r\n /** Optional HKDF strengthening (e.g. device-key binding). */\r\n readonly strengthen?: KdfStrengthenOptions;\r\n}\r\n\r\n/** Generates a cryptographically secure, base64-encoded salt. */\r\nexport function generateSalt(): string {\r\n const salt = new Uint8Array(KDF_SALT_LENGTH);\r\n getCryptoProvider().getRandomValues(salt);\r\n return bytesToBase64(salt);\r\n}\r\n\r\nasync function hkdfStrengthen(\r\n argon2Output: Uint8Array,\r\n options: KdfStrengthenOptions,\r\n): Promise<Uint8Array> {\r\n const baseKey = await subtle().importKey('raw', argon2Output as BufferSource, 'HKDF', false, [\r\n 'deriveBits',\r\n ]);\r\n const info = new TextEncoder().encode(options.info);\r\n const derivedBits = await subtle().deriveBits(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: options.hkdfSalt as BufferSource,\r\n info: info as BufferSource,\r\n },\r\n baseKey,\r\n argon2Output.length * 8,\r\n );\r\n return new Uint8Array(derivedBits);\r\n}\r\n\r\n/**\r\n * Derives raw key bytes from a password using Argon2id (+ optional HKDF).\r\n *\r\n * The caller owns the returned buffer and MUST wipe it (`.fill(0)`).\r\n */\r\nexport async function deriveRawKey(\r\n password: string,\r\n saltBase64: string,\r\n options: DeriveRawKeyOptions = {},\r\n): Promise<Uint8Array> {\r\n const version = options.version ?? CURRENT_KDF_VERSION;\r\n const registry = options.params ?? DEFAULT_KDF_PARAMS;\r\n const params = registry[version];\r\n if (!params) {\r\n throw new DisError('UNSUPPORTED_KDF_VERSION', `Unknown KDF version: ${version}`);\r\n }\r\n if (typeof password !== 'string' || password.length === 0) {\r\n throw new DisInvalidArgumentError('password must be a non-empty string');\r\n }\r\n\r\n const salt = base64ToBytes(saltBase64);\r\n const result = (await argon2id({\r\n password,\r\n salt,\r\n parallelism: params.parallelism,\r\n iterations: params.iterations,\r\n memorySize: params.memory,\r\n hashLength: params.hashLength,\r\n outputType: 'binary',\r\n })) as unknown;\r\n\r\n let argon2Bytes: Uint8Array;\r\n if (result instanceof Uint8Array) {\r\n argon2Bytes = result;\r\n } else if (result instanceof ArrayBuffer) {\r\n argon2Bytes = new Uint8Array(result);\r\n } else {\r\n throw new DisError('KEY_DERIVATION_FAILED', 'Unexpected Argon2id output type');\r\n }\r\n\r\n if (!options.strengthen) {\r\n return argon2Bytes;\r\n }\r\n\r\n try {\r\n return await hkdfStrengthen(argon2Bytes, options.strengthen);\r\n } finally {\r\n argon2Bytes.fill(0);\r\n }\r\n}\r\n\r\n/** Imports raw 256-bit key bytes as a non-extractable AES-GCM CryptoKey. */\r\nexport async function importAesGcmKey(keyBytes: Uint8Array | BufferSource): Promise<CryptoKey> {\r\n return subtle().importKey(\r\n 'raw',\r\n keyBytes as BufferSource,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['encrypt', 'decrypt'],\r\n );\r\n}\r\n\r\n/**\r\n * Derives a non-extractable AES-GCM key from a password. Raw key bytes are\r\n * wiped as soon as the CryptoKey is imported.\r\n */\r\nexport async function deriveAesGcmKey(\r\n password: string,\r\n saltBase64: string,\r\n options: DeriveRawKeyOptions = {},\r\n): Promise<CryptoKey> {\r\n const keyBytes = await deriveRawKey(password, saltBase64, options);\r\n try {\r\n return await importAesGcmKey(keyBytes);\r\n } finally {\r\n keyBytes.fill(0);\r\n }\r\n}\r\n\r\n/** Explicit Argon2id parameters for a single ad-hoc derivation. */\r\nexport interface Argon2idRawParams {\r\n readonly password: string;\r\n readonly salt: Uint8Array;\r\n /** Memory cost in KiB. */\r\n readonly memorySize: number;\r\n readonly iterations: number;\r\n readonly parallelism: number;\r\n readonly hashLength: number;\r\n}\r\n\r\n/**\r\n * Low-level Argon2id derivation returning raw key bytes. This is the audited\r\n * `hash-wasm` Argon2id with caller-chosen parameters and a raw byte salt — for\r\n * call sites that derive a key with a context-specific salt/param set (device\r\n * key transfer wrapping, integrity HMAC key) rather than the versioned account\r\n * KDF registry used by {@link deriveRawKey}.\r\n */\r\nexport async function argon2idRaw(params: Argon2idRawParams): Promise<Uint8Array> {\r\n if (typeof params.password !== 'string' || params.password.length === 0) {\r\n throw new DisInvalidArgumentError('password must be a non-empty string');\r\n }\r\n const result = await argon2id({\r\n password: params.password,\r\n salt: params.salt,\r\n parallelism: params.parallelism,\r\n iterations: params.iterations,\r\n memorySize: params.memorySize,\r\n hashLength: params.hashLength,\r\n outputType: 'binary',\r\n });\r\n return new Uint8Array(result);\r\n}\r\n\r\n/**\r\n * HKDF-SHA-256 expand/extract producing `lengthBits` of key material.\r\n *\r\n * `ikm` is the input keying material, `salt` defaults to the empty salt (the\r\n * op-log record/snapshot derivations use an empty salt and put all context in\r\n * `info`; the device-key derivation uses the device key as salt).\r\n */\r\nexport async function deriveHkdfSha256Bits(\r\n ikm: Uint8Array,\r\n options: { info: Uint8Array; salt?: Uint8Array; lengthBits?: number },\r\n): Promise<Uint8Array> {\r\n const baseKey = await subtle().importKey(\r\n 'raw',\r\n ikm as BufferSource,\r\n { name: 'HKDF' },\r\n false,\r\n ['deriveBits'],\r\n );\r\n const bits = await subtle().deriveBits(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: (options.salt ?? new Uint8Array(0)) as BufferSource,\r\n info: options.info as BufferSource,\r\n },\r\n baseKey,\r\n options.lengthBits ?? 256,\r\n );\r\n return new Uint8Array(bits);\r\n}\r\n\r\n/**\r\n * HKDF-SHA-256 deriving directly into a non-extractable AES-256-GCM key,\r\n * mirroring `crypto.subtle.deriveKey` (used by passkey PRF wrapping and the\r\n * legacy device-key wrapping path).\r\n */\r\nexport async function deriveHkdfAesGcmKey(\r\n ikm: Uint8Array,\r\n options: { info: Uint8Array; salt?: Uint8Array; usages?: KeyUsage[] },\r\n): Promise<CryptoKey> {\r\n const baseKey = await subtle().importKey(\r\n 'raw',\r\n ikm as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey'],\r\n );\r\n return subtle().deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: (options.salt ?? new Uint8Array(0)) as BufferSource,\r\n info: options.info as BufferSource,\r\n },\r\n baseKey,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n options.usages ?? ['encrypt', 'decrypt'],\r\n );\r\n}\r\n"]}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getCryptoProvider } from './chunk-CYIGDF63.js';
|
|
2
|
+
import { DisInvalidArgumentError, DisError } from './chunk-MJO7IJZC.js';
|
|
3
|
+
|
|
4
|
+
// src/secure-memory/index.ts
|
|
5
|
+
var cleanupRegistry = new FinalizationRegistry((buffer) => {
|
|
6
|
+
try {
|
|
7
|
+
buffer.fill(0);
|
|
8
|
+
} catch {
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
function assertLive(destroyed) {
|
|
12
|
+
if (destroyed) {
|
|
13
|
+
throw new DisError("USE_AFTER_DESTROY", "SecureBuffer has been destroyed");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
var SecureBuffer = class _SecureBuffer {
|
|
17
|
+
buffer;
|
|
18
|
+
destroyed = false;
|
|
19
|
+
constructor(size) {
|
|
20
|
+
if (size <= 0 || !Number.isInteger(size)) {
|
|
21
|
+
throw new DisInvalidArgumentError("SecureBuffer size must be a positive integer");
|
|
22
|
+
}
|
|
23
|
+
this.buffer = new Uint8Array(size);
|
|
24
|
+
cleanupRegistry.register(this, this.buffer, this);
|
|
25
|
+
}
|
|
26
|
+
/** Copies `bytes` into a new SecureBuffer. The source is NOT auto-zeroed. */
|
|
27
|
+
static fromBytes(bytes) {
|
|
28
|
+
const secure = new _SecureBuffer(bytes.length || 1);
|
|
29
|
+
if (bytes.length === 0) {
|
|
30
|
+
secure.destroy();
|
|
31
|
+
throw new DisInvalidArgumentError("Cannot create SecureBuffer from empty bytes");
|
|
32
|
+
}
|
|
33
|
+
secure.buffer.set(bytes);
|
|
34
|
+
return secure;
|
|
35
|
+
}
|
|
36
|
+
/** Builds a SecureBuffer from a hex string (spaces/dashes allowed). */
|
|
37
|
+
static fromHex(hex) {
|
|
38
|
+
const cleanHex = hex.replace(/[\s-]/g, "");
|
|
39
|
+
if (cleanHex.length === 0 || cleanHex.length % 2 !== 0) {
|
|
40
|
+
throw new DisInvalidArgumentError("Hex string must have a positive even length");
|
|
41
|
+
}
|
|
42
|
+
const secure = new _SecureBuffer(cleanHex.length / 2);
|
|
43
|
+
for (let i = 0; i < secure.buffer.length; i++) {
|
|
44
|
+
const value = parseInt(cleanHex.substr(i * 2, 2), 16);
|
|
45
|
+
if (Number.isNaN(value)) {
|
|
46
|
+
secure.destroy();
|
|
47
|
+
throw new DisInvalidArgumentError(`Invalid hex byte at position ${i * 2}`);
|
|
48
|
+
}
|
|
49
|
+
secure.buffer[i] = value;
|
|
50
|
+
}
|
|
51
|
+
return secure;
|
|
52
|
+
}
|
|
53
|
+
/** Allocates a SecureBuffer filled with CSPRNG bytes. */
|
|
54
|
+
static random(size) {
|
|
55
|
+
const secure = new _SecureBuffer(size);
|
|
56
|
+
getCryptoProvider().getRandomValues(secure.buffer);
|
|
57
|
+
return secure;
|
|
58
|
+
}
|
|
59
|
+
/** Synchronous controlled access. Do not retain the buffer past `fn`. */
|
|
60
|
+
use(fn) {
|
|
61
|
+
assertLive(this.destroyed);
|
|
62
|
+
return fn(this.buffer);
|
|
63
|
+
}
|
|
64
|
+
/** Asynchronous controlled access. */
|
|
65
|
+
async useAsync(fn) {
|
|
66
|
+
assertLive(this.destroyed);
|
|
67
|
+
return fn(this.buffer);
|
|
68
|
+
}
|
|
69
|
+
get size() {
|
|
70
|
+
assertLive(this.destroyed);
|
|
71
|
+
return this.buffer.length;
|
|
72
|
+
}
|
|
73
|
+
get isDestroyed() {
|
|
74
|
+
return this.destroyed;
|
|
75
|
+
}
|
|
76
|
+
/** Zeros the buffer and marks it destroyed. Idempotent. */
|
|
77
|
+
destroy() {
|
|
78
|
+
if (this.destroyed) return;
|
|
79
|
+
this.buffer.fill(0);
|
|
80
|
+
cleanupRegistry.unregister(this);
|
|
81
|
+
this.destroyed = true;
|
|
82
|
+
}
|
|
83
|
+
/** Returns a mutable copy of the contents (caller must zero it). */
|
|
84
|
+
toBytes() {
|
|
85
|
+
assertLive(this.destroyed);
|
|
86
|
+
return new Uint8Array(this.buffer);
|
|
87
|
+
}
|
|
88
|
+
/** Constant-time equality comparison against another buffer. */
|
|
89
|
+
equals(other) {
|
|
90
|
+
assertLive(this.destroyed);
|
|
91
|
+
const otherBytes = other instanceof _SecureBuffer ? other.buffer : other;
|
|
92
|
+
if (this.buffer.length !== otherBytes.length) return false;
|
|
93
|
+
let result = 0;
|
|
94
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
95
|
+
result |= this.buffer[i] ^ otherBytes[i];
|
|
96
|
+
}
|
|
97
|
+
return result === 0;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
async function withSecureBuffer(bytes, fn) {
|
|
101
|
+
const secure = SecureBuffer.fromBytes(bytes);
|
|
102
|
+
try {
|
|
103
|
+
return await fn(secure);
|
|
104
|
+
} finally {
|
|
105
|
+
secure.destroy();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function zeroBuffers(...buffers) {
|
|
109
|
+
for (const buffer of buffers) {
|
|
110
|
+
buffer?.fill(0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { SecureBuffer, withSecureBuffer, zeroBuffers };
|
|
115
|
+
//# sourceMappingURL=chunk-RTAJJZKO.js.map
|
|
116
|
+
//# sourceMappingURL=chunk-RTAJJZKO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/secure-memory/index.ts"],"names":[],"mappings":"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","file":"chunk-RTAJJZKO.js","sourcesContent":["/**\r\n * dis-secure-storage / secure-memory — memory-safe handling of key material.\r\n *\r\n * Ported from Singra Vault's SecureBuffer. Provides controlled, callback-scoped\r\n * access to sensitive bytes with explicit zeroing on destroy and a\r\n * FinalizationRegistry fallback. This is defense-in-depth: JavaScript cannot\r\n * guarantee memory wiping (GC is non-deterministic, strings are immutable), but\r\n * controlled access plus best-effort zeroing materially reduces exposure\r\n * (cf. KeePass CVE-2023-32784).\r\n */\r\n\r\nimport { getCryptoProvider } from '../core/provider.js';\r\nimport { DisError, DisInvalidArgumentError } from '../core/errors.js';\r\n\r\nconst cleanupRegistry = new FinalizationRegistry<Uint8Array>((buffer) => {\r\n try {\r\n buffer.fill(0);\r\n } catch {\r\n // Buffer may already be detached or GC'd.\r\n }\r\n});\r\n\r\nfunction assertLive(destroyed: boolean): void {\r\n if (destroyed) {\r\n throw new DisError('USE_AFTER_DESTROY', 'SecureBuffer has been destroyed');\r\n }\r\n}\r\n\r\n/** A wrapper for sensitive binary data with controlled access and zeroing. */\r\nexport class SecureBuffer {\r\n private buffer: Uint8Array;\r\n private destroyed = false;\r\n\r\n constructor(size: number) {\r\n if (size <= 0 || !Number.isInteger(size)) {\r\n throw new DisInvalidArgumentError('SecureBuffer size must be a positive integer');\r\n }\r\n this.buffer = new Uint8Array(size);\r\n cleanupRegistry.register(this, this.buffer, this);\r\n }\r\n\r\n /** Copies `bytes` into a new SecureBuffer. The source is NOT auto-zeroed. */\r\n static fromBytes(bytes: Uint8Array): SecureBuffer {\r\n const secure = new SecureBuffer(bytes.length || 1);\r\n if (bytes.length === 0) {\r\n secure.destroy();\r\n throw new DisInvalidArgumentError('Cannot create SecureBuffer from empty bytes');\r\n }\r\n secure.buffer.set(bytes);\r\n return secure;\r\n }\r\n\r\n /** Builds a SecureBuffer from a hex string (spaces/dashes allowed). */\r\n static fromHex(hex: string): SecureBuffer {\r\n const cleanHex = hex.replace(/[\\s-]/g, '');\r\n if (cleanHex.length === 0 || cleanHex.length % 2 !== 0) {\r\n throw new DisInvalidArgumentError('Hex string must have a positive even length');\r\n }\r\n const secure = new SecureBuffer(cleanHex.length / 2);\r\n for (let i = 0; i < secure.buffer.length; i++) {\r\n const value = parseInt(cleanHex.substr(i * 2, 2), 16);\r\n if (Number.isNaN(value)) {\r\n secure.destroy();\r\n throw new DisInvalidArgumentError(`Invalid hex byte at position ${i * 2}`);\r\n }\r\n secure.buffer[i] = value;\r\n }\r\n return secure;\r\n }\r\n\r\n /** Allocates a SecureBuffer filled with CSPRNG bytes. */\r\n static random(size: number): SecureBuffer {\r\n const secure = new SecureBuffer(size);\r\n getCryptoProvider().getRandomValues(secure.buffer);\r\n return secure;\r\n }\r\n\r\n /** Synchronous controlled access. Do not retain the buffer past `fn`. */\r\n use<T>(fn: (data: Uint8Array) => T): T {\r\n assertLive(this.destroyed);\r\n return fn(this.buffer);\r\n }\r\n\r\n /** Asynchronous controlled access. */\r\n async useAsync<T>(fn: (data: Uint8Array) => Promise<T>): Promise<T> {\r\n assertLive(this.destroyed);\r\n return fn(this.buffer);\r\n }\r\n\r\n get size(): number {\r\n assertLive(this.destroyed);\r\n return this.buffer.length;\r\n }\r\n\r\n get isDestroyed(): boolean {\r\n return this.destroyed;\r\n }\r\n\r\n /** Zeros the buffer and marks it destroyed. Idempotent. */\r\n destroy(): void {\r\n if (this.destroyed) return;\r\n this.buffer.fill(0);\r\n cleanupRegistry.unregister(this);\r\n this.destroyed = true;\r\n }\r\n\r\n /** Returns a mutable copy of the contents (caller must zero it). */\r\n toBytes(): Uint8Array {\r\n assertLive(this.destroyed);\r\n return new Uint8Array(this.buffer);\r\n }\r\n\r\n /** Constant-time equality comparison against another buffer. */\r\n equals(other: SecureBuffer | Uint8Array): boolean {\r\n assertLive(this.destroyed);\r\n const otherBytes = other instanceof SecureBuffer ? other.buffer : other;\r\n if (this.buffer.length !== otherBytes.length) return false;\r\n let result = 0;\r\n for (let i = 0; i < this.buffer.length; i++) {\r\n result |= this.buffer[i]! ^ otherBytes[i]!;\r\n }\r\n return result === 0;\r\n }\r\n}\r\n\r\n/** Runs `fn` with a temporary SecureBuffer that is always destroyed afterwards. */\r\nexport async function withSecureBuffer<T>(\r\n bytes: Uint8Array,\r\n fn: (secure: SecureBuffer) => Promise<T>,\r\n): Promise<T> {\r\n const secure = SecureBuffer.fromBytes(bytes);\r\n try {\r\n return await fn(secure);\r\n } finally {\r\n secure.destroy();\r\n }\r\n}\r\n\r\n/** Zeros multiple buffers. Convenience for `finally` cleanup blocks. */\r\nexport function zeroBuffers(...buffers: (Uint8Array | null | undefined)[]): void {\r\n for (const buffer of buffers) {\r\n buffer?.fill(0);\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DisInvalidArgumentError, DisUnsupportedFormatVersionError } from './chunk-MJO7IJZC.js';
|
|
2
|
+
|
|
3
|
+
// src/format-versioning/index.ts
|
|
4
|
+
function formatEnvelope(spec, encryptedBase64) {
|
|
5
|
+
if (!encryptedBase64) {
|
|
6
|
+
throw new DisInvalidArgumentError(`Empty ${spec.subject} ciphertext`);
|
|
7
|
+
}
|
|
8
|
+
return `${spec.currentPrefix}${encryptedBase64}`;
|
|
9
|
+
}
|
|
10
|
+
function parseEnvelope(spec, encryptedData) {
|
|
11
|
+
if (encryptedData.startsWith(spec.currentPrefix)) {
|
|
12
|
+
const payload = encryptedData.slice(spec.currentPrefix.length);
|
|
13
|
+
if (!payload) {
|
|
14
|
+
throw new DisInvalidArgumentError(`Invalid ${spec.subject} encryption envelope`);
|
|
15
|
+
}
|
|
16
|
+
return { version: 1, payload };
|
|
17
|
+
}
|
|
18
|
+
if (encryptedData.startsWith(spec.familyPrefix)) {
|
|
19
|
+
throw new DisUnsupportedFormatVersionError(
|
|
20
|
+
`Unsupported ${spec.subject} encryption envelope version`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return { version: "legacy", payload: encryptedData };
|
|
24
|
+
}
|
|
25
|
+
function isCurrentEnvelope(spec, encryptedData) {
|
|
26
|
+
try {
|
|
27
|
+
return parseEnvelope(spec, encryptedData).version === 1;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { formatEnvelope, isCurrentEnvelope, parseEnvelope };
|
|
34
|
+
//# sourceMappingURL=chunk-SCHZI6YY.js.map
|
|
35
|
+
//# sourceMappingURL=chunk-SCHZI6YY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/format-versioning/index.ts"],"names":[],"mappings":";;;AA4BO,SAAS,cAAA,CAAe,MAAmC,eAAA,EAAiC;AAC/F,EAAA,IAAI,CAAC,eAAA,EAAiB;AAClB,IAAA,MAAM,IAAI,uBAAA,CAAwB,CAAA,MAAA,EAAS,IAAA,CAAK,OAAO,CAAA,WAAA,CAAa,CAAA;AAAA,EACxE;AACA,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,aAAa,CAAA,EAAG,eAAe,CAAA,CAAA;AAClD;AAMO,SAAS,aAAA,CACZ,MACA,aAAA,EACuB;AACvB,EAAA,IAAI,aAAA,CAAc,UAAA,CAAW,IAAA,CAAK,aAAa,CAAA,EAAG;AAC9C,IAAA,MAAM,OAAA,GAAU,aAAA,CAAc,KAAA,CAAM,IAAA,CAAK,cAAc,MAAM,CAAA;AAC7D,IAAA,IAAI,CAAC,OAAA,EAAS;AACV,MAAA,MAAM,IAAI,uBAAA,CAAwB,CAAA,QAAA,EAAW,IAAA,CAAK,OAAO,CAAA,oBAAA,CAAsB,CAAA;AAAA,IACnF;AACA,IAAA,OAAO,EAAE,OAAA,EAAS,CAAA,EAAG,OAAA,EAAQ;AAAA,EACjC;AACA,EAAA,IAAI,aAAA,CAAc,UAAA,CAAW,IAAA,CAAK,YAAY,CAAA,EAAG;AAC7C,IAAA,MAAM,IAAI,gCAAA;AAAA,MACN,CAAA,YAAA,EAAe,KAAK,OAAO,CAAA,4BAAA;AAAA,KAC/B;AAAA,EACJ;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,QAAA,EAAU,OAAA,EAAS,aAAA,EAAc;AACvD;AAGO,SAAS,iBAAA,CACZ,MACA,aAAA,EACO;AACP,EAAA,IAAI;AACA,IAAA,OAAO,aAAA,CAAc,IAAA,EAAM,aAAa,CAAA,CAAE,OAAA,KAAY,CAAA;AAAA,EAC1D,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,KAAA;AAAA,EACX;AACJ","file":"chunk-SCHZI6YY.js","sourcesContent":["/**\r\n * dis-format-versioning — versioned, prefix-tagged cipher envelopes.\r\n *\r\n * Every persisted ciphertext carries an explicit, human-readable version\r\n * prefix (e.g. `sv-vault-v1:`). Parsing dispatches strictly by prefix and\r\n * fails closed for unknown versions in the same family, so a future format can\r\n * never be silently misread as a legacy payload. Payloads with no recognised\r\n * prefix are reported as `legacy`, letting callers decide whether to allow them\r\n * (e.g. only on an explicit migration path).\r\n */\r\n\r\nimport { DisInvalidArgumentError, DisUnsupportedFormatVersionError } from '../core/errors.js';\r\n\r\n/** Describes a family of versioned envelopes and its current prefix. */\r\nexport interface VersionedCipherEnvelopeSpec {\r\n /** The prefix written for the current version, e.g. `sv-vault-v1:`. */\r\n readonly currentPrefix: string;\r\n /** The shared family prefix, e.g. `sv-vault-`. */\r\n readonly familyPrefix: string;\r\n /** Human-readable subject used in error messages, e.g. `vault item`. */\r\n readonly subject: string;\r\n}\r\n\r\nexport type VersionedCipherEnvelope =\r\n | { readonly version: 1; readonly payload: string }\r\n | { readonly version: 'legacy'; readonly payload: string };\r\n\r\n/** Wraps a base64 ciphertext in the spec's current version prefix. */\r\nexport function formatEnvelope(spec: VersionedCipherEnvelopeSpec, encryptedBase64: string): string {\r\n if (!encryptedBase64) {\r\n throw new DisInvalidArgumentError(`Empty ${spec.subject} ciphertext`);\r\n }\r\n return `${spec.currentPrefix}${encryptedBase64}`;\r\n}\r\n\r\n/**\r\n * Parses an envelope. Returns `version: 1` for the current prefix, throws for\r\n * an unknown in-family version, and returns `version: 'legacy'` otherwise.\r\n */\r\nexport function parseEnvelope(\r\n spec: VersionedCipherEnvelopeSpec,\r\n encryptedData: string,\r\n): VersionedCipherEnvelope {\r\n if (encryptedData.startsWith(spec.currentPrefix)) {\r\n const payload = encryptedData.slice(spec.currentPrefix.length);\r\n if (!payload) {\r\n throw new DisInvalidArgumentError(`Invalid ${spec.subject} encryption envelope`);\r\n }\r\n return { version: 1, payload };\r\n }\r\n if (encryptedData.startsWith(spec.familyPrefix)) {\r\n throw new DisUnsupportedFormatVersionError(\r\n `Unsupported ${spec.subject} encryption envelope version`,\r\n );\r\n }\r\n return { version: 'legacy', payload: encryptedData };\r\n}\r\n\r\n/** True if `encryptedData` is in the spec's current (v1) envelope format. */\r\nexport function isCurrentEnvelope(\r\n spec: VersionedCipherEnvelopeSpec,\r\n encryptedData: string,\r\n): boolean {\r\n try {\r\n return parseEnvelope(spec, encryptedData).version === 1;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n"]}
|