@nodetool-ai/security 0.8.8 → 0.8.9-beta.2
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/dist/.nodetool-build-stamp +1 -1
- package/dist/crypto.d.ts +1 -78
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +1 -214
- package/dist/crypto.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-10-
|
|
1
|
+
2026-10-31T10:34:04.590Z
|
package/dist/crypto.d.ts
CHANGED
|
@@ -1,79 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Cryptographic utilities for secret encryption and decryption.
|
|
3
|
-
*
|
|
4
|
-
* This module provides utilities for encrypting and decrypting secrets using
|
|
5
|
-
* AES-256-GCM symmetric encryption with PBKDF2 key derivation.
|
|
6
|
-
*
|
|
7
|
-
* The master key is combined with user_id (as salt) to derive user-specific
|
|
8
|
-
* encryption keys via PBKDF2-SHA256 with 100,000 iterations.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Generate a new random master key.
|
|
12
|
-
*
|
|
13
|
-
* @returns A base64-encoded 32-byte master key string.
|
|
14
|
-
*/
|
|
15
|
-
export declare function generateMasterKey(): string;
|
|
16
|
-
/**
|
|
17
|
-
* Derive an encryption key from the master key using user_id as salt.
|
|
18
|
-
*
|
|
19
|
-
* This ensures each user's secrets are encrypted with a unique derived key,
|
|
20
|
-
* providing isolation between users even if the master key is compromised.
|
|
21
|
-
*
|
|
22
|
-
* @param masterKey - The master key (base64-encoded or raw string).
|
|
23
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
24
|
-
* @returns A 32-byte derived key as a Buffer.
|
|
25
|
-
*/
|
|
26
|
-
export declare function deriveKey(masterKey: string, userId: string): Buffer;
|
|
27
|
-
/**
|
|
28
|
-
* Encrypt a plaintext value using AES-256-GCM.
|
|
29
|
-
*
|
|
30
|
-
* The output format is: base64(iv || ciphertext || authTag)
|
|
31
|
-
*
|
|
32
|
-
* @param masterKey - The master key to use for encryption.
|
|
33
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
34
|
-
* @param plaintext - The plaintext string to encrypt.
|
|
35
|
-
* @returns The encrypted value as a base64-encoded string.
|
|
36
|
-
*/
|
|
37
|
-
export declare function encrypt(masterKey: string, userId: string, plaintext: string): string;
|
|
38
|
-
/**
|
|
39
|
-
* Decrypt an encrypted value using AES-256-GCM.
|
|
40
|
-
*
|
|
41
|
-
* @param masterKey - The master key to use for decryption.
|
|
42
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
43
|
-
* @param encryptedValue - The encrypted value as a base64-encoded string.
|
|
44
|
-
* @returns The decrypted plaintext string.
|
|
45
|
-
* @throws {Error} If the master key is incorrect or the data is corrupted.
|
|
46
|
-
*/
|
|
47
|
-
export declare function decrypt(masterKey: string, userId: string, encryptedValue: string): string;
|
|
48
|
-
/**
|
|
49
|
-
* Decrypt a Fernet token encrypted by the Python side.
|
|
50
|
-
*
|
|
51
|
-
* Python uses: PBKDF2(master_key_bytes, user_id, 100k, 32) → fernet_key
|
|
52
|
-
* Fernet = AES-128-CBC + HMAC-SHA256, token = base64url(ver|ts|iv|ct|hmac)
|
|
53
|
-
*
|
|
54
|
-
* @param masterKey - The master key as a base64 or base64url string.
|
|
55
|
-
* @param userId - The user ID used as PBKDF2 salt.
|
|
56
|
-
* @param encryptedValue - The Fernet token (base64url encoded).
|
|
57
|
-
*/
|
|
58
|
-
export declare function decryptFernet(masterKey: string, userId: string, encryptedValue: string): string;
|
|
59
|
-
/**
|
|
60
|
-
* Encrypt a value using Fernet (Python-compatible).
|
|
61
|
-
*
|
|
62
|
-
* Produces a token that Python's cryptography.Fernet can decrypt.
|
|
63
|
-
* Format: base64url(version(1) | timestamp(8) | iv(16) | ciphertext(N) | hmac(32))
|
|
64
|
-
*
|
|
65
|
-
* @param masterKey - The master key as a base64 or utf-8 string.
|
|
66
|
-
* @param userId - The user ID used as PBKDF2 salt.
|
|
67
|
-
* @param plaintext - The plaintext to encrypt.
|
|
68
|
-
*/
|
|
69
|
-
export declare function encryptFernet(masterKey: string, userId: string, plaintext: string): string;
|
|
70
|
-
/**
|
|
71
|
-
* Check if a master key is valid by attempting to decrypt a test value.
|
|
72
|
-
*
|
|
73
|
-
* @param masterKey - The master key to validate.
|
|
74
|
-
* @param testEncryptedValue - An encrypted test value to decrypt.
|
|
75
|
-
* @param userId - The user ID used as salt for the test value.
|
|
76
|
-
* @returns True if the master key is valid, false otherwise.
|
|
77
|
-
*/
|
|
78
|
-
export declare function isValidMasterKey(masterKey: string, testEncryptedValue: string, userId: string): boolean;
|
|
1
|
+
export { generateMasterKey, deriveKey, encrypt, decrypt, decryptFernet, encryptFernet, isValidMasterKey } from "@nodetool-ai/security-core";
|
|
79
2
|
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EACjB,MAAM,4BAA4B,CAAC"}
|
package/dist/crypto.js
CHANGED
|
@@ -1,215 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Cryptographic utilities for secret encryption and decryption.
|
|
3
|
-
*
|
|
4
|
-
* This module provides utilities for encrypting and decrypting secrets using
|
|
5
|
-
* AES-256-GCM symmetric encryption with PBKDF2 key derivation.
|
|
6
|
-
*
|
|
7
|
-
* The master key is combined with user_id (as salt) to derive user-specific
|
|
8
|
-
* encryption keys via PBKDF2-SHA256 with 100,000 iterations.
|
|
9
|
-
*/
|
|
10
|
-
import { randomBytes, pbkdf2Sync, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from "node:crypto";
|
|
11
|
-
import { createLogger } from "@nodetool-ai/config";
|
|
12
|
-
// Stryker disable next-line StringLiteral: logger name is diagnostic, not behavior
|
|
13
|
-
const log = createLogger("nodetool.security.crypto");
|
|
14
|
-
// Fernet token version byte
|
|
15
|
-
const FERNET_VERSION = 0x80;
|
|
16
|
-
const PBKDF2_ITERATIONS = 100_000;
|
|
17
|
-
const KEY_LENGTH = 32; // 256 bits for AES-256
|
|
18
|
-
const IV_LENGTH = 12; // 96 bits for GCM
|
|
19
|
-
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
20
|
-
/**
|
|
21
|
-
* Generate a new random master key.
|
|
22
|
-
*
|
|
23
|
-
* @returns A base64-encoded 32-byte master key string.
|
|
24
|
-
*/
|
|
25
|
-
export function generateMasterKey() {
|
|
26
|
-
return randomBytes(KEY_LENGTH).toString("base64");
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Derive an encryption key from the master key using user_id as salt.
|
|
30
|
-
*
|
|
31
|
-
* This ensures each user's secrets are encrypted with a unique derived key,
|
|
32
|
-
* providing isolation between users even if the master key is compromised.
|
|
33
|
-
*
|
|
34
|
-
* @param masterKey - The master key (base64-encoded or raw string).
|
|
35
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
36
|
-
* @returns A 32-byte derived key as a Buffer.
|
|
37
|
-
*/
|
|
38
|
-
export function deriveKey(masterKey, userId) {
|
|
39
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
40
|
-
const salt = Buffer.from(userId, "utf-8");
|
|
41
|
-
return pbkdf2Sync(masterKey, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Encrypt a plaintext value using AES-256-GCM.
|
|
45
|
-
*
|
|
46
|
-
* The output format is: base64(iv || ciphertext || authTag)
|
|
47
|
-
*
|
|
48
|
-
* @param masterKey - The master key to use for encryption.
|
|
49
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
50
|
-
* @param plaintext - The plaintext string to encrypt.
|
|
51
|
-
* @returns The encrypted value as a base64-encoded string.
|
|
52
|
-
*/
|
|
53
|
-
export function encrypt(masterKey, userId, plaintext) {
|
|
54
|
-
const key = deriveKey(masterKey, userId);
|
|
55
|
-
const iv = randomBytes(IV_LENGTH);
|
|
56
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
57
|
-
const encrypted = Buffer.concat([
|
|
58
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
59
|
-
cipher.update(plaintext, "utf-8"),
|
|
60
|
-
cipher.final()
|
|
61
|
-
]);
|
|
62
|
-
const authTag = cipher.getAuthTag();
|
|
63
|
-
// Pack: iv (12) + ciphertext (variable) + authTag (16)
|
|
64
|
-
const packed = Buffer.concat([iv, encrypted, authTag]);
|
|
65
|
-
return packed.toString("base64");
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Decrypt an encrypted value using AES-256-GCM.
|
|
69
|
-
*
|
|
70
|
-
* @param masterKey - The master key to use for decryption.
|
|
71
|
-
* @param userId - The user ID to use as salt for key derivation.
|
|
72
|
-
* @param encryptedValue - The encrypted value as a base64-encoded string.
|
|
73
|
-
* @returns The decrypted plaintext string.
|
|
74
|
-
* @throws {Error} If the master key is incorrect or the data is corrupted.
|
|
75
|
-
*/
|
|
76
|
-
export function decrypt(masterKey, userId, encryptedValue) {
|
|
77
|
-
const key = deriveKey(masterKey, userId);
|
|
78
|
-
const packed = Buffer.from(encryptedValue, "base64");
|
|
79
|
-
if (packed.length < IV_LENGTH + AUTH_TAG_LENGTH) {
|
|
80
|
-
throw new Error("Failed to decrypt secret: data too short");
|
|
81
|
-
}
|
|
82
|
-
const iv = packed.subarray(0, IV_LENGTH);
|
|
83
|
-
const authTag = packed.subarray(packed.length - AUTH_TAG_LENGTH);
|
|
84
|
-
const ciphertext = packed.subarray(IV_LENGTH, packed.length - AUTH_TAG_LENGTH);
|
|
85
|
-
try {
|
|
86
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
87
|
-
decipher.setAuthTag(authTag);
|
|
88
|
-
const decrypted = Buffer.concat([
|
|
89
|
-
decipher.update(ciphertext),
|
|
90
|
-
decipher.final()
|
|
91
|
-
]);
|
|
92
|
-
return decrypted.toString("utf-8");
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
// Stryker disable next-line all: diagnostic log, not behavior
|
|
96
|
-
log.debug("AES-GCM decryption failed (may fall back to Fernet)", {
|
|
97
|
-
error: String(err)
|
|
98
|
-
});
|
|
99
|
-
throw new Error("Failed to decrypt secret");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Decrypt a Fernet token encrypted by the Python side.
|
|
104
|
-
*
|
|
105
|
-
* Python uses: PBKDF2(master_key_bytes, user_id, 100k, 32) → fernet_key
|
|
106
|
-
* Fernet = AES-128-CBC + HMAC-SHA256, token = base64url(ver|ts|iv|ct|hmac)
|
|
107
|
-
*
|
|
108
|
-
* @param masterKey - The master key as a base64 or base64url string.
|
|
109
|
-
* @param userId - The user ID used as PBKDF2 salt.
|
|
110
|
-
* @param encryptedValue - The Fernet token (base64url encoded).
|
|
111
|
-
*/
|
|
112
|
-
export function decryptFernet(masterKey, userId, encryptedValue) {
|
|
113
|
-
// Python uses master_key.encode() — the UTF-8 bytes of the base64 string itself as PBKDF2 password
|
|
114
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
115
|
-
const masterKeyBytes = Buffer.from(masterKey, "utf-8");
|
|
116
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
117
|
-
const salt = Buffer.from(userId, "utf-8");
|
|
118
|
-
const derived = pbkdf2Sync(masterKeyBytes, salt, PBKDF2_ITERATIONS, 32, "sha256");
|
|
119
|
-
// Fernet splits the 32-byte derived key: first 16 = HMAC key, last 16 = AES-128 key
|
|
120
|
-
const hmacKey = derived.subarray(0, 16);
|
|
121
|
-
const aesKey = derived.subarray(16, 32);
|
|
122
|
-
// Decode Fernet token from base64url
|
|
123
|
-
const b64token = encryptedValue.replace(/-/g, "+").replace(/_/g, "/");
|
|
124
|
-
const token = Buffer.from(
|
|
125
|
-
// Stryker disable next-line StringLiteral,ArithmeticOperator: re-padding is a no-op — Node's base64 decoder ignores +/- padding, so these mutants are equivalent
|
|
126
|
-
b64token + "=".repeat((4 - (b64token.length % 4)) % 4), "base64");
|
|
127
|
-
// Token layout: version(1) + timestamp(8) + iv(16) + ciphertext(N) + hmac(32)
|
|
128
|
-
if (token.length < 1 + 8 + 16 + 32) {
|
|
129
|
-
throw new Error("Invalid Fernet token: too short");
|
|
130
|
-
}
|
|
131
|
-
if (token[0] !== 0x80) {
|
|
132
|
-
throw new Error(`Invalid Fernet token version: ${token[0]}`);
|
|
133
|
-
}
|
|
134
|
-
const body = token.subarray(0, token.length - 32);
|
|
135
|
-
const tokenHmac = token.subarray(token.length - 32);
|
|
136
|
-
const iv = body.subarray(9, 25); // 1 + 8 = 9
|
|
137
|
-
const ciphertext = body.subarray(25);
|
|
138
|
-
// Verify HMAC-SHA256
|
|
139
|
-
const expectedHmac = createHmac("sha256", hmacKey).update(body).digest();
|
|
140
|
-
if (!timingSafeEqual(expectedHmac, tokenHmac)) {
|
|
141
|
-
throw new Error("Invalid Fernet token: HMAC mismatch");
|
|
142
|
-
}
|
|
143
|
-
// Decrypt AES-128-CBC and strip PKCS7 padding
|
|
144
|
-
const decipher = createDecipheriv("aes-128-cbc", aesKey, iv);
|
|
145
|
-
const decrypted = Buffer.concat([
|
|
146
|
-
decipher.update(ciphertext),
|
|
147
|
-
decipher.final()
|
|
148
|
-
]);
|
|
149
|
-
return decrypted.toString("utf-8");
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Encrypt a value using Fernet (Python-compatible).
|
|
153
|
-
*
|
|
154
|
-
* Produces a token that Python's cryptography.Fernet can decrypt.
|
|
155
|
-
* Format: base64url(version(1) | timestamp(8) | iv(16) | ciphertext(N) | hmac(32))
|
|
156
|
-
*
|
|
157
|
-
* @param masterKey - The master key as a base64 or utf-8 string.
|
|
158
|
-
* @param userId - The user ID used as PBKDF2 salt.
|
|
159
|
-
* @param plaintext - The plaintext to encrypt.
|
|
160
|
-
*/
|
|
161
|
-
export function encryptFernet(masterKey, userId, plaintext) {
|
|
162
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
163
|
-
const masterKeyBytes = Buffer.from(masterKey, "utf-8");
|
|
164
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
165
|
-
const salt = Buffer.from(userId, "utf-8");
|
|
166
|
-
const derived = pbkdf2Sync(masterKeyBytes, salt, PBKDF2_ITERATIONS, 32, "sha256");
|
|
167
|
-
const hmacKey = derived.subarray(0, 16);
|
|
168
|
-
const aesKey = derived.subarray(16, 32);
|
|
169
|
-
const iv = randomBytes(16);
|
|
170
|
-
// Timestamp: seconds since epoch as big-endian uint64
|
|
171
|
-
const timestamp = BigInt(Math.floor(Date.now() / 1000));
|
|
172
|
-
const tsBuf = Buffer.alloc(8);
|
|
173
|
-
tsBuf.writeBigUInt64BE(timestamp);
|
|
174
|
-
// Pad plaintext to 16-byte boundary (PKCS7)
|
|
175
|
-
// Stryker disable next-line StringLiteral: encoding arg — Node decodes "" as utf-8, so this mutant is behaviorally equivalent
|
|
176
|
-
const plaintextBuf = Buffer.from(plaintext, "utf-8");
|
|
177
|
-
const padLen = 16 - (plaintextBuf.length % 16);
|
|
178
|
-
const padded = Buffer.concat([plaintextBuf, Buffer.alloc(padLen, padLen)]);
|
|
179
|
-
const cipher = createCipheriv("aes-128-cbc", aesKey, iv);
|
|
180
|
-
cipher.setAutoPadding(false);
|
|
181
|
-
const ciphertext = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
182
|
-
// Build body: version(1) + timestamp(8) + iv(16) + ciphertext
|
|
183
|
-
const body = Buffer.concat([
|
|
184
|
-
Buffer.from([FERNET_VERSION]),
|
|
185
|
-
tsBuf,
|
|
186
|
-
iv,
|
|
187
|
-
ciphertext
|
|
188
|
-
]);
|
|
189
|
-
// HMAC-SHA256 over body
|
|
190
|
-
const hmac = createHmac("sha256", hmacKey).update(body).digest();
|
|
191
|
-
const token = Buffer.concat([body, hmac]);
|
|
192
|
-
// Encode as base64url *with* padding. Python's cryptography.Fernet decodes
|
|
193
|
-
// tokens with base64.urlsafe_b64decode, which rejects unpadded input
|
|
194
|
-
// ("Incorrect padding" -> InvalidToken), so the "=" padding must be kept for
|
|
195
|
-
// cross-runtime compatibility.
|
|
196
|
-
return token.toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Check if a master key is valid by attempting to decrypt a test value.
|
|
200
|
-
*
|
|
201
|
-
* @param masterKey - The master key to validate.
|
|
202
|
-
* @param testEncryptedValue - An encrypted test value to decrypt.
|
|
203
|
-
* @param userId - The user ID used as salt for the test value.
|
|
204
|
-
* @returns True if the master key is valid, false otherwise.
|
|
205
|
-
*/
|
|
206
|
-
export function isValidMasterKey(masterKey, testEncryptedValue, userId) {
|
|
207
|
-
try {
|
|
208
|
-
decrypt(masterKey, userId, testEncryptedValue);
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
1
|
+
export { generateMasterKey, deriveKey, encrypt, decrypt, decryptFernet, encryptFernet, isValidMasterKey } from "@nodetool-ai/security-core";
|
|
215
2
|
//# sourceMappingURL=crypto.js.map
|
package/dist/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EACjB,MAAM,4BAA4B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodetool-ai/security",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"lint": "node ../../scripts/run-tsc.mjs --noEmit"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@nodetool-ai/config": "0.8.
|
|
19
|
-
"keytar": "^7.9.0"
|
|
18
|
+
"@nodetool-ai/config": "0.8.9-beta.2",
|
|
19
|
+
"keytar": "^7.9.0",
|
|
20
|
+
"@nodetool-ai/security-core": "0.8.9-beta.2"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@stryker-mutator/core": "^9.6.1",
|