cryptoserve 0.1.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 +183 -0
- package/bin/cryptoserve.mjs +812 -0
- package/lib/cli-style.mjs +217 -0
- package/lib/client.mjs +138 -0
- package/lib/context-resolver.mjs +339 -0
- package/lib/credentials.mjs +67 -0
- package/lib/init.mjs +241 -0
- package/lib/keychain.mjs +303 -0
- package/lib/local-crypto.mjs +218 -0
- package/lib/pqc-engine.mjs +636 -0
- package/lib/scanner.mjs +323 -0
- package/lib/vault.mjs +242 -0
- package/package.json +36 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local cryptographic operations for the CryptoServe SDK.
|
|
3
|
+
*
|
|
4
|
+
* Port of sdk/python/cryptoserve/_local_crypto.py.
|
|
5
|
+
* Cross-SDK compatible blob format — data encrypted by Python can be
|
|
6
|
+
* decrypted by Node.js and vice versa.
|
|
7
|
+
*
|
|
8
|
+
* Zero dependencies — uses only node:crypto.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createCipheriv,
|
|
13
|
+
createDecipheriv,
|
|
14
|
+
randomBytes,
|
|
15
|
+
scryptSync,
|
|
16
|
+
pbkdf2Sync,
|
|
17
|
+
createHash,
|
|
18
|
+
} from 'node:crypto';
|
|
19
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
const FORMAT_VERSION = 4;
|
|
22
|
+
const AES_GCM_NONCE_SIZE = 12;
|
|
23
|
+
const CHACHA_NONCE_SIZE = 12;
|
|
24
|
+
const AUTH_TAG_LENGTH = 16;
|
|
25
|
+
|
|
26
|
+
const ALGORITHMS = {
|
|
27
|
+
'AES-256-GCM': { cipher: 'aes-256-gcm', keySize: 32, nonceSize: AES_GCM_NONCE_SIZE },
|
|
28
|
+
'AES-128-GCM': { cipher: 'aes-128-gcm', keySize: 16, nonceSize: AES_GCM_NONCE_SIZE },
|
|
29
|
+
'ChaCha20-Poly1305': { cipher: 'chacha20-poly1305', keySize: 32, nonceSize: CHACHA_NONCE_SIZE },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Core encrypt/decrypt (cross-SDK blob format)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export function encrypt(plaintext, key, keyId, context, algorithm = 'AES-256-GCM', associatedData = null) {
|
|
37
|
+
const spec = ALGORITHMS[algorithm];
|
|
38
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
39
|
+
if (key.length !== spec.keySize) {
|
|
40
|
+
throw new Error(`Invalid key size: expected ${spec.keySize}, got ${key.length}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nonce = randomBytes(spec.nonceSize);
|
|
44
|
+
const cipher = createCipheriv(spec.cipher, key, nonce, { authTagLength: AUTH_TAG_LENGTH });
|
|
45
|
+
|
|
46
|
+
if (associatedData) cipher.setAAD(associatedData);
|
|
47
|
+
|
|
48
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
49
|
+
const authTag = cipher.getAuthTag();
|
|
50
|
+
|
|
51
|
+
const header = {
|
|
52
|
+
v: FORMAT_VERSION,
|
|
53
|
+
ctx: context,
|
|
54
|
+
kid: keyId,
|
|
55
|
+
alg: algorithm,
|
|
56
|
+
nonce: nonce.toString('base64'),
|
|
57
|
+
local: true,
|
|
58
|
+
};
|
|
59
|
+
if (associatedData) header.aad_len = associatedData.length;
|
|
60
|
+
|
|
61
|
+
const headerBytes = Buffer.from(JSON.stringify(header));
|
|
62
|
+
const headerLen = Buffer.alloc(2);
|
|
63
|
+
headerLen.writeUInt16BE(headerBytes.length);
|
|
64
|
+
|
|
65
|
+
// ciphertext includes auth tag appended (matches Python cryptography library behavior)
|
|
66
|
+
return Buffer.concat([headerLen, headerBytes, encrypted, authTag]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function decrypt(ciphertext, key, associatedData = null) {
|
|
70
|
+
const { header, rawCiphertext } = parseCiphertext(ciphertext);
|
|
71
|
+
|
|
72
|
+
const algorithm = header.alg || 'AES-256-GCM';
|
|
73
|
+
const spec = ALGORITHMS[algorithm];
|
|
74
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
75
|
+
if (key.length !== spec.keySize) {
|
|
76
|
+
throw new Error(`Invalid key size: expected ${spec.keySize}, got ${key.length}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const nonce = Buffer.from(header.nonce, 'base64');
|
|
80
|
+
|
|
81
|
+
// Separate ciphertext from auth tag (last 16 bytes)
|
|
82
|
+
const encData = rawCiphertext.subarray(0, rawCiphertext.length - AUTH_TAG_LENGTH);
|
|
83
|
+
const authTag = rawCiphertext.subarray(rawCiphertext.length - AUTH_TAG_LENGTH);
|
|
84
|
+
|
|
85
|
+
const decipher = createDecipheriv(spec.cipher, key, nonce, { authTagLength: AUTH_TAG_LENGTH });
|
|
86
|
+
decipher.setAuthTag(authTag);
|
|
87
|
+
|
|
88
|
+
if (associatedData) decipher.setAAD(associatedData);
|
|
89
|
+
|
|
90
|
+
return Buffer.concat([decipher.update(encData), decipher.final()]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseCiphertext(ciphertext) {
|
|
94
|
+
if (ciphertext.length < 3) throw new Error('Ciphertext too short');
|
|
95
|
+
|
|
96
|
+
const headerLen = ciphertext.readUInt16BE(0);
|
|
97
|
+
if (ciphertext.length < 2 + headerLen) throw new Error('Invalid ciphertext format');
|
|
98
|
+
|
|
99
|
+
const headerBytes = ciphertext.subarray(2, 2 + headerLen);
|
|
100
|
+
const rawCiphertext = ciphertext.subarray(2 + headerLen);
|
|
101
|
+
const header = JSON.parse(headerBytes.toString('utf-8'));
|
|
102
|
+
|
|
103
|
+
return { header, rawCiphertext };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getKeyIdFromCiphertext(ciphertext) {
|
|
107
|
+
try {
|
|
108
|
+
return parseCiphertext(ciphertext).header.kid || null;
|
|
109
|
+
} catch { return null; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getContextFromCiphertext(ciphertext) {
|
|
113
|
+
try {
|
|
114
|
+
return parseCiphertext(ciphertext).header.ctx || null;
|
|
115
|
+
} catch { return null; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Password-based encryption (for CLI encrypt/decrypt commands)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const SCRYPT_N = 2 ** 15;
|
|
123
|
+
const SCRYPT_R = 8;
|
|
124
|
+
const SCRYPT_P = 1;
|
|
125
|
+
const SALT_SIZE = 16;
|
|
126
|
+
|
|
127
|
+
function deriveKeyFromPassword(password, salt, keySize = 32) {
|
|
128
|
+
return scryptSync(password, salt, keySize, {
|
|
129
|
+
N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 64 * 1024 * 1024,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function encryptString(text, password, algorithm = 'AES-256-GCM', context = 'cli') {
|
|
134
|
+
const salt = randomBytes(SALT_SIZE);
|
|
135
|
+
const spec = ALGORITHMS[algorithm];
|
|
136
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
137
|
+
|
|
138
|
+
const key = deriveKeyFromPassword(password, salt, spec.keySize);
|
|
139
|
+
const blob = encrypt(Buffer.from(text, 'utf-8'), key, 'password-derived', context, algorithm);
|
|
140
|
+
|
|
141
|
+
// Format: [16-byte salt][encrypted blob] → base64
|
|
142
|
+
return Buffer.concat([salt, blob]).toString('base64');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function decryptString(base64Text, password) {
|
|
146
|
+
const packed = Buffer.from(base64Text, 'base64');
|
|
147
|
+
if (packed.length < SALT_SIZE + 3) throw new Error('Invalid encrypted data');
|
|
148
|
+
|
|
149
|
+
const salt = packed.subarray(0, SALT_SIZE);
|
|
150
|
+
const blob = packed.subarray(SALT_SIZE);
|
|
151
|
+
|
|
152
|
+
// Parse header to get algorithm and determine key size
|
|
153
|
+
const { header } = parseCiphertext(blob);
|
|
154
|
+
const algorithm = header.alg || 'AES-256-GCM';
|
|
155
|
+
const spec = ALGORITHMS[algorithm];
|
|
156
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
157
|
+
|
|
158
|
+
const key = deriveKeyFromPassword(password, salt, spec.keySize);
|
|
159
|
+
return decrypt(blob, key).toString('utf-8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function encryptFile(inPath, outPath, password, algorithm = 'AES-256-GCM', context = 'file') {
|
|
163
|
+
const plaintext = readFileSync(inPath);
|
|
164
|
+
const salt = randomBytes(SALT_SIZE);
|
|
165
|
+
const spec = ALGORITHMS[algorithm];
|
|
166
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
167
|
+
|
|
168
|
+
const key = deriveKeyFromPassword(password, salt, spec.keySize);
|
|
169
|
+
const blob = encrypt(plaintext, key, 'password-derived', context, algorithm);
|
|
170
|
+
|
|
171
|
+
writeFileSync(outPath, Buffer.concat([salt, blob]), { mode: 0o600 });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function decryptFile(inPath, outPath, password) {
|
|
175
|
+
const packed = readFileSync(inPath);
|
|
176
|
+
if (packed.length < SALT_SIZE + 3) throw new Error('Invalid encrypted file');
|
|
177
|
+
|
|
178
|
+
const salt = packed.subarray(0, SALT_SIZE);
|
|
179
|
+
const blob = packed.subarray(SALT_SIZE);
|
|
180
|
+
|
|
181
|
+
const { header } = parseCiphertext(blob);
|
|
182
|
+
const algorithm = header.alg || 'AES-256-GCM';
|
|
183
|
+
const spec = ALGORITHMS[algorithm];
|
|
184
|
+
if (!spec) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
185
|
+
|
|
186
|
+
const key = deriveKeyFromPassword(password, salt, spec.keySize);
|
|
187
|
+
const plaintext = decrypt(blob, key);
|
|
188
|
+
|
|
189
|
+
writeFileSync(outPath, plaintext);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Password hashing
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
export function hashPassword(password, algorithm = 'scrypt') {
|
|
197
|
+
const salt = randomBytes(SALT_SIZE);
|
|
198
|
+
if (algorithm === 'scrypt') {
|
|
199
|
+
const hash = scryptSync(password, salt, 64, {
|
|
200
|
+
N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P, maxmem: 64 * 1024 * 1024,
|
|
201
|
+
});
|
|
202
|
+
return `$scrypt$N=${SCRYPT_N}$r=${SCRYPT_R}$p=${SCRYPT_P}$${salt.toString('base64')}$${hash.toString('base64')}`;
|
|
203
|
+
}
|
|
204
|
+
if (algorithm === 'pbkdf2') {
|
|
205
|
+
const iterations = 600000;
|
|
206
|
+
const hash = pbkdf2Sync(password, salt, iterations, 64, 'sha256');
|
|
207
|
+
return `$pbkdf2-sha256$${iterations}$${salt.toString('base64')}$${hash.toString('base64')}`;
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Utility: SHA-256 digest
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export function sha256(data) {
|
|
217
|
+
return createHash('sha256').update(data).digest();
|
|
218
|
+
}
|