@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.
Files changed (92) hide show
  1. package/LICENSE +140 -0
  2. package/README.md +106 -0
  3. package/dist/aead/index.d.ts +37 -0
  4. package/dist/aead/index.js +7 -0
  5. package/dist/aead/index.js.map +1 -0
  6. package/dist/asymmetric/index.d.ts +32 -0
  7. package/dist/asymmetric/index.js +6 -0
  8. package/dist/asymmetric/index.js.map +1 -0
  9. package/dist/chunk-3DQPQCAR.js +114 -0
  10. package/dist/chunk-3DQPQCAR.js.map +1 -0
  11. package/dist/chunk-3HCT6A2P.js +55 -0
  12. package/dist/chunk-3HCT6A2P.js.map +1 -0
  13. package/dist/chunk-AB2WZ7Y2.js +57 -0
  14. package/dist/chunk-AB2WZ7Y2.js.map +1 -0
  15. package/dist/chunk-BUFRR5PB.js +9 -0
  16. package/dist/chunk-BUFRR5PB.js.map +1 -0
  17. package/dist/chunk-CYIGDF63.js +30 -0
  18. package/dist/chunk-CYIGDF63.js.map +1 -0
  19. package/dist/chunk-EOXWR7DS.js +153 -0
  20. package/dist/chunk-EOXWR7DS.js.map +1 -0
  21. package/dist/chunk-FUDDBD2G.js +43 -0
  22. package/dist/chunk-FUDDBD2G.js.map +1 -0
  23. package/dist/chunk-JSKIWIEC.js +56 -0
  24. package/dist/chunk-JSKIWIEC.js.map +1 -0
  25. package/dist/chunk-JVFP2GAO.js +66 -0
  26. package/dist/chunk-JVFP2GAO.js.map +1 -0
  27. package/dist/chunk-KNCZMIZA.js +55 -0
  28. package/dist/chunk-KNCZMIZA.js.map +1 -0
  29. package/dist/chunk-MJO7IJZC.js +44 -0
  30. package/dist/chunk-MJO7IJZC.js.map +1 -0
  31. package/dist/chunk-MPWYZXW7.js +66 -0
  32. package/dist/chunk-MPWYZXW7.js.map +1 -0
  33. package/dist/chunk-OA5ARYJM.js +73 -0
  34. package/dist/chunk-OA5ARYJM.js.map +1 -0
  35. package/dist/chunk-OPHN2B3N.js +147 -0
  36. package/dist/chunk-OPHN2B3N.js.map +1 -0
  37. package/dist/chunk-RTAJJZKO.js +116 -0
  38. package/dist/chunk-RTAJJZKO.js.map +1 -0
  39. package/dist/chunk-SCHZI6YY.js +35 -0
  40. package/dist/chunk-SCHZI6YY.js.map +1 -0
  41. package/dist/chunk-T3IV7SHD.js +388 -0
  42. package/dist/chunk-T3IV7SHD.js.map +1 -0
  43. package/dist/chunk-U65A4HIY.js +133 -0
  44. package/dist/chunk-U65A4HIY.js.map +1 -0
  45. package/dist/core/index.d.ts +20 -0
  46. package/dist/core/index.js +6 -0
  47. package/dist/core/index.js.map +1 -0
  48. package/dist/encoding-B-cb7Duu.d.ts +37 -0
  49. package/dist/errors-C79jA9vX.d.ts +65 -0
  50. package/dist/file-encryption/index.d.ts +135 -0
  51. package/dist/file-encryption/index.js +11 -0
  52. package/dist/file-encryption/index.js.map +1 -0
  53. package/dist/format-versioning/index.d.ts +37 -0
  54. package/dist/format-versioning/index.js +4 -0
  55. package/dist/format-versioning/index.js.map +1 -0
  56. package/dist/index.d.ts +27 -0
  57. package/dist/index.js +24 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/integrity/index.d.ts +46 -0
  60. package/dist/integrity/index.js +6 -0
  61. package/dist/integrity/index.js.map +1 -0
  62. package/dist/kdf/index.d.ts +104 -0
  63. package/dist/kdf/index.js +7 -0
  64. package/dist/kdf/index.js.map +1 -0
  65. package/dist/key-management/index.d.ts +69 -0
  66. package/dist/key-management/index.js +10 -0
  67. package/dist/key-management/index.js.map +1 -0
  68. package/dist/migrations/index.d.ts +41 -0
  69. package/dist/migrations/index.js +4 -0
  70. package/dist/migrations/index.js.map +1 -0
  71. package/dist/post-quantum/index.d.ts +153 -0
  72. package/dist/post-quantum/index.js +3 -0
  73. package/dist/post-quantum/index.js.map +1 -0
  74. package/dist/random/index.d.ts +28 -0
  75. package/dist/random/index.js +5 -0
  76. package/dist/random/index.js.map +1 -0
  77. package/dist/secure-memory/index.d.ts +40 -0
  78. package/dist/secure-memory/index.js +5 -0
  79. package/dist/secure-memory/index.js.map +1 -0
  80. package/dist/signing/index.d.ts +41 -0
  81. package/dist/signing/index.js +5 -0
  82. package/dist/signing/index.js.map +1 -0
  83. package/dist/totp/index.d.ts +69 -0
  84. package/dist/totp/index.js +4 -0
  85. package/dist/totp/index.js.map +1 -0
  86. package/dist/vault-crypto/index.d.ts +225 -0
  87. package/dist/vault-crypto/index.js +465 -0
  88. package/dist/vault-crypto/index.js.map +1 -0
  89. package/dist/vault-encryption/index.d.ts +40 -0
  90. package/dist/vault-encryption/index.js +9 -0
  91. package/dist/vault-encryption/index.js.map +1 -0
  92. package/package.json +137 -0
