yamlock 0.3.0 → 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/README.md +223 -38
- package/bin/yamlock +8 -0
- package/dist/cli/cli.js +457 -74
- package/dist/crypto/decrypt.js +112 -13
- package/dist/crypto/encrypt.js +107 -18
- package/dist/crypto/payload-v2.js +371 -0
- package/dist/crypto/utils.js +65 -14
- package/dist/errors.js +59 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +10 -0
- package/dist/utils/config.js +294 -54
- package/dist/utils/file.js +58 -0
- package/dist/utils/migrate.js +170 -0
- package/dist/utils/path.js +58 -9
- package/docs/api.md +56 -0
- package/docs/design/payload-v2.md +344 -0
- package/docs/errors.md +71 -0
- package/docs/yaml-behavior.md +51 -0
- package/examples/basic.js +31 -0
- package/examples/docs/ci-cd.md +58 -0
- package/examples/docs/key-rotation.md +65 -0
- package/package.json +19 -7
package/dist/crypto/utils.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createHash, getCipherInfo, getCiphers, randomBytes } from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
YAMLOCK_ERROR_CODES,
|
|
5
|
+
YamlockPayloadError,
|
|
6
|
+
YamlockValidationError
|
|
7
|
+
} from '../errors.js';
|
|
8
|
+
|
|
3
9
|
export const YAMLOCK_PREFIX = 'yl';
|
|
4
10
|
export const YAMLOCK_DELIMITER = '|';
|
|
5
11
|
export const DEFAULT_ALGORITHM = 'aes-256-cbc';
|
|
@@ -26,20 +32,32 @@ export function listSupportedAlgorithms() {
|
|
|
26
32
|
*/
|
|
27
33
|
export function ensureAlgorithm(algorithm) {
|
|
28
34
|
if (!algorithm) {
|
|
29
|
-
throw new
|
|
35
|
+
throw new YamlockValidationError('Encryption algorithm is required.', {
|
|
36
|
+
code: YAMLOCK_ERROR_CODES.UNSUPPORTED_ALGORITHM
|
|
37
|
+
});
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
if (!listSupportedAlgorithms().includes(algorithm)) {
|
|
33
|
-
throw new
|
|
41
|
+
throw new YamlockValidationError(`Unsupported algorithm: ${algorithm}`, {
|
|
42
|
+
code: YAMLOCK_ERROR_CODES.UNSUPPORTED_ALGORITHM
|
|
43
|
+
});
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
return algorithm;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
function normalizeAlgorithmInput(input) {
|
|
40
|
-
if (
|
|
50
|
+
if (input === undefined || typeof input === 'string') {
|
|
41
51
|
return { algorithm: input };
|
|
42
52
|
}
|
|
53
|
+
|
|
54
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
55
|
+
throw new YamlockValidationError(
|
|
56
|
+
'Algorithm options must be an object or algorithm string.',
|
|
57
|
+
{ code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
return input;
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -61,6 +79,19 @@ export function resolveAlgorithmOptions(input) {
|
|
|
61
79
|
const ivLength = normalized.ivLength ?? preset.ivLength ?? cipherInfo.ivLength ?? 16;
|
|
62
80
|
const authTagLength = normalized.authTagLength ?? preset.authTagLength ?? 0;
|
|
63
81
|
|
|
82
|
+
for (const [name, value, minimum] of [
|
|
83
|
+
['keyLength', keyLength, 1],
|
|
84
|
+
['ivLength', ivLength, 0],
|
|
85
|
+
['authTagLength', authTagLength, 0]
|
|
86
|
+
]) {
|
|
87
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
88
|
+
throw new YamlockValidationError(
|
|
89
|
+
`${name} must be an integer greater than or equal to ${minimum}.`,
|
|
90
|
+
{ code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
return {
|
|
65
96
|
algorithm: algorithmName,
|
|
66
97
|
keyLength,
|
|
@@ -76,13 +107,19 @@ export function resolveAlgorithmOptions(input) {
|
|
|
76
107
|
* @returns {Buffer}
|
|
77
108
|
*/
|
|
78
109
|
export function deriveKey(secret, { algorithm, keyLength }) {
|
|
79
|
-
if (
|
|
80
|
-
|
|
110
|
+
if (
|
|
111
|
+
(!Buffer.isBuffer(secret) && typeof secret !== 'string') ||
|
|
112
|
+
secret.length === 0
|
|
113
|
+
) {
|
|
114
|
+
throw new YamlockValidationError(
|
|
115
|
+
'Encryption key must be a non-empty string or Buffer.',
|
|
116
|
+
{ code: YAMLOCK_ERROR_CODES.INVALID_KEY }
|
|
117
|
+
);
|
|
81
118
|
}
|
|
82
119
|
|
|
83
120
|
const baseBuffer = Buffer.isBuffer(secret)
|
|
84
121
|
? secret
|
|
85
|
-
: Buffer.from(
|
|
122
|
+
: Buffer.from(secret, 'utf8');
|
|
86
123
|
|
|
87
124
|
const normalizedAlgorithm = ensureAlgorithm(algorithm);
|
|
88
125
|
const requiredLength = keyLength ?? getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
|
|
@@ -129,11 +166,14 @@ export function generateIv({ algorithm, ivLength }) {
|
|
|
129
166
|
* @returns {string}
|
|
130
167
|
*/
|
|
131
168
|
export function encodeFieldPathSalt(fieldPath) {
|
|
132
|
-
if (
|
|
133
|
-
throw new
|
|
169
|
+
if (typeof fieldPath !== 'string' || fieldPath.length === 0) {
|
|
170
|
+
throw new YamlockValidationError(
|
|
171
|
+
'Field path must be a non-empty string.',
|
|
172
|
+
{ code: YAMLOCK_ERROR_CODES.INVALID_FIELD_PATH }
|
|
173
|
+
);
|
|
134
174
|
}
|
|
135
175
|
|
|
136
|
-
return Buffer.from(
|
|
176
|
+
return Buffer.from(fieldPath, 'utf8').toString('base64');
|
|
137
177
|
}
|
|
138
178
|
|
|
139
179
|
/**
|
|
@@ -143,7 +183,9 @@ export function encodeFieldPathSalt(fieldPath) {
|
|
|
143
183
|
*/
|
|
144
184
|
export function decodeFieldPathSalt(salt) {
|
|
145
185
|
if (!salt) {
|
|
146
|
-
throw new
|
|
186
|
+
throw new YamlockPayloadError('Salt value is required.', {
|
|
187
|
+
code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
|
|
188
|
+
});
|
|
147
189
|
}
|
|
148
190
|
|
|
149
191
|
return Buffer.from(String(salt), 'base64').toString('utf8');
|
|
@@ -160,7 +202,10 @@ export function decodeFieldPathSalt(salt) {
|
|
|
160
202
|
*/
|
|
161
203
|
export function formatPayload({ algorithm, salt, iv, data }) {
|
|
162
204
|
if (!algorithm || !salt || !iv || !data) {
|
|
163
|
-
throw new
|
|
205
|
+
throw new YamlockValidationError(
|
|
206
|
+
'Algorithm, salt, IV, and data are required to format payload.',
|
|
207
|
+
{ code: YAMLOCK_ERROR_CODES.INVALID_OPTIONS }
|
|
208
|
+
);
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
return [
|
|
@@ -191,17 +236,23 @@ export function isYamlockPayload(candidate) {
|
|
|
191
236
|
*/
|
|
192
237
|
export function parsePayload(value) {
|
|
193
238
|
if (!isYamlockPayload(value)) {
|
|
194
|
-
throw new
|
|
239
|
+
throw new YamlockPayloadError('Value is not a yamlock payload.', {
|
|
240
|
+
code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
|
|
241
|
+
});
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
const parts = value.split(YAMLOCK_DELIMITER);
|
|
198
245
|
if (parts.length !== 5) {
|
|
199
|
-
throw new
|
|
246
|
+
throw new YamlockPayloadError('Malformed yamlock payload.', {
|
|
247
|
+
code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
|
|
248
|
+
});
|
|
200
249
|
}
|
|
201
250
|
|
|
202
251
|
const [, algorithm, salt, ivBase64, dataBase64] = parts;
|
|
203
252
|
if (!algorithm || !salt || !ivBase64 || !dataBase64) {
|
|
204
|
-
throw new
|
|
253
|
+
throw new YamlockPayloadError('Malformed yamlock payload segments.', {
|
|
254
|
+
code: YAMLOCK_ERROR_CODES.INVALID_PAYLOAD
|
|
255
|
+
});
|
|
205
256
|
}
|
|
206
257
|
|
|
207
258
|
return {
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const ERROR_CODE_PATTERN = /^ERR_[A-Z0-9_]+$/;
|
|
2
|
+
|
|
3
|
+
export const YAMLOCK_ERROR_CODES = Object.freeze({
|
|
4
|
+
ALREADY_ENCRYPTED: 'ERR_ALREADY_ENCRYPTED',
|
|
5
|
+
AUTHENTICATION_FAILED: 'ERR_AUTHENTICATION_FAILED',
|
|
6
|
+
CIRCULAR_CONFIG: 'ERR_CIRCULAR_CONFIG',
|
|
7
|
+
DECRYPTION_FAILED: 'ERR_DECRYPTION_FAILED',
|
|
8
|
+
FIELD_PATH_MISMATCH: 'ERR_FIELD_PATH_MISMATCH',
|
|
9
|
+
INVALID_CONFIG_OPTIONS: 'ERR_INVALID_CONFIG_OPTIONS',
|
|
10
|
+
INVALID_CONFIG_ROOT: 'ERR_INVALID_CONFIG_ROOT',
|
|
11
|
+
INVALID_EXISTING_PAYLOAD_POLICY: 'ERR_INVALID_EXISTING_PAYLOAD_POLICY',
|
|
12
|
+
INVALID_FIELD_PATH: 'ERR_INVALID_FIELD_PATH',
|
|
13
|
+
INVALID_KEY: 'ERR_INVALID_KEY',
|
|
14
|
+
INVALID_MODE: 'ERR_INVALID_MODE',
|
|
15
|
+
INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY',
|
|
16
|
+
INVALID_OPTIONS: 'ERR_INVALID_OPTIONS',
|
|
17
|
+
INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS',
|
|
18
|
+
INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER',
|
|
19
|
+
INVALID_PATHS: 'ERR_INVALID_PATHS',
|
|
20
|
+
INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD',
|
|
21
|
+
INVALID_VALUE: 'ERR_INVALID_VALUE',
|
|
22
|
+
NON_STRING_VALUE: 'ERR_NON_STRING_VALUE',
|
|
23
|
+
PATH_COLLISION: 'ERR_PATH_COLLISION',
|
|
24
|
+
PAYLOAD_TOO_LARGE: 'ERR_PAYLOAD_TOO_LARGE',
|
|
25
|
+
UNSUPPORTED_ALGORITHM: 'ERR_UNSUPPORTED_ALGORITHM',
|
|
26
|
+
UNSUPPORTED_CONFIG_VALUE: 'ERR_UNSUPPORTED_CONFIG_VALUE',
|
|
27
|
+
UNSUPPORTED_PAYLOAD: 'ERR_UNSUPPORTED_PAYLOAD',
|
|
28
|
+
UNSUPPORTED_PAYLOAD_VERSION: 'ERR_UNSUPPORTED_PAYLOAD_VERSION',
|
|
29
|
+
VALUE_TOO_LARGE: 'ERR_VALUE_TOO_LARGE'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export class YamlockError extends Error {
|
|
33
|
+
constructor(message, { code, cause } = {}) {
|
|
34
|
+
if (typeof code !== 'string' || !ERROR_CODE_PATTERN.test(code)) {
|
|
35
|
+
throw new TypeError('YamlockError requires an ERR_* code.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
39
|
+
this.name = new.target.name;
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class YamlockValidationError extends YamlockError {}
|
|
45
|
+
|
|
46
|
+
export class YamlockPayloadError extends YamlockError {}
|
|
47
|
+
|
|
48
|
+
export class YamlockAuthenticationError extends YamlockPayloadError {
|
|
49
|
+
constructor(message = 'Payload authentication failed.', options = {}) {
|
|
50
|
+
super(message, {
|
|
51
|
+
...options,
|
|
52
|
+
code: YAMLOCK_ERROR_CODES.AUTHENTICATION_FAILED
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class YamlockDecryptionError extends YamlockError {}
|
|
58
|
+
|
|
59
|
+
export class YamlockConfigError extends YamlockValidationError {}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
export type YamlockKey = string | Buffer;
|
|
4
|
+
export type YamlockPathSegment = string | number;
|
|
5
|
+
export type YamlockFormatVersion = 1 | 2;
|
|
6
|
+
export type YamlockNonStringPolicy = 'ignore' | 'stringify' | 'error';
|
|
7
|
+
export type YamlockExistingPayloadPolicy = 'preserve' | 'error' | 'encrypt';
|
|
8
|
+
export type YamlockConfig = Record<string, unknown> | unknown[];
|
|
9
|
+
|
|
10
|
+
export interface YamlockCryptoOptions {
|
|
11
|
+
algorithm?: string;
|
|
12
|
+
keyLength?: number;
|
|
13
|
+
ivLength?: number;
|
|
14
|
+
authTagLength?: number;
|
|
15
|
+
formatVersion?: YamlockFormatVersion;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type YamlockCryptoOptionsInput = string | YamlockCryptoOptions;
|
|
19
|
+
|
|
20
|
+
export interface ProcessConfigCommonOptions {
|
|
21
|
+
key: YamlockKey;
|
|
22
|
+
algorithm?: YamlockCryptoOptionsInput;
|
|
23
|
+
algorithmOptions?: YamlockCryptoOptions;
|
|
24
|
+
formatVersion?: YamlockFormatVersion;
|
|
25
|
+
nonStringPolicy?: YamlockNonStringPolicy;
|
|
26
|
+
pathSerializer?: (segments: YamlockPathSegment[]) => string;
|
|
27
|
+
paths?: string[];
|
|
28
|
+
parentPath?: YamlockPathSegment[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EncryptProcessConfigOptions extends ProcessConfigCommonOptions {
|
|
32
|
+
mode: 'encrypt';
|
|
33
|
+
existingPayloadPolicy?: YamlockExistingPayloadPolicy;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DecryptProcessConfigOptions extends ProcessConfigCommonOptions {
|
|
37
|
+
mode: 'decrypt';
|
|
38
|
+
existingPayloadPolicy?: never;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ProcessConfigOptions =
|
|
42
|
+
| EncryptProcessConfigOptions
|
|
43
|
+
| DecryptProcessConfigOptions;
|
|
44
|
+
|
|
45
|
+
export const YAMLOCK_ERROR_CODES: Readonly<{
|
|
46
|
+
ALREADY_ENCRYPTED: 'ERR_ALREADY_ENCRYPTED';
|
|
47
|
+
AUTHENTICATION_FAILED: 'ERR_AUTHENTICATION_FAILED';
|
|
48
|
+
CIRCULAR_CONFIG: 'ERR_CIRCULAR_CONFIG';
|
|
49
|
+
DECRYPTION_FAILED: 'ERR_DECRYPTION_FAILED';
|
|
50
|
+
FIELD_PATH_MISMATCH: 'ERR_FIELD_PATH_MISMATCH';
|
|
51
|
+
INVALID_CONFIG_OPTIONS: 'ERR_INVALID_CONFIG_OPTIONS';
|
|
52
|
+
INVALID_CONFIG_ROOT: 'ERR_INVALID_CONFIG_ROOT';
|
|
53
|
+
INVALID_EXISTING_PAYLOAD_POLICY: 'ERR_INVALID_EXISTING_PAYLOAD_POLICY';
|
|
54
|
+
INVALID_FIELD_PATH: 'ERR_INVALID_FIELD_PATH';
|
|
55
|
+
INVALID_KEY: 'ERR_INVALID_KEY';
|
|
56
|
+
INVALID_MODE: 'ERR_INVALID_MODE';
|
|
57
|
+
INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY';
|
|
58
|
+
INVALID_OPTIONS: 'ERR_INVALID_OPTIONS';
|
|
59
|
+
INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS';
|
|
60
|
+
INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER';
|
|
61
|
+
INVALID_PATHS: 'ERR_INVALID_PATHS';
|
|
62
|
+
INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD';
|
|
63
|
+
INVALID_VALUE: 'ERR_INVALID_VALUE';
|
|
64
|
+
NON_STRING_VALUE: 'ERR_NON_STRING_VALUE';
|
|
65
|
+
PATH_COLLISION: 'ERR_PATH_COLLISION';
|
|
66
|
+
PAYLOAD_TOO_LARGE: 'ERR_PAYLOAD_TOO_LARGE';
|
|
67
|
+
UNSUPPORTED_ALGORITHM: 'ERR_UNSUPPORTED_ALGORITHM';
|
|
68
|
+
UNSUPPORTED_CONFIG_VALUE: 'ERR_UNSUPPORTED_CONFIG_VALUE';
|
|
69
|
+
UNSUPPORTED_PAYLOAD: 'ERR_UNSUPPORTED_PAYLOAD';
|
|
70
|
+
UNSUPPORTED_PAYLOAD_VERSION: 'ERR_UNSUPPORTED_PAYLOAD_VERSION';
|
|
71
|
+
VALUE_TOO_LARGE: 'ERR_VALUE_TOO_LARGE';
|
|
72
|
+
}>;
|
|
73
|
+
|
|
74
|
+
export type YamlockErrorCode =
|
|
75
|
+
(typeof YAMLOCK_ERROR_CODES)[keyof typeof YAMLOCK_ERROR_CODES];
|
|
76
|
+
|
|
77
|
+
export interface YamlockErrorOptions {
|
|
78
|
+
code: YamlockErrorCode | `ERR_${string}`;
|
|
79
|
+
cause?: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class YamlockError extends Error {
|
|
83
|
+
constructor(message: string, options: YamlockErrorOptions);
|
|
84
|
+
code: YamlockErrorCode | `ERR_${string}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class YamlockValidationError extends YamlockError {}
|
|
88
|
+
export class YamlockPayloadError extends YamlockError {}
|
|
89
|
+
|
|
90
|
+
export class YamlockAuthenticationError extends YamlockPayloadError {
|
|
91
|
+
constructor(message?: string, options?: { cause?: unknown });
|
|
92
|
+
code: 'ERR_AUTHENTICATION_FAILED';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class YamlockDecryptionError extends YamlockError {}
|
|
96
|
+
export class YamlockConfigError extends YamlockValidationError {}
|
|
97
|
+
|
|
98
|
+
export function encryptValue(
|
|
99
|
+
value: string,
|
|
100
|
+
key: YamlockKey,
|
|
101
|
+
fieldPath: string,
|
|
102
|
+
algorithmOptions?: YamlockCryptoOptionsInput
|
|
103
|
+
): string;
|
|
104
|
+
|
|
105
|
+
export function decryptValue(
|
|
106
|
+
encryptedValue: string,
|
|
107
|
+
key: YamlockKey,
|
|
108
|
+
fieldPath: string,
|
|
109
|
+
algorithmOptions?: YamlockCryptoOptionsInput
|
|
110
|
+
): string;
|
|
111
|
+
|
|
112
|
+
export function processConfig<T extends YamlockConfig>(
|
|
113
|
+
node: T,
|
|
114
|
+
options: ProcessConfigOptions & { nonStringPolicy: 'stringify' }
|
|
115
|
+
): YamlockConfig;
|
|
116
|
+
|
|
117
|
+
export function processConfig<T extends YamlockConfig>(
|
|
118
|
+
node: T,
|
|
119
|
+
options: ProcessConfigOptions
|
|
120
|
+
): T;
|
|
121
|
+
|
|
122
|
+
export function serializePath(segments: YamlockPathSegment[]): string;
|
|
123
|
+
export function getSupportedAlgorithms(): string[];
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export { encryptValue } from './crypto/encrypt.js';
|
|
2
2
|
export { decryptValue } from './crypto/decrypt.js';
|
|
3
3
|
export { processConfig } from './utils/config.js';
|
|
4
|
+
export { serializePath } from './utils/path.js';
|
|
4
5
|
export { listSupportedAlgorithms as getSupportedAlgorithms } from './crypto/utils.js';
|
|
6
|
+
export {
|
|
7
|
+
YAMLOCK_ERROR_CODES,
|
|
8
|
+
YamlockAuthenticationError,
|
|
9
|
+
YamlockConfigError,
|
|
10
|
+
YamlockDecryptionError,
|
|
11
|
+
YamlockError,
|
|
12
|
+
YamlockPayloadError,
|
|
13
|
+
YamlockValidationError
|
|
14
|
+
} from './errors.js';
|