@molecule/api-encryption-aes 1.0.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 +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/provider.d.ts +46 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +196 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +22 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the
|
|
45
|
+
original version of the Work and any modifications or additions
|
|
46
|
+
to that Work, that is intentionally submitted to the Licensor for
|
|
47
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
48
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
49
|
+
|
|
50
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
51
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
52
|
+
subsequently incorporated within the Work.
|
|
53
|
+
|
|
54
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
55
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
56
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
57
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
58
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
59
|
+
Work and such Derivative Works in Source or Object form.
|
|
60
|
+
|
|
61
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
62
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
63
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
64
|
+
patent license to make, have made, use, offer to sell, sell, import,
|
|
65
|
+
and otherwise transfer the Work.
|
|
66
|
+
|
|
67
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
68
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
69
|
+
modifications, and in Source or Object form, provided that You
|
|
70
|
+
meet the following conditions:
|
|
71
|
+
|
|
72
|
+
(a) You must give any other recipients of the Work or
|
|
73
|
+
Derivative Works a copy of this License; and
|
|
74
|
+
|
|
75
|
+
(b) You must cause any modified files to carry prominent notices
|
|
76
|
+
stating that You changed the files; and
|
|
77
|
+
|
|
78
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
79
|
+
that You distribute, all copyright, patent, trademark, and
|
|
80
|
+
attribution notices from the Source form of the Work,
|
|
81
|
+
excluding those notices that do not pertain to any part of
|
|
82
|
+
the Derivative Works; and
|
|
83
|
+
|
|
84
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
85
|
+
distribution, then any Derivative Works that You distribute must
|
|
86
|
+
include a readable copy of the attribution notices contained
|
|
87
|
+
within such NOTICE file.
|
|
88
|
+
|
|
89
|
+
5. Submission of Contributions.
|
|
90
|
+
|
|
91
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
92
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
93
|
+
|
|
94
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
95
|
+
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
96
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
97
|
+
|
|
98
|
+
8. Limitation of Liability. In no event and under no legal theory shall
|
|
99
|
+
any Contributor be liable to You for damages.
|
|
100
|
+
|
|
101
|
+
9. Accepting Warranty or Additional Liability.
|
|
102
|
+
|
|
103
|
+
Copyright 2026 Molecule Dev, Inc.
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
106
|
+
you may not use this file except in compliance with the License.
|
|
107
|
+
You may obtain a copy of the License at
|
|
108
|
+
|
|
109
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
110
|
+
|
|
111
|
+
Unless required by applicable law or agreed to in writing, software
|
|
112
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
113
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
See the License for the specific language governing permissions and
|
|
115
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser guard — `@molecule/api-encryption-aes` is SERVER-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
|
|
5
|
+
* Evaluating a server package in a browser bundle is always an import-graph mistake
|
|
6
|
+
* (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
|
|
7
|
+
* ("Buffer is not defined") far from the culprit. Throwing here names the package and
|
|
8
|
+
* the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
|
|
9
|
+
* unaffected: the throw requires browser globals AND the absence of a node runtime.
|
|
10
|
+
*/
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
|
|
13
|
+
throw new Error('@molecule/api-encryption-aes is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
|
|
14
|
+
'from server code (a server route/function or your API), or dynamic-import it inside ' +
|
|
15
|
+
'the server handler — never from components or shared client modules, and never ' +
|
|
16
|
+
'polyfill Buffer/process to silence this.');
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in `crypto` for AES-256-GCM authenticated encryption,
|
|
5
|
+
* SHA-256 hashing, and timing-safe verification. Supports key rotation with
|
|
6
|
+
* versioned ciphertext format.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { setProvider } from '@molecule/api-encryption'
|
|
12
|
+
* import { provider } from '@molecule/api-encryption-aes'
|
|
13
|
+
*
|
|
14
|
+
* // Wire the provider at startup (reads ENCRYPTION_KEY from env)
|
|
15
|
+
* setProvider(provider)
|
|
16
|
+
*
|
|
17
|
+
* // Or create with explicit config
|
|
18
|
+
* import { createProvider } from '@molecule/api-encryption-aes'
|
|
19
|
+
* const customProvider = createProvider({ key: 'your-64-char-hex-key' })
|
|
20
|
+
* setProvider(customProvider)
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - **`rotateKey()` is transition-safe — it never orphans data.** Keys live in
|
|
25
|
+
* a version-keyed keyring: `encrypt()` tags each ciphertext with the current
|
|
26
|
+
* `v{n}`, `decrypt()` reads that tag and selects the matching key, and
|
|
27
|
+
* `rotateKey(oldKey, newKey)` ADDS `newKey` at the next version while RETAINING
|
|
28
|
+
* the prior key(s). So ciphertext written before a rotation still decrypts
|
|
29
|
+
* afterward (the core contract's "previously encrypted data can still be
|
|
30
|
+
* decrypted during a transition period"). Once you have re-encrypted the old
|
|
31
|
+
* ciphertext under the new key, retire the old keys explicitly with
|
|
32
|
+
* `pruneKeyVersions()` — rotation alone deliberately keeps them.
|
|
33
|
+
* - A ciphertext whose `v{n}` version is not in the keyring (unknown/pruned key)
|
|
34
|
+
* fails cleanly with a descriptive error — never a silent wrong decrypt.
|
|
35
|
+
* - Rotation state is per-process and in-memory: the lazy `provider` singleton
|
|
36
|
+
* starts a fresh keyring at version 1 from `ENCRYPTION_KEY` on each process,
|
|
37
|
+
* so a rotation done in a prior process is not restored. For rotation that
|
|
38
|
+
* survives restarts, build with `createProvider({ key, priorKeys })`, seeding
|
|
39
|
+
* the historical `{ version, key }` entries from your secret store.
|
|
40
|
+
* - `hash()`/`verify()` are plain unsalted SHA-256 — integrity checks only.
|
|
41
|
+
* NEVER use them for passwords; use `@molecule/api-password` with a bond
|
|
42
|
+
* like `@molecule/api-password-bcrypt`.
|
|
43
|
+
* - `encrypt(plaintext, context)`: the optional `context` is GCM AAD — the
|
|
44
|
+
* SAME context string must be supplied to `decrypt()` or authentication
|
|
45
|
+
* fails.
|
|
46
|
+
*/
|
|
47
|
+
export * from './browser-guard.js';
|
|
48
|
+
export * from './provider.js';
|
|
49
|
+
export * from './secrets.js';
|
|
50
|
+
export * from './types.js';
|
|
51
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in `crypto` for AES-256-GCM authenticated encryption,
|
|
5
|
+
* SHA-256 hashing, and timing-safe verification. Supports key rotation with
|
|
6
|
+
* versioned ciphertext format.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { setProvider } from '@molecule/api-encryption'
|
|
12
|
+
* import { provider } from '@molecule/api-encryption-aes'
|
|
13
|
+
*
|
|
14
|
+
* // Wire the provider at startup (reads ENCRYPTION_KEY from env)
|
|
15
|
+
* setProvider(provider)
|
|
16
|
+
*
|
|
17
|
+
* // Or create with explicit config
|
|
18
|
+
* import { createProvider } from '@molecule/api-encryption-aes'
|
|
19
|
+
* const customProvider = createProvider({ key: 'your-64-char-hex-key' })
|
|
20
|
+
* setProvider(customProvider)
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - **`rotateKey()` is transition-safe — it never orphans data.** Keys live in
|
|
25
|
+
* a version-keyed keyring: `encrypt()` tags each ciphertext with the current
|
|
26
|
+
* `v{n}`, `decrypt()` reads that tag and selects the matching key, and
|
|
27
|
+
* `rotateKey(oldKey, newKey)` ADDS `newKey` at the next version while RETAINING
|
|
28
|
+
* the prior key(s). So ciphertext written before a rotation still decrypts
|
|
29
|
+
* afterward (the core contract's "previously encrypted data can still be
|
|
30
|
+
* decrypted during a transition period"). Once you have re-encrypted the old
|
|
31
|
+
* ciphertext under the new key, retire the old keys explicitly with
|
|
32
|
+
* `pruneKeyVersions()` — rotation alone deliberately keeps them.
|
|
33
|
+
* - A ciphertext whose `v{n}` version is not in the keyring (unknown/pruned key)
|
|
34
|
+
* fails cleanly with a descriptive error — never a silent wrong decrypt.
|
|
35
|
+
* - Rotation state is per-process and in-memory: the lazy `provider` singleton
|
|
36
|
+
* starts a fresh keyring at version 1 from `ENCRYPTION_KEY` on each process,
|
|
37
|
+
* so a rotation done in a prior process is not restored. For rotation that
|
|
38
|
+
* survives restarts, build with `createProvider({ key, priorKeys })`, seeding
|
|
39
|
+
* the historical `{ version, key }` entries from your secret store.
|
|
40
|
+
* - `hash()`/`verify()` are plain unsalted SHA-256 — integrity checks only.
|
|
41
|
+
* NEVER use them for passwords; use `@molecule/api-password` with a bond
|
|
42
|
+
* like `@molecule/api-password-bcrypt`.
|
|
43
|
+
* - `encrypt(plaintext, context)`: the optional `context` is GCM AAD — the
|
|
44
|
+
* SAME context string must be supplied to `decrypt()` or authentication
|
|
45
|
+
* fails.
|
|
46
|
+
*/
|
|
47
|
+
export * from './browser-guard.js';
|
|
48
|
+
export * from './provider.js';
|
|
49
|
+
export * from './secrets.js';
|
|
50
|
+
export * from './types.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM implementation of `EncryptionProvider`.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in `crypto` module for all operations:
|
|
5
|
+
* - **encrypt/decrypt**: AES-256-GCM with random IV and authentication tag
|
|
6
|
+
* - **hash**: SHA-256
|
|
7
|
+
* - **verify**: Timing-safe comparison of SHA-256 hashes
|
|
8
|
+
* - **rotateKey**: Adds the new key to a versioned keyring (retaining prior
|
|
9
|
+
* keys) so ciphertext encrypted before the rotation still decrypts
|
|
10
|
+
*
|
|
11
|
+
* Ciphertext format: `v{keyVersion}:{iv}:{authTag}:{ciphertext}` (all
|
|
12
|
+
* hex-encoded). `decrypt()` reads the `v{n}` tag and selects the matching key
|
|
13
|
+
* from the keyring — so a rotation never orphans existing data.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import './secrets.js';
|
|
18
|
+
import type { AesConfig, AesEncryptionProvider } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Creates an AES-256-GCM encryption provider.
|
|
21
|
+
*
|
|
22
|
+
* Rotation is transition-safe: keys are held in a version-keyed keyring.
|
|
23
|
+
* `encrypt()` tags each ciphertext with the current key version, `decrypt()`
|
|
24
|
+
* selects the key named by that tag, and `rotateKey()` ADDS the new key while
|
|
25
|
+
* keeping prior keys — so ciphertext from before a rotation still decrypts.
|
|
26
|
+
* Retire old keys explicitly (once their data is re-encrypted) via
|
|
27
|
+
* `pruneKeyVersions()`.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Provider configuration including the hex-encoded 256-bit key.
|
|
30
|
+
* @returns An `AesEncryptionProvider` using AES-256-GCM.
|
|
31
|
+
*/
|
|
32
|
+
export declare const createProvider: (config: AesConfig) => AesEncryptionProvider;
|
|
33
|
+
/**
|
|
34
|
+
* Default AES-256-GCM encryption provider instance.
|
|
35
|
+
*
|
|
36
|
+
* Lazily initializes on first property access using the `ENCRYPTION_KEY`
|
|
37
|
+
* environment variable (hex-encoded 256-bit key). This singleton starts a
|
|
38
|
+
* fresh keyring at version 1 on each process, so a rotation performed in a
|
|
39
|
+
* prior process is NOT restored here — for rotation that survives restarts,
|
|
40
|
+
* construct with `createProvider({ key, priorKeys })` seeding the historical
|
|
41
|
+
* keys from your secret store.
|
|
42
|
+
*
|
|
43
|
+
* @throws {Error} If `ENCRYPTION_KEY` environment variable is not set.
|
|
44
|
+
*/
|
|
45
|
+
export declare const provider: AesEncryptionProvider;
|
|
46
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AA0ClE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,SAAS,KAAG,qBAoHlD,CAAA;AAKD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,QAAQ,EAAE,qBA0BrB,CAAA"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM implementation of `EncryptionProvider`.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in `crypto` module for all operations:
|
|
5
|
+
* - **encrypt/decrypt**: AES-256-GCM with random IV and authentication tag
|
|
6
|
+
* - **hash**: SHA-256
|
|
7
|
+
* - **verify**: Timing-safe comparison of SHA-256 hashes
|
|
8
|
+
* - **rotateKey**: Adds the new key to a versioned keyring (retaining prior
|
|
9
|
+
* keys) so ciphertext encrypted before the rotation still decrypts
|
|
10
|
+
*
|
|
11
|
+
* Ciphertext format: `v{keyVersion}:{iv}:{authTag}:{ciphertext}` (all
|
|
12
|
+
* hex-encoded). `decrypt()` reads the `v{n}` tag and selects the matching key
|
|
13
|
+
* from the keyring — so a rotation never orphans existing data.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, timingSafeEqual, } from 'node:crypto';
|
|
18
|
+
// Side-effect import: registers this bond's secret definitions so the
|
|
19
|
+
// runtime registry is populated even when provider.js is imported directly
|
|
20
|
+
// (not through the package barrel).
|
|
21
|
+
import './secrets.js';
|
|
22
|
+
/** AES-256-GCM algorithm identifier. */
|
|
23
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
24
|
+
/** IV length in bytes (96-bit / 12 bytes recommended for GCM). */
|
|
25
|
+
const IV_LENGTH = 12;
|
|
26
|
+
/** Authentication tag length in bytes. */
|
|
27
|
+
const AUTH_TAG_LENGTH = 16;
|
|
28
|
+
/** Parses the `v{n}` key-version prefix of a ciphertext tag. */
|
|
29
|
+
const parseKeyVersion = (tag) => {
|
|
30
|
+
const match = /^v(\d+)$/.exec(tag);
|
|
31
|
+
return match ? Number(match[1]) : null;
|
|
32
|
+
};
|
|
33
|
+
/** A 256-bit key is exactly 64 hex characters. */
|
|
34
|
+
const HEX_256_BIT = /^[0-9a-fA-F]{64}$/;
|
|
35
|
+
/**
|
|
36
|
+
* Decode a hex-encoded 256-bit key, refusing anything that isn't exactly 64 hex
|
|
37
|
+
* chars. `Buffer.from(x, 'hex')` SILENTLY truncates at the first non-hex byte
|
|
38
|
+
* and silently drops an odd trailing nibble, so a typo'd or short key would
|
|
39
|
+
* otherwise yield a wrong-length buffer that either 500s on every encrypt or —
|
|
40
|
+
* worse — encrypts under a key nobody can reproduce. Fail loud at construction.
|
|
41
|
+
*
|
|
42
|
+
* @param key - The hex-encoded key to validate and decode.
|
|
43
|
+
* @param label - Human context for the error (e.g. `'ENCRYPTION_KEY'`).
|
|
44
|
+
* @returns The 32-byte key buffer.
|
|
45
|
+
* @throws {Error} When the key is not exactly 64 hex characters.
|
|
46
|
+
*/
|
|
47
|
+
const decodeKey = (key, label) => {
|
|
48
|
+
if (!HEX_256_BIT.test(key)) {
|
|
49
|
+
throw new Error(`Invalid AES ${label}: expected exactly 64 hex characters (a 256-bit key); ` +
|
|
50
|
+
`got ${key.length} character(s). Generate one with \`openssl rand -hex 32\`.`);
|
|
51
|
+
}
|
|
52
|
+
return Buffer.from(key, 'hex');
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Creates an AES-256-GCM encryption provider.
|
|
56
|
+
*
|
|
57
|
+
* Rotation is transition-safe: keys are held in a version-keyed keyring.
|
|
58
|
+
* `encrypt()` tags each ciphertext with the current key version, `decrypt()`
|
|
59
|
+
* selects the key named by that tag, and `rotateKey()` ADDS the new key while
|
|
60
|
+
* keeping prior keys — so ciphertext from before a rotation still decrypts.
|
|
61
|
+
* Retire old keys explicitly (once their data is re-encrypted) via
|
|
62
|
+
* `pruneKeyVersions()`.
|
|
63
|
+
*
|
|
64
|
+
* @param config - Provider configuration including the hex-encoded 256-bit key.
|
|
65
|
+
* @returns An `AesEncryptionProvider` using AES-256-GCM.
|
|
66
|
+
*/
|
|
67
|
+
export const createProvider = (config) => {
|
|
68
|
+
let currentVersion = config.keyVersion ?? 1;
|
|
69
|
+
let currentKey = decodeKey(config.key, 'key');
|
|
70
|
+
// Version -> key buffer. Rotation adds a new version and retains prior keys,
|
|
71
|
+
// so ciphertext tagged with an older `v{n}` still decrypts. Seed historical
|
|
72
|
+
// keys first, then set the current one so it wins on any version collision.
|
|
73
|
+
const keyring = new Map();
|
|
74
|
+
for (const prior of config.priorKeys ?? []) {
|
|
75
|
+
keyring.set(prior.version, decodeKey(prior.key, `priorKeys[v${prior.version}]`));
|
|
76
|
+
}
|
|
77
|
+
keyring.set(currentVersion, currentKey);
|
|
78
|
+
return {
|
|
79
|
+
async encrypt(plaintext, context) {
|
|
80
|
+
const iv = randomBytes(IV_LENGTH);
|
|
81
|
+
const cipher = createCipheriv(ALGORITHM, currentKey, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
82
|
+
if (context) {
|
|
83
|
+
cipher.setAAD(Buffer.from(context, 'utf-8'));
|
|
84
|
+
}
|
|
85
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
86
|
+
const authTag = cipher.getAuthTag();
|
|
87
|
+
return `v${currentVersion}:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
88
|
+
},
|
|
89
|
+
async decrypt(ciphertext, context) {
|
|
90
|
+
const parts = ciphertext.split(':');
|
|
91
|
+
if (parts.length !== 4) {
|
|
92
|
+
throw new Error('Invalid ciphertext format');
|
|
93
|
+
}
|
|
94
|
+
const version = parseKeyVersion(parts[0]);
|
|
95
|
+
if (version === null) {
|
|
96
|
+
throw new Error('Invalid ciphertext format: missing or malformed key version prefix');
|
|
97
|
+
}
|
|
98
|
+
// Select the key that ENCRYPTED this ciphertext by its version tag — not
|
|
99
|
+
// the current key — so pre-rotation ciphertext decrypts with its own key.
|
|
100
|
+
const key = keyring.get(version);
|
|
101
|
+
if (!key) {
|
|
102
|
+
throw new Error(`No encryption key available for key version ${version}; ` +
|
|
103
|
+
`seed it via priorKeys or do not prune it before re-encrypting its ciphertext`);
|
|
104
|
+
}
|
|
105
|
+
const iv = Buffer.from(parts[1], 'hex');
|
|
106
|
+
const authTag = Buffer.from(parts[2], 'hex');
|
|
107
|
+
const encrypted = Buffer.from(parts[3], 'hex');
|
|
108
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, {
|
|
109
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
110
|
+
});
|
|
111
|
+
decipher.setAuthTag(authTag);
|
|
112
|
+
if (context) {
|
|
113
|
+
decipher.setAAD(Buffer.from(context, 'utf-8'));
|
|
114
|
+
}
|
|
115
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
116
|
+
return decrypted.toString('utf-8');
|
|
117
|
+
},
|
|
118
|
+
async hash(data) {
|
|
119
|
+
return createHash('sha256').update(data, 'utf-8').digest('hex');
|
|
120
|
+
},
|
|
121
|
+
async verify(data, hashed) {
|
|
122
|
+
const computed = createHash('sha256').update(data, 'utf-8').digest('hex');
|
|
123
|
+
if (computed.length !== hashed.length) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return timingSafeEqual(Buffer.from(computed, 'utf-8'), Buffer.from(hashed, 'utf-8'));
|
|
127
|
+
},
|
|
128
|
+
async rotateKey(oldKey, newKey) {
|
|
129
|
+
// Validate the NEW key before anything else: a malformed newKey must not
|
|
130
|
+
// become currentKey (it would encrypt future data under a wrong-length /
|
|
131
|
+
// truncated key). oldKey is only compared, so a bad one just fails to match.
|
|
132
|
+
const newBuffer = decodeKey(newKey, 'newKey');
|
|
133
|
+
const oldBuffer = Buffer.from(oldKey, 'hex');
|
|
134
|
+
if (oldBuffer.length !== currentKey.length || !timingSafeEqual(oldBuffer, currentKey)) {
|
|
135
|
+
throw new Error('Old key does not match the current encryption key');
|
|
136
|
+
}
|
|
137
|
+
// Add the new key at the next version and make it current. The prior key
|
|
138
|
+
// stays in the keyring so ciphertext tagged with the old version still
|
|
139
|
+
// decrypts — rotation NEVER orphans existing data. Retire old keys
|
|
140
|
+
// explicitly with pruneKeyVersions() once their data is re-encrypted.
|
|
141
|
+
currentVersion += 1;
|
|
142
|
+
currentKey = newBuffer;
|
|
143
|
+
keyring.set(currentVersion, currentKey);
|
|
144
|
+
},
|
|
145
|
+
pruneKeyVersions(keep) {
|
|
146
|
+
const retained = new Set([currentVersion, ...(keep ?? [])]);
|
|
147
|
+
const pruned = [];
|
|
148
|
+
for (const version of keyring.keys()) {
|
|
149
|
+
if (!retained.has(version)) {
|
|
150
|
+
pruned.push(version);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const version of pruned) {
|
|
154
|
+
keyring.delete(version);
|
|
155
|
+
}
|
|
156
|
+
return pruned;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/** Lazily-initialized default provider instance. */
|
|
161
|
+
let _provider = null;
|
|
162
|
+
/**
|
|
163
|
+
* Default AES-256-GCM encryption provider instance.
|
|
164
|
+
*
|
|
165
|
+
* Lazily initializes on first property access using the `ENCRYPTION_KEY`
|
|
166
|
+
* environment variable (hex-encoded 256-bit key). This singleton starts a
|
|
167
|
+
* fresh keyring at version 1 on each process, so a rotation performed in a
|
|
168
|
+
* prior process is NOT restored here — for rotation that survives restarts,
|
|
169
|
+
* construct with `createProvider({ key, priorKeys })` seeding the historical
|
|
170
|
+
* keys from your secret store.
|
|
171
|
+
*
|
|
172
|
+
* @throws {Error} If `ENCRYPTION_KEY` environment variable is not set.
|
|
173
|
+
*/
|
|
174
|
+
export const provider = new Proxy({}, {
|
|
175
|
+
get(_, prop, receiver) {
|
|
176
|
+
if (!_provider) {
|
|
177
|
+
const key = process.env['ENCRYPTION_KEY'];
|
|
178
|
+
if (!key) {
|
|
179
|
+
throw new Error('ENCRYPTION_KEY environment variable is required for AES encryption provider');
|
|
180
|
+
}
|
|
181
|
+
_provider = createProvider({ key });
|
|
182
|
+
}
|
|
183
|
+
return Reflect.get(_provider, prop, receiver);
|
|
184
|
+
},
|
|
185
|
+
// set trap: methods run with `this` bound to the proxy — without it, instance-state writes land on the dummy target and are lost (see api-push-notifications-web-push)
|
|
186
|
+
set(_, prop, value) {
|
|
187
|
+
if (!_provider) {
|
|
188
|
+
const key = process.env['ENCRYPTION_KEY'];
|
|
189
|
+
if (!key) {
|
|
190
|
+
throw new Error('ENCRYPTION_KEY environment variable is required for AES encryption provider');
|
|
191
|
+
}
|
|
192
|
+
_provider = createProvider({ key });
|
|
193
|
+
}
|
|
194
|
+
return Reflect.set(_provider, prop, value);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES encryption secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { SecretDefinition } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the AES encryption bond. */
|
|
15
|
+
export declare const encryptionAesSecretDefinitions: SecretDefinition[];
|
|
16
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,EAAE,gBAAgB,EAO5D,CAAA"}
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES encryption secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { registerSecrets } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the AES encryption bond. */
|
|
15
|
+
export const encryptionAesSecretDefinitions = [
|
|
16
|
+
{
|
|
17
|
+
key: 'ENCRYPTION_KEY',
|
|
18
|
+
description: 'AES-256 encryption key — 64-character hex (256-bit) key for AES-GCM encryption. Auto-generated at scaffold — no manual setup needed.',
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
registerSecrets(encryptionAesSecretDefinitions);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption provider configuration.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { EncryptionProvider } from '@molecule/api-encryption';
|
|
7
|
+
/**
|
|
8
|
+
* A historical (pre-rotation) key, retained so ciphertext encrypted under it
|
|
9
|
+
* remains decryptable.
|
|
10
|
+
*
|
|
11
|
+
* Ciphertext is tagged with the `v{version}` of the key that produced it; the
|
|
12
|
+
* provider selects the matching key from its keyring at decrypt time. Seed the
|
|
13
|
+
* keyring with the app's prior keys (from a secret store) so rotations survive
|
|
14
|
+
* a process restart — see {@link AesConfig.priorKeys}.
|
|
15
|
+
*/
|
|
16
|
+
export interface PriorKey {
|
|
17
|
+
/** The key version tag this key decrypts (matches the `v{n}` ciphertext prefix). */
|
|
18
|
+
version: number;
|
|
19
|
+
/** The 256-bit key, hex-encoded (64 hex characters). */
|
|
20
|
+
key: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for the AES-256-GCM encryption provider.
|
|
24
|
+
*/
|
|
25
|
+
export interface AesConfig {
|
|
26
|
+
/** The current 256-bit encryption key, hex-encoded (64 hex characters). */
|
|
27
|
+
key: string;
|
|
28
|
+
/**
|
|
29
|
+
* Version of the current `key`. Ciphertext produced by `encrypt()` is tagged
|
|
30
|
+
* with this version; `decrypt()` selects the key to use from the `v{n}` tag,
|
|
31
|
+
* so it must be stable for a given key. `rotateKey()` advances it.
|
|
32
|
+
*
|
|
33
|
+
* @default 1
|
|
34
|
+
*/
|
|
35
|
+
keyVersion?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Prior keys to seed the keyring with, so ciphertext encrypted before a
|
|
38
|
+
* rotation still decrypts after this provider is (re)constructed — e.g. on a
|
|
39
|
+
* process restart, when the in-memory rotation state would otherwise be lost.
|
|
40
|
+
* Each entry maps a historical `v{n}` version to its key. The current `key`
|
|
41
|
+
* (at {@link AesConfig.keyVersion}) always wins over any prior entry sharing
|
|
42
|
+
* its version.
|
|
43
|
+
*
|
|
44
|
+
* @default []
|
|
45
|
+
*/
|
|
46
|
+
priorKeys?: PriorKey[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* AES-256-GCM encryption provider.
|
|
50
|
+
*
|
|
51
|
+
* Fully satisfies the core `EncryptionProvider` contract, so it bonds anywhere
|
|
52
|
+
* an `EncryptionProvider` is expected. It additionally exposes
|
|
53
|
+
* {@link AesEncryptionProvider.pruneKeyVersions} to explicitly retire old keys
|
|
54
|
+
* once their ciphertext has been re-encrypted — rotation alone never drops a
|
|
55
|
+
* key, so it can never orphan data.
|
|
56
|
+
*/
|
|
57
|
+
export interface AesEncryptionProvider extends EncryptionProvider {
|
|
58
|
+
/**
|
|
59
|
+
* Retires key versions from the in-memory keyring. Call this only AFTER every
|
|
60
|
+
* ciphertext encrypted under those versions has been re-encrypted under the
|
|
61
|
+
* current key — a pruned version can no longer decrypt its ciphertext.
|
|
62
|
+
*
|
|
63
|
+
* The current key version is ALWAYS retained (it cannot be pruned). By
|
|
64
|
+
* default every non-current version is removed; pass `keep` to retain
|
|
65
|
+
* specific older versions during a staged migration.
|
|
66
|
+
*
|
|
67
|
+
* @param keep - Old versions to retain in addition to the current one.
|
|
68
|
+
* @returns The versions that were removed.
|
|
69
|
+
*/
|
|
70
|
+
pruneKeyVersions(keep?: number[]): number[];
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAElE;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,oFAAoF;IACpF,OAAO,EAAE,MAAM,CAAA;IAEf,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAA;IAEX;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;CAC5C"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@molecule/api-encryption-aes",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AES-256-GCM encryption provider for molecule.dev — authenticated encryption, SHA-256 hashing, and key rotation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"molecule",
|
|
24
|
+
"encryption",
|
|
25
|
+
"aes",
|
|
26
|
+
"aes-256-gcm",
|
|
27
|
+
"field-level-encryption",
|
|
28
|
+
"key-rotation"
|
|
29
|
+
],
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@molecule/api-encryption": "^1.0.0",
|
|
33
|
+
"@molecule/api-secrets": "^1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@molecule/api-encryption": "1.0.0",
|
|
37
|
+
"@types/node": "26.1.2",
|
|
38
|
+
"typescript": "6.0.3",
|
|
39
|
+
"vitest": "4.1.10"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
44
|
+
"directory": "packages/api/bonds/encryption/aes"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/encryption/aes",
|
|
47
|
+
"bugs": "https://github.com/molecule-dev/molecule/issues",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|