package/LICENSE ADDED
@@ -0,0 +1,140 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree
8
+ to them as both strict obligations and conditions to all
9
+ your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the
14
+ software to do everything you might do with the software
15
+ that would otherwise infringe the licensor's copyright
16
+ in it for any permitted purpose. However, you may
17
+ only distribute the software according to [Distribution
18
+ License](#distribution-license) and make changes or new works
19
+ based on the software according to [Changes and New Works
20
+ License](#changes-and-new-works-license).
21
+
22
+ ## Distribution License
23
+
24
+ The licensor grants you an additional copyright license
25
+ to distribute copies of the software. Your license
26
+ to distribute covers distributing the software with
27
+ changes and new works permitted by [Changes and New Works
28
+ License](#changes-and-new-works-license).
29
+
30
+ ## Notices
31
+
32
+ You must ensure that anyone who gets a copy of any part of
33
+ the software from you also gets a copy of these terms or the
34
+ URL for them above, as well as copies of any plain-text lines
35
+ beginning with `Required Notice:` that the licensor provided
36
+ with the software. For example:
37
+
38
+ > Required Notice: Copyright Maunting Studios (https://github.com/einmalmaik/dis)
39
+
40
+ ## Changes and New Works License
41
+
42
+ The licensor grants you an additional copyright license to
43
+ make changes and new works based on the software for any
44
+ permitted purpose.
45
+
46
+ ## Patent License
47
+
48
+ The licensor grants you a patent license for the software that
49
+ covers patent claims the licensor can license, or becomes able
50
+ to license, that you would infringe by using the software.
51
+
52
+ ## Noncommercial Purposes
53
+
54
+ Any noncommercial purpose is a permitted purpose.
55
+
56
+ ## Personal Uses
57
+
58
+ Personal use for research, experiment, and testing for
59
+ the benefit of public knowledge, personal study, private
60
+ entertainment, hobby projects, amateur pursuits, or religious
61
+ observance, without any anticipated commercial application,
62
+ is use for a permitted purpose.
63
+
64
+ ## Noncommercial Organizations
65
+
66
+ Use by any charitable organization, educational institution,
67
+ public research organization, public safety or health
68
+ organization, environmental protection organization,
69
+ or government institution is use for a permitted purpose
70
+ regardless of the source of funding or obligations resulting
71
+ from the funding.
72
+
73
+ ## Fair Use
74
+
75
+ You may have "fair use" rights for the software under the
76
+ law. These terms do not limit them.
77
+
78
+ ## No Other Rights
79
+
80
+ These terms do not allow you to sublicense or transfer any of
81
+ your licenses to anyone else, or prevent the licensor from
82
+ granting licenses to anyone else. These terms do not imply
83
+ any other licenses.
84
+
85
+ ## Patent Defense
86
+
87
+ If you make any written claim that the software infringes or
88
+ contributes to infringement of any patent, your patent license
89
+ for the software granted under these terms ends immediately. If
90
+ your company makes such a claim, your patent license ends
91
+ immediately for work on behalf of your company.
92
+
93
+ ## Violations
94
+
95
+ The first time you are notified in writing that you have
96
+ violated any of these terms, or done anything with the software
97
+ not covered by your licenses, your licenses can nonetheless
98
+ continue if you come into full compliance with these terms,
99
+ and take practical steps to correct past violations, within
100
+ 32 days of receiving notice. Otherwise, all your licenses
101
+ end immediately.
102
+
103
+ ## No Liability
104
+
105
+ As far as the law allows, the software comes as is, without
106
+ any warranty or condition, and the licensor will not be
107
+ liable to you for any damages arising out of these terms or
108
+ the use or nature of the software, under any kind of legal
109
+ claim.
110
+
111
+ ## Definitions
112
+
113
+ The **licensor** is the individual or entity offering these
114
+ terms, and the **software** is the software the licensor makes
115
+ available under these terms.
116
+
117
+ **You** refers to the individual or entity agreeing to these
118
+ terms.
119
+
120
+ **Your company** is any legal entity, sole proprietorship,
121
+ or other kind of organization that you work for, plus all
122
+ organizations that have control over, are under the control of,
123
+ or are under common control with that organization. **Control**
124
+ means ownership of substantially all the assets of an entity,
125
+ or the power to direct its management and policies by vote,
126
+ contract, or otherwise. Control can be direct or indirect.
127
+
128
+ **Your licenses** are all the licenses granted to you for the
129
+ software under these terms.
130
+
131
+ **Use** means anything you do with the software requiring one
132
+ of your licenses.
133
+
134
+ ---
135
+
136
+ Required Notice: Copyright (c) 2025-2026 Maunting Studios
137
+
138
+ For commercial licensing of DIS — Defensive Integration Shield,
139
+ contact the licensor. See `docs/licensing.md` for the full
140
+ licensing rationale and dual-licensing options.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # DIS — Defensive Integration Shield
2
+
3
+ > **Powered by DIS — Defensive Integration Shield**
4
+
5
+ DIS is a central, framework-agnostic cryptographic security platform. It is the
6
+ single place where encryption, key management, secure data formats, KDFs,
7
+ versioning, and future migrations live — so that consuming applications
8
+ (Singra Vault, Singra Premium, and future projects) hold **no direct crypto
9
+ logic** and depend only on stable, versioned public APIs.
10
+
11
+ DIS **does not invent cryptography**. It composes audited, established
12
+ primitives:
13
+
14
+ | Concern | Primitive | Source |
15
+ | --- | --- | --- |
16
+ | Password KDF | Argon2id (versioned params) | [`hash-wasm`](https://github.com/Daninet/hash-wasm) |
17
+ | Authenticated encryption | AES-256-GCM (96-bit IV, 128-bit tag) | WebCrypto `SubtleCrypto` |
18
+ | Key separation / wrapping | HKDF-SHA-256 + AES-GCM | WebCrypto |
19
+ | Random | CSPRNG | WebCrypto `getRandomValues` |
20
+ | Post-quantum (optional) | ML-KEM-768 hybrid | [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) |
21
+
22
+ ## Status
23
+
24
+ This is the **foundation release** (`0.1.0`). The pure, portable primitives
25
+ (encoding, random, secure memory, KDF, AEAD, vault-entry encryption, key
26
+ wrapping/rotation, chunked file encryption, integrity, format versioning,
27
+ migration framework) are implemented and tested. The post-quantum hybrid
28
+ sharing layer and the application cutover are tracked in
29
+ [`docs/migration-plan.md`](docs/migration-plan.md).
30
+
31
+ ## Branding
32
+
33
+ DIS exports a `DIS_BRANDING` string constant for applications that want to
34
+ display a "Powered by DIS — Defensive Integration Shield" badge. This is
35
+ optional; the branding requirements live in [`docs/licensing.md`](docs/licensing.md).
36
+
37
+ ```ts
38
+ import { DIS_BRANDING } from '@dis/shield';
39
+ // 'Powered by DIS — Defensive Integration Shield'
40
+ ```
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install @dis/shield
46
+ # Optional, only if you use the post-quantum sharing module:
47
+ npm install @noble/post-quantum
48
+ ```
49
+
50
+ Requires Node `>=20.19.0` or a browser with WebCrypto.
51
+
52
+ ## Usage
53
+
54
+ ```ts
55
+ import {
56
+ deriveMasterKey,
57
+ encryptVaultEntry,
58
+ decryptVaultEntry,
59
+ createWrappedUserKey,
60
+ rotateEncryptionKeys,
61
+ } from '@dis/shield';
62
+
63
+ const salt = '...'; // base64, stored per account
64
+ const kek = await deriveMasterKey(masterPassword, salt); // Argon2id -> AES-GCM key
65
+
66
+ const sealed = await encryptVaultEntry({ password: 's3cret' }, kek, entryId);
67
+ const data = await decryptVaultEntry(sealed, kek, entryId);
68
+ ```
69
+
70
+ Narrow imports are tree-shakeable:
71
+
72
+ ```ts
73
+ import { encryptBytes } from '@dis/shield/aead';
74
+ import { deriveRawKey } from '@dis/shield/kdf';
75
+ ```
76
+
77
+ ## Modules
78
+
79
+ | Entry point | Responsibility |
80
+ | --- | --- |
81
+ | `@dis/shield/core` | Errors, encoding, constants, crypto-provider abstraction |
82
+ | `@dis/shield/random` | Secure random bytes / UUIDs |
83
+ | `@dis/shield/secure-memory` | `SecureBuffer` for key material |
84
+ | `@dis/shield/kdf` | Argon2id, versioned parameters, HKDF strengthening |
85
+ | `@dis/shield/aead` | AES-256-GCM encrypt/decrypt with AAD |
86
+ | `@dis/shield/format-versioning` | Versioned, prefix-tagged envelopes |
87
+ | `@dis/shield/vault-encryption` | Vault-entry sealing (entry-id AAD binding) |
88
+ | `@dis/shield/file-encryption` | Chunked attachment encryption + manifests |
89
+ | `@dis/shield/key-management` | Content-key wrap / unwrap / rotation |
90
+ | `@dis/shield/post-quantum` | ML-KEM-768 + RSA-4096 hybrid key wrapping (sharing / emergency access) |
91
+ | `@dis/shield/integrity` | SHA-256, constant-time compare, verification |
92
+ | `@dis/shield/migrations` | Ordered, explicit payload migrations |
93
+
94
+ ## Security
95
+
96
+ See [`docs/`](docs/) for the architecture overview, threat model,
97
+ trust-boundary analysis, crypto review, and migration plan. Security
98
+ properties are only claimed where they are backed by code, tests, or
99
+ documentation. Anything unverified is labelled `not verified`.
100
+
101
+ ## License
102
+
103
+ [PolyForm Noncommercial 1.0.0](LICENSE) — source-available, free for
104
+ noncommercial use. **Not free for commercial use.** See
105
+ [`docs/licensing.md`](docs/licensing.md) for the rationale, trademark/branding
106
+ rules, and commercial dual-licensing.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * dis-aead — authenticated encryption with associated data.
3
+ *
4
+ * Primitive: AES-256-GCM via WebCrypto. Output format is
5
+ * `base64(IV(12) || ciphertext || authTag(16))`, byte-compatible with Singra
6
+ * Vault. A fresh random 96-bit IV is generated per call. Associated data (AAD)
7
+ * is authenticated but not stored; the same AAD must be supplied on decrypt,
8
+ * which lets callers bind ciphertext to a context (e.g. an entry id) and defeat
9
+ * ciphertext-swap attacks.
10
+ */
11
+ /** Encrypts bytes with AES-256-GCM. Caller still owns/wipes `plaintextBytes`. */
12
+ declare function encryptBytes(plaintextBytes: Uint8Array, key: CryptoKey, associatedData?: string): Promise<string>;
13
+ /** Decrypts AES-256-GCM bytes. Returned buffer is secret — caller must wipe it. */
14
+ declare function decryptBytes(encryptedBase64: string, key: CryptoKey, associatedData?: string): Promise<Uint8Array>;
15
+ /** Imports raw key bytes as an AES-GCM key with the given usages. */
16
+ declare function importAesGcmRawKey(keyBytes: Uint8Array, usages: KeyUsage[]): Promise<CryptoKey>;
17
+ /** Generates a fresh non-extractable AES-256-GCM key. */
18
+ declare function generateAesGcmKey(usages?: KeyUsage[], extractable?: boolean): Promise<CryptoKey>;
19
+ /**
20
+ * AES-256-GCM encrypt with a caller-supplied nonce and optional binary AAD.
21
+ * Returns raw `ciphertext || authTag` bytes (no nonce prefix). `key` may be a
22
+ * raw 32-byte array or an already-imported `CryptoKey`.
23
+ */
24
+ declare function aesGcmEncrypt(key: CryptoKey | Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, associatedData?: Uint8Array): Promise<Uint8Array>;
25
+ /**
26
+ * AES-256-GCM decrypt with a caller-supplied nonce and optional binary AAD.
27
+ * `ciphertext` is raw `ciphertext || authTag` bytes. Throws
28
+ * {@link DisDecryptionError} on any failure (wrong key / tamper / AAD mismatch)
29
+ * without distinguishing the cause.
30
+ */
31
+ declare function aesGcmDecrypt(key: CryptoKey | Uint8Array, nonce: Uint8Array, ciphertext: Uint8Array, associatedData?: Uint8Array): Promise<Uint8Array>;
32
+ /** Encrypts a UTF-8 string. The intermediate plaintext bytes are wiped. */
33
+ declare function encryptString(plaintext: string, key: CryptoKey, associatedData?: string): Promise<string>;
34
+ /** Decrypts to a UTF-8 string. The intermediate plaintext bytes are wiped. */
35
+ declare function decryptString(encryptedBase64: string, key: CryptoKey, associatedData?: string): Promise<string>;
36
+
37
+ export { aesGcmDecrypt, aesGcmEncrypt, decryptBytes, decryptString, encryptBytes, encryptString, generateAesGcmKey, importAesGcmRawKey };
@@ -0,0 +1,7 @@
1
+ export { aesGcmDecrypt, aesGcmEncrypt, decryptBytes, decryptString, encryptBytes, encryptString, generateAesGcmKey, importAesGcmRawKey } from '../chunk-U65A4HIY.js';
2
+ import '../chunk-BUFRR5PB.js';
3
+ import '../chunk-JSKIWIEC.js';
4
+ import '../chunk-CYIGDF63.js';
5
+ import '../chunk-MJO7IJZC.js';
6
+ //# sourceMappingURL=index.js.map
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dis-asymmetric — RSA-OAEP public-key operations.
3
+ *
4
+ * Primitive: RSA-OAEP (4096-bit modulus, SHA-256) via WebCrypto. Used by the
5
+ * Singra sharing / emergency-access profile to wrap symmetric material for a
6
+ * recipient's public key. DIS does not invent any asymmetric scheme — this is
7
+ * a thin, audited wrapper over WebCrypto so applications never touch the raw
8
+ * `crypto.subtle` surface.
9
+ *
10
+ * Key material is exported/imported as JWK (the format Singra persists), so
11
+ * existing stored keys remain byte-compatible. Wire format for ciphertext is
12
+ * `base64(rsa_oaep_output)`, identical to the legacy implementation.
13
+ */
14
+ /** RSA-OAEP modulus length in bits. Part of the key-generation format contract. */
15
+ declare const RSA_OAEP_MODULUS_LENGTH = 4096;
16
+ /**
17
+ * Generates an extractable RSA-OAEP-4096 key pair (SHA-256, e=65537).
18
+ * Extractable so the private key can be exported as JWK and wrapped at rest.
19
+ */
20
+ declare function generateRsaOaepKeyPair(): Promise<CryptoKeyPair>;
21
+ /** Exports an RSA key (public or private) as a JWK object. */
22
+ declare function exportJwk(key: CryptoKey): Promise<JsonWebKey>;
23
+ /** Imports an RSA-OAEP public key (JWK) for `encrypt`. Extractable. */
24
+ declare function importRsaOaepPublicKey(jwk: JsonWebKey): Promise<CryptoKey>;
25
+ /** Imports an RSA-OAEP private key (JWK) for `decrypt`. Non-extractable. */
26
+ declare function importRsaOaepPrivateKey(jwk: JsonWebKey): Promise<CryptoKey>;
27
+ /** Encrypts a UTF-8 string under an RSA-OAEP public key. Returns base64. */
28
+ declare function rsaOaepEncrypt(plaintext: string, publicKey: CryptoKey): Promise<string>;
29
+ /** Decrypts base64 RSA-OAEP ciphertext under an RSA-OAEP private key. */
30
+ declare function rsaOaepDecrypt(ciphertextBase64: string, privateKey: CryptoKey): Promise<string>;
31
+
32
+ export { RSA_OAEP_MODULUS_LENGTH, exportJwk, generateRsaOaepKeyPair, importRsaOaepPrivateKey, importRsaOaepPublicKey, rsaOaepDecrypt, rsaOaepEncrypt };
@@ -0,0 +1,6 @@
1
+ export { RSA_OAEP_MODULUS_LENGTH, exportJwk, generateRsaOaepKeyPair, importRsaOaepPrivateKey, importRsaOaepPublicKey, rsaOaepDecrypt, rsaOaepEncrypt } from '../chunk-KNCZMIZA.js';
2
+ import '../chunk-JSKIWIEC.js';
3
+ import '../chunk-CYIGDF63.js';
4
+ import '../chunk-MJO7IJZC.js';
5
+ //# sourceMappingURL=index.js.map
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,114 @@
1
+ import { randomBytes } from './chunk-3HCT6A2P.js';
2
+ import { importAesGcmKey } from './chunk-OPHN2B3N.js';
3
+ import { encryptBytes, decryptBytes, decryptString } from './chunk-U65A4HIY.js';
4
+ import { AES_KEY_LENGTH } from './chunk-BUFRR5PB.js';
5
+ import { base64ToBytes } from './chunk-JSKIWIEC.js';
6
+ import { subtle } from './chunk-CYIGDF63.js';
7
+ import { DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
8
+
9
+ // src/key-management/index.ts
10
+ var DEFAULT_KEY_WRAP_SCHEME = {
11
+ prefix: "usk-wrap-v2:",
12
+ hkdfInfo: "singra-vault-wrap-v1"
13
+ };
14
+ async function deriveWrapKeyBytes(kdfOutputBytes, scheme) {
15
+ const baseKey = await subtle().importKey("raw", kdfOutputBytes, "HKDF", false, [
16
+ "deriveBits"
17
+ ]);
18
+ const bits = await subtle().deriveBits(
19
+ {
20
+ name: "HKDF",
21
+ hash: "SHA-256",
22
+ // Zero salt is correct: IKM is already high-entropy Argon2id output.
23
+ salt: new Uint8Array(32),
24
+ info: new TextEncoder().encode(scheme.hkdfInfo)
25
+ },
26
+ baseKey,
27
+ 256
28
+ );
29
+ return new Uint8Array(bits);
30
+ }
31
+ function generateContentKeyBytes() {
32
+ return randomBytes(AES_KEY_LENGTH);
33
+ }
34
+ async function createWrappedUserKey(kdfOutputBytes, scheme = DEFAULT_KEY_WRAP_SCHEME) {
35
+ const userKeyBytes = generateContentKeyBytes();
36
+ let wrapKeyBytes = null;
37
+ try {
38
+ wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);
39
+ const wrapKey = await importAesGcmKey(wrapKeyBytes);
40
+ const encryptedUserKey = `${scheme.prefix}${await encryptBytes(userKeyBytes, wrapKey)}`;
41
+ const userKey = await importAesGcmKey(userKeyBytes);
42
+ return { encryptedUserKey, userKey };
43
+ } finally {
44
+ userKeyBytes.fill(0);
45
+ wrapKeyBytes?.fill(0);
46
+ }
47
+ }
48
+ async function unwrapUserKeyBytes(encryptedUserKey, kdfOutputBytes, scheme = DEFAULT_KEY_WRAP_SCHEME) {
49
+ let wrapKeyBytes = null;
50
+ try {
51
+ wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);
52
+ const wrapKey = await importAesGcmKey(wrapKeyBytes);
53
+ if (encryptedUserKey.startsWith(scheme.prefix)) {
54
+ return await decryptBytes(encryptedUserKey.slice(scheme.prefix.length), wrapKey);
55
+ }
56
+ const userKeyBase64 = await decryptString(encryptedUserKey, wrapKey);
57
+ return base64ToBytes(userKeyBase64);
58
+ } finally {
59
+ wrapKeyBytes?.fill(0);
60
+ }
61
+ }
62
+ async function unwrapUserKey(encryptedUserKey, kdfOutputBytes, scheme = DEFAULT_KEY_WRAP_SCHEME) {
63
+ const userKeyBytes = await unwrapUserKeyBytes(encryptedUserKey, kdfOutputBytes, scheme);
64
+ try {
65
+ return await importAesGcmKey(userKeyBytes);
66
+ } finally {
67
+ userKeyBytes.fill(0);
68
+ }
69
+ }
70
+ async function createDeterministicWrappedUserKey(kdfOutputBytes, scheme = DEFAULT_KEY_WRAP_SCHEME) {
71
+ const userKeyBytes = new Uint8Array(kdfOutputBytes);
72
+ let wrapKeyBytes = null;
73
+ try {
74
+ wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);
75
+ const wrapKey = await importAesGcmKey(wrapKeyBytes);
76
+ const encryptedUserKey = `${scheme.prefix}${await encryptBytes(userKeyBytes, wrapKey)}`;
77
+ const userKey = await importAesGcmKey(userKeyBytes);
78
+ return { encryptedUserKey, userKey };
79
+ } finally {
80
+ userKeyBytes.fill(0);
81
+ wrapKeyBytes?.fill(0);
82
+ }
83
+ }
84
+ async function generateAesGcmKeyJwk() {
85
+ const key = await subtle().generateKey({ name: "AES-GCM", length: 256 }, true, [
86
+ "encrypt",
87
+ "decrypt"
88
+ ]);
89
+ const jwk = await subtle().exportKey("jwk", key);
90
+ return JSON.stringify(jwk);
91
+ }
92
+ async function importAesGcmKeyFromJwk(jwkString, usages) {
93
+ const jwk = JSON.parse(jwkString);
94
+ return subtle().importKey("jwk", jwk, { name: "AES-GCM", length: 256 }, false, [...usages]);
95
+ }
96
+ async function rotateWrappedKey(encryptedUserKey, oldKdfOutputBytes, newKdfOutputBytes, scheme = DEFAULT_KEY_WRAP_SCHEME) {
97
+ if (!encryptedUserKey) {
98
+ throw new DisInvalidArgumentError("encryptedUserKey is required");
99
+ }
100
+ const userKeyBytes = await unwrapUserKeyBytes(encryptedUserKey, oldKdfOutputBytes, scheme);
101
+ let newWrapKeyBytes = null;
102
+ try {
103
+ newWrapKeyBytes = await deriveWrapKeyBytes(newKdfOutputBytes, scheme);
104
+ const newWrapKey = await importAesGcmKey(newWrapKeyBytes);
105
+ return `${scheme.prefix}${await encryptBytes(userKeyBytes, newWrapKey)}`;
106
+ } finally {
107
+ userKeyBytes.fill(0);
108
+ newWrapKeyBytes?.fill(0);
109
+ }
110
+ }
111
+
112
+ export { DEFAULT_KEY_WRAP_SCHEME, createDeterministicWrappedUserKey, createWrappedUserKey, generateAesGcmKeyJwk, generateContentKeyBytes, importAesGcmKeyFromJwk, rotateWrappedKey, unwrapUserKey, unwrapUserKeyBytes };
113
+ //# sourceMappingURL=chunk-3DQPQCAR.js.map
114
+ //# sourceMappingURL=chunk-3DQPQCAR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/key-management/index.ts"],"names":[],"mappings":";;;;;;;;;AAqCO,IAAM,uBAAA,GAAyC;AAAA,EAClD,MAAA,EAAQ,cAAA;AAAA,EACR,QAAA,EAAU;AACd;AAYA,eAAe,kBAAA,CACX,gBACA,MAAA,EACmB;AACnB,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,EAAO,CAAE,UAAU,KAAA,EAAO,cAAA,EAAgC,QAAQ,KAAA,EAAO;AAAA,IAC3F;AAAA,GACH,CAAA;AACD,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,EAAO,CAAE,UAAA;AAAA,IACxB;AAAA,MACI,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,SAAA;AAAA;AAAA,MAEN,IAAA,EAAM,IAAI,UAAA,CAAW,EAAE,CAAA;AAAA,MACvB,MAAM,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,OAAO,QAAQ;AAAA,KAClD;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACJ;AACA,EAAA,OAAO,IAAI,WAAW,IAAI,CAAA;AAC9B;AAGO,SAAS,uBAAA,GAAsC;AAClD,EAAA,OAAO,YAAY,cAAc,CAAA;AACrC;AAMA,eAAsB,oBAAA,CAClB,cAAA,EACA,MAAA,GAAwB,uBAAA,EACF;AACtB,EAAA,MAAM,eAAe,uBAAA,EAAwB;AAC7C,EAAA,IAAI,YAAA,GAAkC,IAAA;AACtC,EAAA,IAAI;AACA,IAAA,YAAA,GAAe,MAAM,kBAAA,CAAmB,cAAA,EAAgB,MAAM,CAAA;AAC9D,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,YAAY,CAAA;AAClD,IAAA,MAAM,gBAAA,GAAmB,GAAG,MAAA,CAAO,MAAM,GAAG,MAAM,YAAA,CAAa,YAAA,EAAc,OAAO,CAAC,CAAA,CAAA;AACrF,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,YAAY,CAAA;AAClD,IAAA,OAAO,EAAE,kBAAkB,OAAA,EAAQ;AAAA,EACvC,CAAA,SAAE;AACE,IAAA,YAAA,CAAa,KAAK,CAAC,CAAA;AACnB,IAAA,YAAA,EAAc,KAAK,CAAC,CAAA;AAAA,EACxB;AACJ;AAGA,eAAsB,kBAAA,CAClB,gBAAA,EACA,cAAA,EACA,MAAA,GAAwB,uBAAA,EACL;AACnB,EAAA,IAAI,YAAA,GAAkC,IAAA;AACtC,EAAA,IAAI;AACA,IAAA,YAAA,GAAe,MAAM,kBAAA,CAAmB,cAAA,EAAgB,MAAM,CAAA;AAC9D,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,YAAY,CAAA;AAClD,IAAA,IAAI,gBAAA,CAAiB,UAAA,CAAW,MAAA,CAAO,MAAM,CAAA,EAAG;AAC5C,MAAA,OAAO,MAAM,aAAa,gBAAA,CAAiB,KAAA,CAAM,OAAO,MAAA,CAAO,MAAM,GAAG,OAAO,CAAA;AAAA,IACnF;AAEA,IAAA,MAAM,aAAA,GAAgB,MAAM,aAAA,CAAc,gBAAA,EAAkB,OAAO,CAAA;AACnE,IAAA,OAAO,cAAc,aAAa,CAAA;AAAA,EACtC,CAAA,SAAE;AACE,IAAA,YAAA,EAAc,KAAK,CAAC,CAAA;AAAA,EACxB;AACJ;AAGA,eAAsB,aAAA,CAClB,gBAAA,EACA,cAAA,EACA,MAAA,GAAwB,uBAAA,EACN;AAClB,EAAA,MAAM,YAAA,GAAe,MAAM,kBAAA,CAAmB,gBAAA,EAAkB,gBAAgB,MAAM,CAAA;AACtF,EAAA,IAAI;AACA,IAAA,OAAO,MAAM,gBAAgB,YAAY,CAAA;AAAA,EAC7C,CAAA,SAAE;AACE,IAAA,YAAA,CAAa,KAAK,CAAC,CAAA;AAAA,EACvB;AACJ;AASA,eAAsB,iCAAA,CAClB,cAAA,EACA,MAAA,GAAwB,uBAAA,EACF;AAEtB,EAAA,MAAM,YAAA,GAAe,IAAI,UAAA,CAAW,cAAc,CAAA;AAClD,EAAA,IAAI,YAAA,GAAkC,IAAA;AACtC,EAAA,IAAI;AACA,IAAA,YAAA,GAAe,MAAM,kBAAA,CAAmB,cAAA,EAAgB,MAAM,CAAA;AAC9D,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,YAAY,CAAA;AAClD,IAAA,MAAM,gBAAA,GAAmB,GAAG,MAAA,CAAO,MAAM,GAAG,MAAM,YAAA,CAAa,YAAA,EAAc,OAAO,CAAC,CAAA,CAAA;AACrF,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,YAAY,CAAA;AAClD,IAAA,OAAO,EAAE,kBAAkB,OAAA,EAAQ;AAAA,EACvC,CAAA,SAAE;AACE,IAAA,YAAA,CAAa,KAAK,CAAC,CAAA;AACnB,IAAA,YAAA,EAAc,KAAK,CAAC,CAAA;AAAA,EACxB;AACJ;AAGA,eAAsB,oBAAA,GAAwC;AAC1D,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,EAAO,CAAE,WAAA,CAAY,EAAE,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,GAAA,EAAI,EAAG,IAAA,EAAM;AAAA,IAC3E,SAAA;AAAA,IACA;AAAA,GACH,CAAA;AACD,EAAA,MAAM,MAAM,MAAM,MAAA,EAAO,CAAE,SAAA,CAAU,OAAO,GAAG,CAAA;AAC/C,EAAA,OAAO,IAAA,CAAK,UAAU,GAAG,CAAA;AAC7B;AAGA,eAAsB,sBAAA,CAClB,WACA,MAAA,EACkB;AAClB,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAChC,EAAA,OAAO,MAAA,EAAO,CAAE,SAAA,CAAU,KAAA,EAAO,KAAK,EAAE,IAAA,EAAM,SAAA,EAAW,MAAA,EAAQ,KAAI,EAAG,KAAA,EAAO,CAAC,GAAG,MAAM,CAAC,CAAA;AAC9F;AAOA,eAAsB,gBAAA,CAClB,gBAAA,EACA,iBAAA,EACA,iBAAA,EACA,SAAwB,uBAAA,EACT;AACf,EAAA,IAAI,CAAC,gBAAA,EAAkB;AACnB,IAAA,MAAM,IAAI,wBAAwB,8BAA8B,CAAA;AAAA,EACpE;AACA,EAAA,MAAM,YAAA,GAAe,MAAM,kBAAA,CAAmB,gBAAA,EAAkB,mBAAmB,MAAM,CAAA;AACzF,EAAA,IAAI,eAAA,GAAqC,IAAA;AACzC,EAAA,IAAI;AACA,IAAA,eAAA,GAAkB,MAAM,kBAAA,CAAmB,iBAAA,EAAmB,MAAM,CAAA;AACpE,IAAA,MAAM,UAAA,GAAa,MAAM,eAAA,CAAgB,eAAe,CAAA;AACxD,IAAA,OAAO,CAAA,EAAG,OAAO,MAAM,CAAA,EAAG,MAAM,YAAA,CAAa,YAAA,EAAc,UAAU,CAAC,CAAA,CAAA;AAAA,EAC1E,CAAA,SAAE;AACE,IAAA,YAAA,CAAa,KAAK,CAAC,CAAA;AACnB,IAAA,eAAA,EAAiB,KAAK,CAAC,CAAA;AAAA,EAC3B;AACJ","file":"chunk-3DQPQCAR.js","sourcesContent":["/**\r\n * dis-key-management — key hierarchy, wrapping, and rotation.\r\n *\r\n * Implements the two-tier key model used by Singra Vault:\r\n * - A high-entropy random *content key* (the \"UserKey\") encrypts vault data.\r\n * - The content key is wrapped under a *key-encryption key* (KEK) that is\r\n * derived from the KDF output via HKDF-Expand (domain-separated `info`).\r\n *\r\n * Because the content key is independent of the password, changing the master\r\n * password only re-wraps the content key — no vault data is re-encrypted. This\r\n * is what makes {@link rotateWrappedKey} cheap and safe.\r\n *\r\n * Wrapped keys carry a stable, versioned prefix. The default scheme is byte\r\n * compatible with Singra's `usk-wrap-v2:` envelope.\r\n */\r\n\r\nimport { decryptBytes, decryptString, encryptBytes } from '../aead/index.js';\r\nimport { importAesGcmKey } from '../kdf/index.js';\r\nimport { randomBytes } from '../random/index.js';\r\nimport { base64ToBytes } from '../core/encoding.js';\r\nimport { subtle } from '../core/provider.js';\r\nimport { AES_KEY_LENGTH } from '../core/constants.js';\r\nimport { DisInvalidArgumentError } from '../core/errors.js';\r\n\r\n/** Describes how a content key is wrapped: envelope prefix + HKDF domain. */\r\nexport interface KeyWrapScheme {\r\n /** Stable envelope prefix written for new wraps, e.g. `usk-wrap-v2:`. */\r\n readonly prefix: string;\r\n /** HKDF-Expand `info` for deriving the KEK from KDF output. */\r\n readonly hkdfInfo: string;\r\n}\r\n\r\n/**\r\n * Default wrap scheme. Byte-compatible with Singra Vault so existing\r\n * `encrypted_user_key` values decrypt unchanged. The `info` label is part of\r\n * the format contract and must not be renamed without a new scheme version.\r\n */\r\nexport const DEFAULT_KEY_WRAP_SCHEME: KeyWrapScheme = {\r\n prefix: 'usk-wrap-v2:',\r\n hkdfInfo: 'singra-vault-wrap-v1',\r\n};\r\n\r\nexport { importAesGcmKey };\r\n\r\nexport interface UserKeyBundle {\r\n /** Wrapped content key: `<prefix><base64(IV||CT||tag)>`. */\r\n readonly encryptedUserKey: string;\r\n /** Non-extractable AES-GCM content key, ready for vault operations. */\r\n readonly userKey: CryptoKey;\r\n}\r\n\r\n/** Derives the KEK from raw KDF output via HKDF-Expand (zero salt, RFC 5869). */\r\nasync function deriveWrapKeyBytes(\r\n kdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme,\r\n): Promise<Uint8Array> {\r\n const baseKey = await subtle().importKey('raw', kdfOutputBytes as BufferSource, 'HKDF', 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 // Zero salt is correct: IKM is already high-entropy Argon2id output.\r\n salt: new Uint8Array(32) as BufferSource,\r\n info: new TextEncoder().encode(scheme.hkdfInfo) as BufferSource,\r\n },\r\n baseKey,\r\n 256,\r\n );\r\n return new Uint8Array(bits);\r\n}\r\n\r\n/** Generates fresh random content-key bytes (caller must wipe). */\r\nexport function generateContentKeyBytes(): Uint8Array {\r\n return randomBytes(AES_KEY_LENGTH);\r\n}\r\n\r\n/**\r\n * Creates a new random content key and wraps it under the KEK derived from\r\n * `kdfOutputBytes`. For new accounts. The caller still owns `kdfOutputBytes`.\r\n */\r\nexport async function createWrappedUserKey(\r\n kdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme = DEFAULT_KEY_WRAP_SCHEME,\r\n): Promise<UserKeyBundle> {\r\n const userKeyBytes = generateContentKeyBytes();\r\n let wrapKeyBytes: Uint8Array | null = null;\r\n try {\r\n wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);\r\n const wrapKey = await importAesGcmKey(wrapKeyBytes);\r\n const encryptedUserKey = `${scheme.prefix}${await encryptBytes(userKeyBytes, wrapKey)}`;\r\n const userKey = await importAesGcmKey(userKeyBytes);\r\n return { encryptedUserKey, userKey };\r\n } finally {\r\n userKeyBytes.fill(0);\r\n wrapKeyBytes?.fill(0);\r\n }\r\n}\r\n\r\n/** Unwraps a wrapped content key to raw bytes (caller must wipe). */\r\nexport async function unwrapUserKeyBytes(\r\n encryptedUserKey: string,\r\n kdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme = DEFAULT_KEY_WRAP_SCHEME,\r\n): Promise<Uint8Array> {\r\n let wrapKeyBytes: Uint8Array | null = null;\r\n try {\r\n wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);\r\n const wrapKey = await importAesGcmKey(wrapKeyBytes);\r\n if (encryptedUserKey.startsWith(scheme.prefix)) {\r\n return await decryptBytes(encryptedUserKey.slice(scheme.prefix.length), wrapKey);\r\n }\r\n // Legacy wrappers encrypted a base64 string (no prefix). Read-compatible.\r\n const userKeyBase64 = await decryptString(encryptedUserKey, wrapKey);\r\n return base64ToBytes(userKeyBase64);\r\n } finally {\r\n wrapKeyBytes?.fill(0);\r\n }\r\n}\r\n\r\n/** Unwraps a wrapped content key and imports it as an AES-GCM CryptoKey. */\r\nexport async function unwrapUserKey(\r\n encryptedUserKey: string,\r\n kdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme = DEFAULT_KEY_WRAP_SCHEME,\r\n): Promise<CryptoKey> {\r\n const userKeyBytes = await unwrapUserKeyBytes(encryptedUserKey, kdfOutputBytes, scheme);\r\n try {\r\n return await importAesGcmKey(userKeyBytes);\r\n } finally {\r\n userKeyBytes.fill(0);\r\n }\r\n}\r\n\r\n/**\r\n * Wraps a *deterministic* content key derived directly from `kdfOutputBytes`,\r\n * for EXISTING accounts migrating to the two-tier key model. The content key\r\n * equals the raw KDF output, so vault data previously encrypted directly under\r\n * that output remains readable without re-encryption. The KEK is still\r\n * domain-separated (HKDF `info`), so the wrapper is independent of the key.\r\n */\r\nexport async function createDeterministicWrappedUserKey(\r\n kdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme = DEFAULT_KEY_WRAP_SCHEME,\r\n): Promise<UserKeyBundle> {\r\n // Copy so callers can wipe their buffer without aliasing the returned key.\r\n const userKeyBytes = new Uint8Array(kdfOutputBytes);\r\n let wrapKeyBytes: Uint8Array | null = null;\r\n try {\r\n wrapKeyBytes = await deriveWrapKeyBytes(kdfOutputBytes, scheme);\r\n const wrapKey = await importAesGcmKey(wrapKeyBytes);\r\n const encryptedUserKey = `${scheme.prefix}${await encryptBytes(userKeyBytes, wrapKey)}`;\r\n const userKey = await importAesGcmKey(userKeyBytes);\r\n return { encryptedUserKey, userKey };\r\n } finally {\r\n userKeyBytes.fill(0);\r\n wrapKeyBytes?.fill(0);\r\n }\r\n}\r\n\r\n/** Generates a fresh AES-256-GCM key and returns it as a JWK JSON string. */\r\nexport async function generateAesGcmKeyJwk(): Promise<string> {\r\n const key = await subtle().generateKey({ name: 'AES-GCM', length: 256 }, true, [\r\n 'encrypt',\r\n 'decrypt',\r\n ]);\r\n const jwk = await subtle().exportKey('jwk', key);\r\n return JSON.stringify(jwk);\r\n}\r\n\r\n/** Imports an AES-256-GCM key from a JWK JSON string for the given usages. */\r\nexport async function importAesGcmKeyFromJwk(\r\n jwkString: string,\r\n usages: ReadonlyArray<KeyUsage>,\r\n): Promise<CryptoKey> {\r\n const jwk = JSON.parse(jwkString) as JsonWebKey;\r\n return subtle().importKey('jwk', jwk, { name: 'AES-GCM', length: 256 }, false, [...usages]);\r\n}\r\n\r\n/**\r\n * Re-wraps an existing content key under a new KDF output (new master password\r\n * / new salt). The content key itself is unchanged, so NO vault data is\r\n * re-encrypted — only the wrapper string changes.\r\n */\r\nexport async function rotateWrappedKey(\r\n encryptedUserKey: string,\r\n oldKdfOutputBytes: Uint8Array,\r\n newKdfOutputBytes: Uint8Array,\r\n scheme: KeyWrapScheme = DEFAULT_KEY_WRAP_SCHEME,\r\n): Promise<string> {\r\n if (!encryptedUserKey) {\r\n throw new DisInvalidArgumentError('encryptedUserKey is required');\r\n }\r\n const userKeyBytes = await unwrapUserKeyBytes(encryptedUserKey, oldKdfOutputBytes, scheme);\r\n let newWrapKeyBytes: Uint8Array | null = null;\r\n try {\r\n newWrapKeyBytes = await deriveWrapKeyBytes(newKdfOutputBytes, scheme);\r\n const newWrapKey = await importAesGcmKey(newWrapKeyBytes);\r\n return `${scheme.prefix}${await encryptBytes(userKeyBytes, newWrapKey)}`;\r\n } finally {\r\n userKeyBytes.fill(0);\r\n newWrapKeyBytes?.fill(0);\r\n }\r\n}\r\n"]}
@@ -0,0 +1,55 @@
1
+ import { getCryptoProvider } from './chunk-CYIGDF63.js';
2
+ import { DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
3
+
4
+ // src/random/index.ts
5
+ function randomBytes(length) {
6
+ if (!Number.isInteger(length) || length <= 0) {
7
+ throw new DisInvalidArgumentError("randomBytes length must be a positive integer");
8
+ }
9
+ const out = new Uint8Array(length);
10
+ getCryptoProvider().getRandomValues(out);
11
+ return out;
12
+ }
13
+ function fillRandom(view) {
14
+ return getCryptoProvider().getRandomValues(view);
15
+ }
16
+ function randomInt(min, max) {
17
+ if (!Number.isInteger(min) || !Number.isInteger(max)) {
18
+ throw new DisInvalidArgumentError("randomInt bounds must be integers");
19
+ }
20
+ if (max < min) {
21
+ throw new DisInvalidArgumentError("randomInt max must be >= min");
22
+ }
23
+ const range = max - min + 1;
24
+ if (range === 1) {
25
+ return min;
26
+ }
27
+ const bytesNeeded = Math.ceil(Math.log2(range) / 8) || 1;
28
+ const maxValid = Math.floor(256 ** bytesNeeded / range) * range - 1;
29
+ const buffer = new Uint8Array(bytesNeeded);
30
+ let randomValue;
31
+ do {
32
+ getCryptoProvider().getRandomValues(buffer);
33
+ randomValue = 0;
34
+ for (let i = 0; i < bytesNeeded; i++) {
35
+ randomValue = randomValue << 8 | buffer[i];
36
+ }
37
+ } while (randomValue > maxValid);
38
+ return min + randomValue % range;
39
+ }
40
+ function randomUuid() {
41
+ const provider = getCryptoProvider();
42
+ if (typeof provider.randomUUID === "function") {
43
+ return provider.randomUUID();
44
+ }
45
+ const bytes = randomBytes(16);
46
+ bytes[6] = bytes[6] & 15 | 64;
47
+ bytes[8] = bytes[8] & 63 | 128;
48
+ const hex = [];
49
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
50
+ return `${hex[0]}${hex[1]}${hex[2]}${hex[3]}-${hex[4]}${hex[5]}-${hex[6]}${hex[7]}-${hex[8]}${hex[9]}-${hex[10]}${hex[11]}${hex[12]}${hex[13]}${hex[14]}${hex[15]}`;
51
+ }
52
+
53
+ export { fillRandom, randomBytes, randomInt, randomUuid };
54
+ //# sourceMappingURL=chunk-3HCT6A2P.js.map
55
+ //# sourceMappingURL=chunk-3HCT6A2P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/random/index.ts"],"names":[],"mappings":"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","file":"chunk-3HCT6A2P.js","sourcesContent":["/**\r\n * dis-random — secure random generation.\r\n *\r\n * Always sources entropy from the active crypto provider's CSPRNG\r\n * (`getRandomValues`). DIS never uses `Math.random` for any value that affects\r\n * confidentiality, integrity, or unpredictability.\r\n */\r\n\r\nimport { getCryptoProvider } from '../core/provider.js';\r\nimport { DisInvalidArgumentError } from '../core/errors.js';\r\n\r\n/** Returns `length` cryptographically secure random bytes. */\r\nexport function randomBytes(length: number): Uint8Array {\r\n if (!Number.isInteger(length) || length <= 0) {\r\n throw new DisInvalidArgumentError('randomBytes length must be a positive integer');\r\n }\r\n const out = new Uint8Array(length);\r\n getCryptoProvider().getRandomValues(out);\r\n return out;\r\n}\r\n\r\n/**\r\n * Fills an existing `ArrayBufferView` with cryptographically secure random\r\n * bytes in place. Used by callers that own a buffer they want to overwrite\r\n * (e.g. secure-memory scrubbing) without allocating a second array.\r\n */\r\nexport function fillRandom<T extends ArrayBufferView>(view: T): T {\r\n return getCryptoProvider().getRandomValues(view);\r\n}\r\n\r\n/**\r\n * Returns a cryptographically secure random integer in the inclusive range\r\n * `[min, max]` using rejection sampling to avoid modulo bias.\r\n *\r\n * Byte-for-byte equivalent to the `getSecureRandomInt` helpers in Singra Vault\r\n * (password generator, backup-code generator): it draws `ceil(log2(range)/8)`\r\n * bytes big-endian and rejects values above the largest multiple of `range`.\r\n */\r\nexport function randomInt(min: number, max: number): number {\r\n if (!Number.isInteger(min) || !Number.isInteger(max)) {\r\n throw new DisInvalidArgumentError('randomInt bounds must be integers');\r\n }\r\n if (max < min) {\r\n throw new DisInvalidArgumentError('randomInt max must be >= min');\r\n }\r\n const range = max - min + 1;\r\n if (range === 1) {\r\n return min;\r\n }\r\n const bytesNeeded = Math.ceil(Math.log2(range) / 8) || 1;\r\n const maxValid = Math.floor((256 ** bytesNeeded) / range) * range - 1;\r\n const buffer = new Uint8Array(bytesNeeded);\r\n let randomValue: number;\r\n do {\r\n getCryptoProvider().getRandomValues(buffer);\r\n randomValue = 0;\r\n for (let i = 0; i < bytesNeeded; i++) {\r\n randomValue = (randomValue << 8) | buffer[i]!;\r\n }\r\n } while (randomValue > maxValid);\r\n return min + (randomValue % range);\r\n}\r\n\r\n/** Returns a RFC 4122 v4 UUID using the provider's CSPRNG. */\r\nexport function randomUuid(): string {\r\n const provider = getCryptoProvider() as { randomUUID?: () => string };\r\n if (typeof provider.randomUUID === 'function') {\r\n return provider.randomUUID();\r\n }\r\n // Fallback: build a v4 UUID from random bytes.\r\n const bytes = randomBytes(16);\r\n bytes[6] = (bytes[6]! & 0x0f) | 0x40;\r\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\r\n const hex: string[] = [];\r\n for (let i = 0; i < 16; i++) hex.push(bytes[i]!.toString(16).padStart(2, '0'));\r\n return (\r\n `${hex[0]}${hex[1]}${hex[2]}${hex[3]}-${hex[4]}${hex[5]}-${hex[6]}${hex[7]}-` +\r\n `${hex[8]}${hex[9]}-${hex[10]}${hex[11]}${hex[12]}${hex[13]}${hex[14]}${hex[15]}`\r\n );\r\n}\r\n"]}
@@ -0,0 +1,57 @@
1
+ import { subtle } from './chunk-CYIGDF63.js';
2
+ import { DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
3
+
4
+ // src/signing/index.ts
5
+ var ECDSA_P256_ALGORITHM = { name: "ECDSA", namedCurve: "P-256" };
6
+ var ECDSA_P256_PARAMS = { name: "ECDSA", hash: "SHA-256" };
7
+ var ECDSA_P256_SIGNATURE_LENGTH = 64;
8
+ async function generateEcdsaP256KeyPair() {
9
+ const keyPair = await subtle().generateKey(
10
+ ECDSA_P256_ALGORITHM,
11
+ /* extractable */
12
+ false,
13
+ ["sign", "verify"]
14
+ );
15
+ const spki = await subtle().exportKey("spki", keyPair.publicKey);
16
+ return {
17
+ privateKey: keyPair.privateKey,
18
+ publicKey: keyPair.publicKey,
19
+ publicKeySpki: new Uint8Array(spki)
20
+ };
21
+ }
22
+ async function importEcdsaP256PublicKeySpki(spki) {
23
+ return subtle().importKey(
24
+ "spki",
25
+ spki,
26
+ ECDSA_P256_ALGORITHM,
27
+ false,
28
+ ["verify"]
29
+ );
30
+ }
31
+ async function signEcdsaP256(privateKey, data) {
32
+ const signature = await subtle().sign(ECDSA_P256_PARAMS, privateKey, data);
33
+ const bytes = new Uint8Array(signature);
34
+ if (bytes.length !== ECDSA_P256_SIGNATURE_LENGTH) {
35
+ throw new DisInvalidArgumentError("unexpected ECDSA signature byte length");
36
+ }
37
+ return bytes;
38
+ }
39
+ async function verifyEcdsaP256(publicKey, signature, data) {
40
+ if (signature.length !== ECDSA_P256_SIGNATURE_LENGTH) {
41
+ throw new DisInvalidArgumentError("unexpected ECDSA signature byte length");
42
+ }
43
+ try {
44
+ return await subtle().verify(
45
+ ECDSA_P256_PARAMS,
46
+ publicKey,
47
+ signature,
48
+ data
49
+ );
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ export { ECDSA_P256_SIGNATURE_LENGTH, generateEcdsaP256KeyPair, importEcdsaP256PublicKeySpki, signEcdsaP256, verifyEcdsaP256 };
56
+ //# sourceMappingURL=chunk-AB2WZ7Y2.js.map
57
+ //# sourceMappingURL=chunk-AB2WZ7Y2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/signing/index.ts"],"names":[],"mappings":";;;;AAgBA,IAAM,oBAAA,GAAuB,EAAE,IAAA,EAAM,OAAA,EAAS,YAAY,OAAA,EAAQ;AAClE,IAAM,iBAAA,GAAoB,EAAE,IAAA,EAAM,OAAA,EAAS,MAAM,SAAA,EAAU;AAGpD,IAAM,2BAAA,GAA8B;AAa3C,eAAsB,wBAAA,GAAsD;AACxE,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,EAAO,CAAE,WAAA;AAAA,IAC3B,oBAAA;AAAA;AAAA,IACkB,KAAA;AAAA,IAClB,CAAC,QAAQ,QAAQ;AAAA,GACrB;AACA,EAAA,MAAM,OAAO,MAAM,MAAA,GAAS,SAAA,CAAU,MAAA,EAAQ,QAAQ,SAAS,CAAA;AAC/D,EAAA,OAAO;AAAA,IACH,YAAY,OAAA,CAAQ,UAAA;AAAA,IACpB,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,aAAA,EAAe,IAAI,UAAA,CAAW,IAAI;AAAA,GACtC;AACJ;AAGA,eAAsB,6BAA6B,IAAA,EAAsC;AACrF,EAAA,OAAO,QAAO,CAAE,SAAA;AAAA,IACZ,MAAA;AAAA,IACA,IAAA;AAAA,IACA,oBAAA;AAAA,IACA,KAAA;AAAA,IACA,CAAC,QAAQ;AAAA,GACb;AACJ;AAOA,eAAsB,aAAA,CAAc,YAAuB,IAAA,EAAuC;AAC9F,EAAA,MAAM,YAAY,MAAM,MAAA,GAAS,IAAA,CAAK,iBAAA,EAAmB,YAAY,IAAoB,CAAA;AACzF,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,SAAS,CAAA;AACtC,EAAA,IAAI,KAAA,CAAM,WAAW,2BAAA,EAA6B;AAC9C,IAAA,MAAM,IAAI,wBAAwB,wCAAwC,CAAA;AAAA,EAC9E;AACA,EAAA,OAAO,KAAA;AACX;AAOA,eAAsB,eAAA,CAClB,SAAA,EACA,SAAA,EACA,IAAA,EACgB;AAChB,EAAA,IAAI,SAAA,CAAU,WAAW,2BAAA,EAA6B;AAClD,IAAA,MAAM,IAAI,wBAAwB,wCAAwC,CAAA;AAAA,EAC9E;AACA,EAAA,IAAI;AACA,IAAA,OAAO,MAAM,QAAO,CAAE,MAAA;AAAA,MAClB,iBAAA;AAAA,MACA,SAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ,CAAA,CAAA,MAAQ;AACJ,IAAA,OAAO,KAAA;AAAA,EACX;AACJ","file":"chunk-AB2WZ7Y2.js","sourcesContent":["/**\r\n * dis-signing — asymmetric digital signatures.\r\n *\r\n * Primitive: ECDSA over the NIST P-256 curve with SHA-256, via WebCrypto.\r\n * Public keys are exchanged as SPKI bytes; private keys are generated\r\n * non-extractable so they never leave the device. Signatures are the raw\r\n * `r || s` concatenation WebCrypto produces (fixed 64 bytes for P-256), which\r\n * is the exact wire form Singra Vault's op-log device signatures use.\r\n *\r\n * Canonicalisation of the signed payload is the caller's responsibility — DIS\r\n * signs and verifies opaque byte strings and never interprets their structure.\r\n */\r\n\r\nimport { subtle } from '../core/provider.js';\r\nimport { DisInvalidArgumentError } from '../core/errors.js';\r\n\r\nconst ECDSA_P256_ALGORITHM = { name: 'ECDSA', namedCurve: 'P-256' } as const;\r\nconst ECDSA_P256_PARAMS = { name: 'ECDSA', hash: 'SHA-256' } as const;\r\n\r\n/** Raw byte length of a P-256 ECDSA signature (`r || s`, 32 bytes each). */\r\nexport const ECDSA_P256_SIGNATURE_LENGTH = 64;\r\n\r\nexport interface EcdsaP256KeyPair {\r\n readonly privateKey: CryptoKey;\r\n readonly publicKey: CryptoKey;\r\n /** SPKI-encoded public key bytes, ready to be base64url-encoded for storage. */\r\n readonly publicKeySpki: Uint8Array;\r\n}\r\n\r\n/**\r\n * Generates a fresh non-extractable ECDSA P-256 key pair and exports the\r\n * public key as SPKI bytes.\r\n */\r\nexport async function generateEcdsaP256KeyPair(): Promise<EcdsaP256KeyPair> {\r\n const keyPair = await subtle().generateKey(\r\n ECDSA_P256_ALGORITHM,\r\n /* extractable */ false,\r\n ['sign', 'verify'],\r\n );\r\n const spki = await subtle().exportKey('spki', keyPair.publicKey);\r\n return {\r\n privateKey: keyPair.privateKey,\r\n publicKey: keyPair.publicKey,\r\n publicKeySpki: new Uint8Array(spki),\r\n };\r\n}\r\n\r\n/** Imports an SPKI-encoded P-256 public key for signature verification. */\r\nexport async function importEcdsaP256PublicKeySpki(spki: Uint8Array): Promise<CryptoKey> {\r\n return subtle().importKey(\r\n 'spki',\r\n spki as BufferSource,\r\n ECDSA_P256_ALGORITHM,\r\n false,\r\n ['verify'],\r\n );\r\n}\r\n\r\n/**\r\n * Signs `data` with an ECDSA P-256 private key, returning the raw 64-byte\r\n * `r || s` signature. Throws {@link DisInvalidArgumentError} if WebCrypto\r\n * returns an unexpected length (defends against curve/algorithm misuse).\r\n */\r\nexport async function signEcdsaP256(privateKey: CryptoKey, data: Uint8Array): Promise<Uint8Array> {\r\n const signature = await subtle().sign(ECDSA_P256_PARAMS, privateKey, data as BufferSource);\r\n const bytes = new Uint8Array(signature);\r\n if (bytes.length !== ECDSA_P256_SIGNATURE_LENGTH) {\r\n throw new DisInvalidArgumentError('unexpected ECDSA signature byte length');\r\n }\r\n return bytes;\r\n}\r\n\r\n/**\r\n * Verifies a raw 64-byte `r || s` ECDSA P-256 signature over `data`. Returns\r\n * `false` for an invalid signature; throws {@link DisInvalidArgumentError} if\r\n * the signature is not the expected length.\r\n */\r\nexport async function verifyEcdsaP256(\r\n publicKey: CryptoKey,\r\n signature: Uint8Array,\r\n data: Uint8Array,\r\n): Promise<boolean> {\r\n if (signature.length !== ECDSA_P256_SIGNATURE_LENGTH) {\r\n throw new DisInvalidArgumentError('unexpected ECDSA signature byte length');\r\n }\r\n try {\r\n return await subtle().verify(\r\n ECDSA_P256_PARAMS,\r\n publicKey,\r\n signature as BufferSource,\r\n data as BufferSource,\r\n );\r\n } catch {\r\n return false;\r\n }\r\n}\r\n"]}
@@ -0,0 +1,9 @@
1
+ // src/core/constants.ts
2
+ var AES_GCM_IV_LENGTH = 12;
3
+ var AES_GCM_TAG_LENGTH = 128;
4
+ var AES_KEY_LENGTH = 32;
5
+ var KDF_SALT_LENGTH = 16;
6
+
7
+ export { AES_GCM_IV_LENGTH, AES_GCM_TAG_LENGTH, AES_KEY_LENGTH, KDF_SALT_LENGTH };
8
+ //# sourceMappingURL=chunk-BUFRR5PB.js.map
9
+ //# sourceMappingURL=chunk-BUFRR5PB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/constants.ts"],"names":[],"mappings":";AASO,IAAM,iBAAA,GAAoB;AAG1B,IAAM,kBAAA,GAAqB;AAG3B,IAAM,cAAA,GAAiB;AAGvB,IAAM,eAAA,GAAkB","file":"chunk-BUFRR5PB.js","sourcesContent":["/**\r\n * Cryptographic constants shared across DIS modules.\r\n *\r\n * These values are part of the on-the-wire / on-disk format contract. Changing\r\n * any released constant is a breaking format change and MUST be expressed as a\r\n * new format version, never an in-place edit.\r\n */\r\n\r\n/** AES-GCM IV length in bytes (96 bits — the recommended GCM nonce size). */\r\nexport const AES_GCM_IV_LENGTH = 12;\r\n\r\n/** AES-GCM authentication tag length in bits. */\r\nexport const AES_GCM_TAG_LENGTH = 128;\r\n\r\n/** Symmetric key length in bytes (AES-256). */\r\nexport const AES_KEY_LENGTH = 32;\r\n\r\n/** Salt length in bytes (128 bits) for password-based key derivation. */\r\nexport const KDF_SALT_LENGTH = 16;\r\n"]}