yamlock 0.1.1 → 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/README.md +76 -31
- package/bin/yamlock +32 -4
- package/dist/cli/cli.js +3 -3
- package/dist/crypto/decrypt.js +37 -7
- package/dist/crypto/encrypt.js +25 -11
- package/dist/crypto/utils.js +56 -16
- package/dist/utils/config.js +8 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -13,27 +13,29 @@ Value-level encryption for YAML and JSON configuration files. The name **yamlock
|
|
|
13
13
|
- Node.js 22.x via `asdf`
|
|
14
14
|
- Yarn Classic (1.x)
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Installation
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
- Core crypto helpers (`encryptValue`, `decryptValue`, and supporting utils) work with per-field salts and have unit tests.
|
|
20
|
-
- `processConfig` can walk nested objects/arrays and apply encryption/decryption to every string value.
|
|
21
|
-
- Public API exports (`encryptValue`, `decryptValue`, `processConfig`, `getSupportedAlgorithms`) are wired and verified by tests.
|
|
22
|
-
- Directory structure for source, CLI, tests, and examples exists.
|
|
23
|
-
- CLI binary can encrypt/decrypt YAML and JSON files by calling `processConfig`.
|
|
18
|
+
### npm
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g yamlock # CLI usage
|
|
22
|
+
npm install yamlock # project dependency
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Yarn Classic
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
```bash
|
|
28
|
+
yarn global add yamlock # CLI usage
|
|
29
|
+
yarn add yamlock # project dependency
|
|
30
|
+
```
|
|
30
31
|
|
|
31
|
-
##
|
|
32
|
+
## Features
|
|
32
33
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
34
|
+
- Encrypt/decrypt individual configuration values with deterministic field-path salts.
|
|
35
|
+
- CLI workflow that processes YAML or JSON files in place.
|
|
36
|
+
- Recursively lock/unlock entire objects via `processConfig`.
|
|
37
|
+
- Public API exports that mirror CLI behavior for programmatic use.
|
|
38
|
+
- Focus on Node.js 22+, ESM modules, and a lightweight dependency set (`js-yaml`).
|
|
37
39
|
|
|
38
40
|
## Usage
|
|
39
41
|
|
|
@@ -64,6 +66,49 @@ const unlocked = processConfig(locked, { mode: 'decrypt', key: process.env.YAMLO
|
|
|
64
66
|
|
|
65
67
|
See `examples/basic.js` for a runnable end-to-end script (`node examples/basic.js`).
|
|
66
68
|
|
|
69
|
+
### Algorithm customization
|
|
70
|
+
|
|
71
|
+
Each function accepts either a cipher name or an options object:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const encrypted = encryptValue('swordfish', KEY, 'db.password', {
|
|
75
|
+
algorithm: 'chacha20-poly1305',
|
|
76
|
+
ivLength: 12 // override the IV size used during encryption
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// When decrypting, the algorithm is inferred from the payload,
|
|
80
|
+
// but you can still override key/IV sizes if the cipher requires it.
|
|
81
|
+
const decrypted = decryptValue(encrypted, KEY, 'db.password', /* optional overrides */);
|
|
82
|
+
|
|
83
|
+
// processConfig propagates the same options through every nested field.
|
|
84
|
+
const processed = processConfig(
|
|
85
|
+
{ db: { password: 'swordfish' }, api: { token: 'secret' } },
|
|
86
|
+
{
|
|
87
|
+
mode: 'encrypt',
|
|
88
|
+
key: KEY,
|
|
89
|
+
algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Later you can decrypt with the same options:
|
|
94
|
+
const restored = processConfig(processed, {
|
|
95
|
+
mode: 'decrypt',
|
|
96
|
+
key: KEY,
|
|
97
|
+
algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Supported algorithms
|
|
102
|
+
|
|
103
|
+
| Algorithm | Type | Notes |
|
|
104
|
+
|-----------|------|-------|
|
|
105
|
+
| `aes-128-cbc` | Block cipher (CBC) | 128-bit keys, 16-byte IV. Works well for backward-compatibility scenarios. |
|
|
106
|
+
| `aes-192-cbc` | Block cipher (CBC) | 192-bit keys, 16-byte IV. Slightly stronger than AES-128 with the same IV requirements. |
|
|
107
|
+
| `aes-256-cbc` (default) | Block cipher (CBC) | 256-bit keys, 16-byte IV. Balanced combination of strength and compatibility. |
|
|
108
|
+
| `chacha20-poly1305` | AEAD stream cipher | 256-bit keys, 12-byte nonce, 16-byte auth tag. Provides built-in integrity/authentication. |
|
|
109
|
+
|
|
110
|
+
You can also pass any algorithm supported by the current Node.js runtime (`crypto.getCiphers()`), along with custom `keyLength`, `ivLength`, or `authTagLength` overrides. Only the algorithms above are actively tested; additional presets may be added or revised in future releases.
|
|
111
|
+
|
|
67
112
|
### Encrypted value format
|
|
68
113
|
|
|
69
114
|
Every locked string follows the format:
|
|
@@ -72,25 +117,19 @@ Every locked string follows the format:
|
|
|
72
117
|
yl|<algorithm>|<salt_base64>|<iv_base64>|<data_base64>
|
|
73
118
|
```
|
|
74
119
|
|
|
75
|
-
|
|
120
|
+
Where:
|
|
121
|
+
- `yl` - format marker prefix
|
|
122
|
+
- `<algorithm>` - algorithm name (e.g., aes-256-cbc)
|
|
123
|
+
- `<salt_base64>` - Base64-encoded field path
|
|
124
|
+
- `<iv_base64>` - Base64-encoded initialization vector
|
|
125
|
+
- `<data_base64>` - Base64-encoded encrypted data
|
|
126
|
+
|
|
127
|
+
The salt is derived from the full field path. Moving or renaming the field invalidates the salt, preventing accidental decryption in the wrong location.
|
|
76
128
|
|
|
77
129
|
### Key rotation
|
|
78
130
|
|
|
79
131
|
See [docs/key-rotation.md](docs/key-rotation.md) for a step-by-step guide to rotating `YAMLOCK_KEY` without losing data.
|
|
80
132
|
|
|
81
|
-
## Project structure
|
|
82
|
-
|
|
83
|
-
```txt
|
|
84
|
-
yamlock/
|
|
85
|
-
├── src/ # Source files (API, CLI, utilities)
|
|
86
|
-
├── dist/ # Build output created by `yarn build`
|
|
87
|
-
├── bin/ # CLI entry point (loads dist/cli/cli.js)
|
|
88
|
-
├── test/ # Unit and integration suites
|
|
89
|
-
├── examples/ # Usage demos (TBD)
|
|
90
|
-
├── CHANGELOG.md # Step-by-step release history
|
|
91
|
-
└── README.md / LICENSE
|
|
92
|
-
```
|
|
93
|
-
|
|
94
133
|
## Inspiration and motivation
|
|
95
134
|
|
|
96
135
|
I have worked with Ruby on Rails apps for more than ten years and appreciated how its secret management evolved between 4.2 and 6.x. That flow influenced **yamlock**, but I also explored modern tools such as:
|
|
@@ -104,12 +143,18 @@ Each of those projects solves secure config storage differently, yet none fit my
|
|
|
104
143
|
|
|
105
144
|
## Contributing
|
|
106
145
|
|
|
107
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow,
|
|
146
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, available scripts, and release instructions.
|
|
108
147
|
|
|
109
148
|
## Exit codes
|
|
110
149
|
|
|
111
150
|
`yamlock` returns `0` when encryption/decryption completes successfully and `1` on validation or runtime errors (missing keys, malformed payloads, failed file reads). Use these exit codes to gate CI jobs or deployment steps.
|
|
112
151
|
|
|
152
|
+
## Future work
|
|
153
|
+
|
|
154
|
+
- Additional cipher presets and stronger default algorithms.
|
|
155
|
+
- More CLI/API examples for rotating keys, selective field targeting, and CI automation.
|
|
156
|
+
- Configurable behavior for non-string values (skip vs. coerce) and stricter file format validation.
|
|
157
|
+
|
|
113
158
|
## License
|
|
114
159
|
|
|
115
160
|
MIT © PAVEL TKACHEV
|
package/bin/yamlock
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
async function loadCliEntry() {
|
|
4
|
+
try {
|
|
5
|
+
const distModule = await import('../dist/cli/cli.js');
|
|
6
|
+
if (distModule.runCli) {
|
|
7
|
+
return distModule.runCli;
|
|
8
|
+
}
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (!(error.code === 'ERR_MODULE_NOT_FOUND' || (error.message && error.message.includes('dist/cli/cli.js')))) {
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const srcModule = await import('../src/cli/cli.js');
|
|
17
|
+
if (srcModule.runCli) {
|
|
18
|
+
return srcModule.runCli;
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!(error.code === 'ERR_MODULE_NOT_FOUND')) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error('CLI entry not found.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
loadCliEntry()
|
|
30
|
+
.then((runCli) => runCli(process.argv))
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
console.error('[yamlock] CLI failed to start:', error);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
package/dist/cli/cli.js
CHANGED
|
@@ -87,8 +87,8 @@ function parseArgs(argv) {
|
|
|
87
87
|
return result;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async function
|
|
91
|
-
const { command, file, options } = parseArgs(
|
|
90
|
+
export async function runCli(argv = process.argv) {
|
|
91
|
+
const { command, file, options } = parseArgs(argv);
|
|
92
92
|
|
|
93
93
|
if (!command || !file) {
|
|
94
94
|
print(HELP_TEXT.trim());
|
|
@@ -135,5 +135,5 @@ async function main() {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
138
|
-
|
|
138
|
+
runCli();
|
|
139
139
|
}
|
package/dist/crypto/decrypt.js
CHANGED
|
@@ -3,25 +3,37 @@ import { createDecipheriv } from 'node:crypto';
|
|
|
3
3
|
import {
|
|
4
4
|
decodeFieldPathSalt,
|
|
5
5
|
deriveKey,
|
|
6
|
-
ensureAlgorithm,
|
|
7
6
|
isYamlockPayload,
|
|
8
|
-
parsePayload
|
|
7
|
+
parsePayload,
|
|
8
|
+
resolveAlgorithmOptions
|
|
9
9
|
} from './utils.js';
|
|
10
10
|
|
|
11
|
+
function resolveDecryptOptions(payloadAlgorithm, overrides) {
|
|
12
|
+
if (typeof overrides === 'string' || overrides === undefined) {
|
|
13
|
+
return resolveAlgorithmOptions({ algorithm: payloadAlgorithm });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return resolveAlgorithmOptions({
|
|
17
|
+
...overrides,
|
|
18
|
+
algorithm: payloadAlgorithm
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* Decrypts a yamlock payload for the provided field path.
|
|
13
24
|
* @param {string} encryptedValue
|
|
14
25
|
* @param {string|Buffer} key
|
|
15
26
|
* @param {string} fieldPath
|
|
27
|
+
* @param {string|object} [algorithmOptions]
|
|
16
28
|
* @returns {string}
|
|
17
29
|
*/
|
|
18
|
-
export function decryptValue(encryptedValue, key, fieldPath) {
|
|
30
|
+
export function decryptValue(encryptedValue, key, fieldPath, algorithmOptions) {
|
|
19
31
|
if (!isYamlockPayload(encryptedValue)) {
|
|
20
32
|
throw new Error('decryptValue expects a yamlock-formatted payload.');
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
const payload = parsePayload(encryptedValue);
|
|
24
|
-
const
|
|
36
|
+
const resolvedOptions = resolveDecryptOptions(payload.algorithm, algorithmOptions);
|
|
25
37
|
const saltFieldPath = decodeFieldPathSalt(payload.salt);
|
|
26
38
|
|
|
27
39
|
if (!fieldPath) {
|
|
@@ -32,9 +44,27 @@ export function decryptValue(encryptedValue, key, fieldPath) {
|
|
|
32
44
|
throw new Error('Field path does not match the encrypted payload.');
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
const derivedKey = deriveKey(key,
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
const derivedKey = deriveKey(key, resolvedOptions);
|
|
48
|
+
let ciphertext = payload.data;
|
|
49
|
+
let authTag;
|
|
50
|
+
if (resolvedOptions.authTagLength) {
|
|
51
|
+
if (ciphertext.length < resolvedOptions.authTagLength) {
|
|
52
|
+
throw new Error('Encrypted payload is missing an authentication tag.');
|
|
53
|
+
}
|
|
54
|
+
authTag = ciphertext.subarray(ciphertext.length - resolvedOptions.authTagLength);
|
|
55
|
+
ciphertext = ciphertext.subarray(0, ciphertext.length - resolvedOptions.authTagLength);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const decipherOptions = resolvedOptions.authTagLength ? { authTagLength: resolvedOptions.authTagLength } : undefined;
|
|
59
|
+
const decipher = createDecipheriv(resolvedOptions.algorithm, derivedKey, payload.iv, decipherOptions);
|
|
60
|
+
if (authTag) {
|
|
61
|
+
if (typeof decipher.setAuthTag !== 'function') {
|
|
62
|
+
throw new Error(`Algorithm ${resolvedOptions.algorithm} requires auth tags but setAuthTag is unavailable.`);
|
|
63
|
+
}
|
|
64
|
+
decipher.setAuthTag(authTag);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
38
68
|
|
|
39
69
|
return decrypted.toString('utf8');
|
|
40
70
|
}
|
package/dist/crypto/encrypt.js
CHANGED
|
@@ -1,38 +1,52 @@
|
|
|
1
1
|
import { createCipheriv } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
DEFAULT_ALGORITHM,
|
|
4
5
|
deriveKey,
|
|
5
6
|
encodeFieldPathSalt,
|
|
6
|
-
ensureAlgorithm,
|
|
7
7
|
formatPayload,
|
|
8
|
-
generateIv
|
|
8
|
+
generateIv,
|
|
9
|
+
resolveAlgorithmOptions
|
|
9
10
|
} from './utils.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
function resolveOptions(input) {
|
|
13
|
+
if (typeof input === 'string' || input === undefined) {
|
|
14
|
+
return resolveAlgorithmOptions({ algorithm: input ?? DEFAULT_ALGORITHM });
|
|
15
|
+
}
|
|
16
|
+
return resolveAlgorithmOptions(input);
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Encrypts a string value for a specific configuration field path.
|
|
15
21
|
* @param {string} value
|
|
16
22
|
* @param {string|Buffer} key
|
|
17
23
|
* @param {string} fieldPath
|
|
18
|
-
* @param {string} [
|
|
24
|
+
* @param {string|object} [algorithmOptions=DEFAULT_ALGORITHM]
|
|
19
25
|
* @returns {string}
|
|
20
26
|
*/
|
|
21
|
-
export function encryptValue(value, key, fieldPath,
|
|
27
|
+
export function encryptValue(value, key, fieldPath, algorithmOptions = DEFAULT_ALGORITHM) {
|
|
22
28
|
if (typeof value !== 'string') {
|
|
23
29
|
throw new Error('encryptValue expects the value to be a string.');
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const
|
|
27
|
-
const derivedKey = deriveKey(key,
|
|
28
|
-
const iv = generateIv(
|
|
32
|
+
const resolvedOptions = resolveOptions(algorithmOptions);
|
|
33
|
+
const derivedKey = deriveKey(key, resolvedOptions);
|
|
34
|
+
const iv = generateIv(resolvedOptions);
|
|
29
35
|
const salt = encodeFieldPathSalt(fieldPath);
|
|
30
36
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
37
|
+
const cipherOptions = resolvedOptions.authTagLength ? { authTagLength: resolvedOptions.authTagLength } : undefined;
|
|
38
|
+
const cipher = createCipheriv(resolvedOptions.algorithm, derivedKey, iv, cipherOptions);
|
|
39
|
+
let encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
40
|
+
if (resolvedOptions.authTagLength) {
|
|
41
|
+
if (typeof cipher.getAuthTag !== 'function') {
|
|
42
|
+
throw new Error(`Algorithm ${resolvedOptions.algorithm} requires auth tags but getAuthTag is unavailable.`);
|
|
43
|
+
}
|
|
44
|
+
const authTag = cipher.getAuthTag();
|
|
45
|
+
encrypted = Buffer.concat([encrypted, authTag]);
|
|
46
|
+
}
|
|
33
47
|
|
|
34
48
|
return formatPayload({
|
|
35
|
-
algorithm:
|
|
49
|
+
algorithm: resolvedOptions.algorithm,
|
|
36
50
|
salt,
|
|
37
51
|
iv,
|
|
38
52
|
data: encrypted
|
package/dist/crypto/utils.js
CHANGED
|
@@ -2,6 +2,13 @@ import { createHash, getCipherInfo, getCiphers, randomBytes } from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
export const YAMLOCK_PREFIX = 'yl';
|
|
4
4
|
export const YAMLOCK_DELIMITER = '|';
|
|
5
|
+
export const DEFAULT_ALGORITHM = 'aes-256-cbc';
|
|
6
|
+
export const ALGORITHM_PRESETS = {
|
|
7
|
+
'aes-256-cbc': { keyLength: 32, ivLength: 16, authTagLength: 0 },
|
|
8
|
+
'aes-192-cbc': { keyLength: 24, ivLength: 16, authTagLength: 0 },
|
|
9
|
+
'aes-128-cbc': { keyLength: 16, ivLength: 16, authTagLength: 0 },
|
|
10
|
+
'chacha20-poly1305': { keyLength: 32, ivLength: 12, authTagLength: 16 }
|
|
11
|
+
};
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Returns the sorted list of cipher algorithms supported by the current runtime.
|
|
@@ -28,14 +35,46 @@ export function ensureAlgorithm(algorithm) {
|
|
|
28
35
|
return algorithm;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
function normalizeAlgorithmInput(input) {
|
|
39
|
+
if (!input || typeof input === 'string') {
|
|
40
|
+
return { algorithm: input };
|
|
41
|
+
}
|
|
42
|
+
return input;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves algorithm settings with optional key/IV overrides.
|
|
47
|
+
* @param {object|string} [input]
|
|
48
|
+
* @param {string} [input.algorithm]
|
|
49
|
+
* @param {number} [input.keyLength]
|
|
50
|
+
* @param {number} [input.ivLength]
|
|
51
|
+
* @returns {{ algorithm: string, keyLength: number, ivLength: number, authTagLength: number }}
|
|
52
|
+
*/
|
|
53
|
+
export function resolveAlgorithmOptions(input) {
|
|
54
|
+
const normalized = normalizeAlgorithmInput(input);
|
|
55
|
+
const algorithmName = ensureAlgorithm(normalized.algorithm ?? DEFAULT_ALGORITHM);
|
|
56
|
+
const preset = ALGORITHM_PRESETS[algorithmName] ?? {};
|
|
57
|
+
const cipherInfo = getCipherInfo(algorithmName) ?? {};
|
|
58
|
+
|
|
59
|
+
const keyLength = normalized.keyLength ?? preset.keyLength ?? cipherInfo.keyLength ?? 32;
|
|
60
|
+
const ivLength = normalized.ivLength ?? preset.ivLength ?? cipherInfo.ivLength ?? 16;
|
|
61
|
+
const authTagLength = normalized.authTagLength ?? preset.authTagLength ?? 0;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
algorithm: algorithmName,
|
|
65
|
+
keyLength,
|
|
66
|
+
ivLength,
|
|
67
|
+
authTagLength
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
/**
|
|
32
72
|
* Derives a key buffer with the exact size required by the cipher.
|
|
33
73
|
* @param {string|Buffer} secret
|
|
34
|
-
* @param {string}
|
|
74
|
+
* @param {{ algorithm: string, keyLength: number }} options
|
|
35
75
|
* @returns {Buffer}
|
|
36
76
|
*/
|
|
37
|
-
export function deriveKey(secret, algorithm) {
|
|
38
|
-
const normalizedAlgorithm = ensureAlgorithm(algorithm);
|
|
77
|
+
export function deriveKey(secret, { algorithm, keyLength }) {
|
|
39
78
|
if (secret === undefined || secret === null || secret === '') {
|
|
40
79
|
throw new Error('Encryption key is required to derive a cipher key.');
|
|
41
80
|
}
|
|
@@ -44,21 +83,22 @@ export function deriveKey(secret, algorithm) {
|
|
|
44
83
|
? secret
|
|
45
84
|
: Buffer.from(String(secret), 'utf8');
|
|
46
85
|
|
|
47
|
-
const
|
|
48
|
-
|
|
86
|
+
const normalizedAlgorithm = ensureAlgorithm(algorithm);
|
|
87
|
+
const requiredLength = keyLength ?? getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
|
|
88
|
+
if (baseBuffer.length === requiredLength) {
|
|
49
89
|
return baseBuffer;
|
|
50
90
|
}
|
|
51
91
|
|
|
52
92
|
const hash = createHash('sha512').update(baseBuffer).digest();
|
|
53
|
-
if (hash.length >=
|
|
54
|
-
return hash.subarray(0,
|
|
93
|
+
if (hash.length >= requiredLength) {
|
|
94
|
+
return hash.subarray(0, requiredLength);
|
|
55
95
|
}
|
|
56
96
|
|
|
57
|
-
const result = Buffer.allocUnsafe(
|
|
97
|
+
const result = Buffer.allocUnsafe(requiredLength);
|
|
58
98
|
let offset = 0;
|
|
59
99
|
let material = hash;
|
|
60
|
-
while (offset <
|
|
61
|
-
const chunk = material.subarray(0, Math.min(material.length,
|
|
100
|
+
while (offset < requiredLength) {
|
|
101
|
+
const chunk = material.subarray(0, Math.min(material.length, requiredLength - offset));
|
|
62
102
|
chunk.copy(result, offset);
|
|
63
103
|
offset += chunk.length;
|
|
64
104
|
material = createHash('sha512').update(material).digest();
|
|
@@ -68,18 +108,18 @@ export function deriveKey(secret, algorithm) {
|
|
|
68
108
|
}
|
|
69
109
|
|
|
70
110
|
/**
|
|
71
|
-
* Generates an IV buffer for the given algorithm
|
|
72
|
-
* @param {string}
|
|
111
|
+
* Generates an IV buffer for the given algorithm configuration.
|
|
112
|
+
* @param {{ algorithm: string, ivLength: number }} options
|
|
73
113
|
* @returns {Buffer}
|
|
74
114
|
*/
|
|
75
|
-
export function generateIv(algorithm) {
|
|
115
|
+
export function generateIv({ algorithm, ivLength }) {
|
|
76
116
|
const normalizedAlgorithm = ensureAlgorithm(algorithm);
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
117
|
+
const length = ivLength ?? getCipherInfo(normalizedAlgorithm)?.ivLength ?? 16;
|
|
118
|
+
if (length === 0) {
|
|
79
119
|
return Buffer.alloc(0);
|
|
80
120
|
}
|
|
81
121
|
|
|
82
|
-
return randomBytes(
|
|
122
|
+
return randomBytes(length);
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
/**
|
package/dist/utils/config.js
CHANGED
|
@@ -13,11 +13,12 @@ const MODES = {
|
|
|
13
13
|
* @param {Object} options
|
|
14
14
|
* @param {'encrypt'|'decrypt'} options.mode
|
|
15
15
|
* @param {string|Buffer} options.key
|
|
16
|
-
* @param {string} [options.algorithm]
|
|
16
|
+
* @param {string|object} [options.algorithm]
|
|
17
|
+
* @param {object} [options.algorithmOptions]
|
|
17
18
|
* @param {Array<string|number>} [options.parentPath]
|
|
18
19
|
* @returns {Object|Array}
|
|
19
20
|
*/
|
|
20
|
-
export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
|
|
21
|
+
export function processConfig(node, { mode, key, algorithm, algorithmOptions, parentPath = [] }) {
|
|
21
22
|
if (typeof node !== 'object' || node === null) {
|
|
22
23
|
throw new Error('processConfig expects a non-null object or array.');
|
|
23
24
|
}
|
|
@@ -28,6 +29,7 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
|
|
|
28
29
|
|
|
29
30
|
const isArrayNode = Array.isArray(node);
|
|
30
31
|
const result = isArrayNode ? [] : {};
|
|
32
|
+
const cryptoOptions = algorithmOptions ?? algorithm;
|
|
31
33
|
|
|
32
34
|
Object.entries(node).forEach(([rawKey, value]) => {
|
|
33
35
|
const segment = isArrayNode ? Number(rawKey) : rawKey;
|
|
@@ -38,7 +40,8 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
|
|
|
38
40
|
result[targetKey] = processConfig(value, {
|
|
39
41
|
mode,
|
|
40
42
|
key,
|
|
41
|
-
algorithm,
|
|
43
|
+
algorithm: cryptoOptions,
|
|
44
|
+
algorithmOptions: cryptoOptions,
|
|
42
45
|
parentPath: [...parentPath, segment]
|
|
43
46
|
});
|
|
44
47
|
return;
|
|
@@ -50,9 +53,9 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
if (mode === MODES.ENCRYPT) {
|
|
53
|
-
result[targetKey] = encryptValue(value, key, currentPath,
|
|
56
|
+
result[targetKey] = encryptValue(value, key, currentPath, cryptoOptions);
|
|
54
57
|
} else {
|
|
55
|
-
result[targetKey] = decryptValue(value, key, currentPath);
|
|
58
|
+
result[targetKey] = decryptValue(value, key, currentPath, cryptoOptions);
|
|
56
59
|
}
|
|
57
60
|
});
|
|
58
61
|
|
package/package.json
CHANGED