@fgv/ts-extras 5.1.0-17 → 5.1.0-19
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/packlets/ai-assist/apiClient.js +247 -24
- package/dist/packlets/ai-assist/index.js +1 -1
- package/dist/packlets/ai-assist/registry.js +49 -4
- package/dist/packlets/crypto-utils/index.browser.js +2 -0
- package/dist/packlets/crypto-utils/index.js +2 -0
- package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +47 -0
- package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
- package/dist/packlets/crypto-utils/keystore/index.js +1 -0
- package/dist/packlets/crypto-utils/keystore/keyStore.js +271 -46
- package/dist/packlets/crypto-utils/keystore/model.js +22 -1
- package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
- package/dist/packlets/crypto-utils/model.js +5 -0
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +140 -1
- package/dist/test/unit/crypto/keystore/inMemoryPrivateKeyStorage.js +78 -0
- package/dist/ts-extras.d.ts +799 -40
- package/lib/packlets/ai-assist/apiClient.d.ts +11 -3
- package/lib/packlets/ai-assist/apiClient.js +245 -22
- package/lib/packlets/ai-assist/index.d.ts +2 -2
- package/lib/packlets/ai-assist/index.js +3 -1
- package/lib/packlets/ai-assist/model.d.ts +66 -5
- package/lib/packlets/ai-assist/registry.d.ts +25 -1
- package/lib/packlets/ai-assist/registry.js +51 -4
- package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.browser.js +4 -1
- package/lib/packlets/crypto-utils/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/index.js +4 -1
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +39 -0
- package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +50 -0
- package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
- package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
- package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
- package/lib/packlets/crypto-utils/keystore/index.js +1 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +77 -9
- package/lib/packlets/crypto-utils/keystore/keyStore.js +271 -46
- package/lib/packlets/crypto-utils/keystore/model.d.ts +238 -19
- package/lib/packlets/crypto-utils/keystore/model.js +24 -2
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
- package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
- package/lib/packlets/crypto-utils/model.d.ts +130 -0
- package/lib/packlets/crypto-utils/model.js +6 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +45 -1
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +139 -0
- package/package.json +7 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Result } from '@fgv/ts-utils';
|
|
2
|
-
import { ICryptoProvider, IEncryptionResult } from './model';
|
|
2
|
+
import { ICryptoProvider, IEncryptionResult, IWrapBytesOptions, IWrappedBytes, KeyPairAlgorithm } from './model';
|
|
3
3
|
/**
|
|
4
4
|
* Node.js implementation of {@link CryptoUtils.ICryptoProvider} using the built-in crypto module.
|
|
5
5
|
* Uses AES-256-GCM for authenticated encryption.
|
|
@@ -59,6 +59,50 @@ export declare class NodeCryptoProvider implements ICryptoProvider {
|
|
|
59
59
|
* @returns Success with decoded bytes, or Failure if invalid base64
|
|
60
60
|
*/
|
|
61
61
|
fromBase64(base64: string): Result<Uint8Array>;
|
|
62
|
+
/**
|
|
63
|
+
* Generates a new asymmetric keypair using Node's WebCrypto.
|
|
64
|
+
* @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm | algorithm} to use.
|
|
65
|
+
* @param extractable - Whether the resulting keys may be exported.
|
|
66
|
+
* @returns `Success` with the generated `CryptoKeyPair`, or `Failure` with an error.
|
|
67
|
+
*/
|
|
68
|
+
generateKeyPair(algorithm: KeyPairAlgorithm, extractable: boolean): Promise<Result<CryptoKeyPair>>;
|
|
69
|
+
/**
|
|
70
|
+
* Exports a public `CryptoKey` as a JSON Web Key.
|
|
71
|
+
* @remarks
|
|
72
|
+
* Rejects non-public keys at runtime. WebCrypto's `exportKey('jwk', ...)`
|
|
73
|
+
* does not enforce public-vs-private; without this guard a caller that
|
|
74
|
+
* passed an extractable private key would receive its private fields
|
|
75
|
+
* (`d`, `p`, `q`, ...) as JWK, defeating the method's name.
|
|
76
|
+
* @param publicKey - Extractable public key to export.
|
|
77
|
+
* @returns `Success` with the JWK, or `Failure` if not a public key or if export fails.
|
|
78
|
+
*/
|
|
79
|
+
exportPublicKeyJwk(publicKey: CryptoKey): Promise<Result<JsonWebKey>>;
|
|
80
|
+
/**
|
|
81
|
+
* Imports a public-key JWK as a `CryptoKey` for the requested algorithm.
|
|
82
|
+
* @param jwk - The JSON Web Key produced by a prior export.
|
|
83
|
+
* @param algorithm - The algorithm the key was generated for.
|
|
84
|
+
* @returns `Success` with the imported public `CryptoKey`, or `Failure` with an error.
|
|
85
|
+
*/
|
|
86
|
+
importPublicKeyJwk(jwk: JsonWebKey, algorithm: KeyPairAlgorithm): Promise<Result<CryptoKey>>;
|
|
87
|
+
/**
|
|
88
|
+
* Wraps `plaintext` for the holder of `recipientPublicKey` using
|
|
89
|
+
* ECIES (ECDH P-256 + HKDF-SHA256 + AES-GCM-256). See
|
|
90
|
+
* {@link CryptoUtils.ICryptoProvider.wrapBytes | ICryptoProvider.wrapBytes}.
|
|
91
|
+
* @param plaintext - The bytes to wrap.
|
|
92
|
+
* @param recipientPublicKey - The recipient's ECDH P-256 public `CryptoKey`.
|
|
93
|
+
* @param options - HKDF salt and info; see {@link CryptoUtils.IWrapBytesOptions | IWrapBytesOptions}.
|
|
94
|
+
* @returns `Success` with the wrapped payload, or `Failure` with an error.
|
|
95
|
+
*/
|
|
96
|
+
wrapBytes(plaintext: Uint8Array, recipientPublicKey: CryptoKey, options: IWrapBytesOptions): Promise<Result<IWrappedBytes>>;
|
|
97
|
+
/**
|
|
98
|
+
* Unwraps a payload produced by `wrapBytes` using the recipient's private
|
|
99
|
+
* key. See {@link CryptoUtils.ICryptoProvider.unwrapBytes | ICryptoProvider.unwrapBytes}.
|
|
100
|
+
* @param wrapped - The wrapped payload.
|
|
101
|
+
* @param recipientPrivateKey - The recipient's ECDH P-256 private `CryptoKey`.
|
|
102
|
+
* @param options - HKDF salt and info matching the wrap call.
|
|
103
|
+
* @returns `Success` with the original `plaintext`, or `Failure` with an error.
|
|
104
|
+
*/
|
|
105
|
+
unwrapBytes(wrapped: IWrappedBytes, recipientPrivateKey: CryptoKey, options: IWrapBytesOptions): Promise<Result<Uint8Array>>;
|
|
62
106
|
}
|
|
63
107
|
/**
|
|
64
108
|
* Singleton instance of {@link CryptoUtils.NodeCryptoProvider}.
|
|
@@ -56,6 +56,7 @@ exports.nodeCryptoProvider = exports.NodeCryptoProvider = void 0;
|
|
|
56
56
|
const crypto = __importStar(require("crypto"));
|
|
57
57
|
const ts_utils_1 = require("@fgv/ts-utils");
|
|
58
58
|
const Constants = __importStar(require("./constants"));
|
|
59
|
+
const keyPairAlgorithmParams_1 = require("./keyPairAlgorithmParams");
|
|
59
60
|
/**
|
|
60
61
|
* Node.js implementation of {@link CryptoUtils.ICryptoProvider} using the built-in crypto module.
|
|
61
62
|
* Uses AES-256-GCM for authenticated encryption.
|
|
@@ -198,8 +199,146 @@ class NodeCryptoProvider {
|
|
|
198
199
|
}
|
|
199
200
|
return ts_utils_1.Success.with(new Uint8Array(Buffer.from(base64, 'base64')));
|
|
200
201
|
}
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Asymmetric Key Operations
|
|
204
|
+
// ============================================================================
|
|
205
|
+
/**
|
|
206
|
+
* Generates a new asymmetric keypair using Node's WebCrypto.
|
|
207
|
+
* @param algorithm - The {@link CryptoUtils.KeyPairAlgorithm | algorithm} to use.
|
|
208
|
+
* @param extractable - Whether the resulting keys may be exported.
|
|
209
|
+
* @returns `Success` with the generated `CryptoKeyPair`, or `Failure` with an error.
|
|
210
|
+
*/
|
|
211
|
+
async generateKeyPair(algorithm, extractable) {
|
|
212
|
+
const params = keyPairAlgorithmParams_1.keyPairAlgorithmParams[algorithm];
|
|
213
|
+
const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.generateKey(params.generateKey, extractable, params.keyPairUsages));
|
|
214
|
+
return result.withErrorFormat((e) => `Failed to generate ${algorithm} keypair: ${e}`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Exports a public `CryptoKey` as a JSON Web Key.
|
|
218
|
+
* @remarks
|
|
219
|
+
* Rejects non-public keys at runtime. WebCrypto's `exportKey('jwk', ...)`
|
|
220
|
+
* does not enforce public-vs-private; without this guard a caller that
|
|
221
|
+
* passed an extractable private key would receive its private fields
|
|
222
|
+
* (`d`, `p`, `q`, ...) as JWK, defeating the method's name.
|
|
223
|
+
* @param publicKey - Extractable public key to export.
|
|
224
|
+
* @returns `Success` with the JWK, or `Failure` if not a public key or if export fails.
|
|
225
|
+
*/
|
|
226
|
+
async exportPublicKeyJwk(publicKey) {
|
|
227
|
+
if (publicKey.type !== 'public') {
|
|
228
|
+
return (0, ts_utils_1.fail)(`exportPublicKeyJwk requires a public CryptoKey, got '${publicKey.type}'`);
|
|
229
|
+
}
|
|
230
|
+
const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.exportKey('jwk', publicKey));
|
|
231
|
+
return result.withErrorFormat((e) => `Failed to export public key as JWK: ${e}`);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Imports a public-key JWK as a `CryptoKey` for the requested algorithm.
|
|
235
|
+
* @param jwk - The JSON Web Key produced by a prior export.
|
|
236
|
+
* @param algorithm - The algorithm the key was generated for.
|
|
237
|
+
* @returns `Success` with the imported public `CryptoKey`, or `Failure` with an error.
|
|
238
|
+
*/
|
|
239
|
+
async importPublicKeyJwk(jwk, algorithm) {
|
|
240
|
+
const params = keyPairAlgorithmParams_1.keyPairAlgorithmParams[algorithm];
|
|
241
|
+
const result = await (0, ts_utils_1.captureAsyncResult)(() => crypto.webcrypto.subtle.importKey('jwk', jwk, params.importPublicKey, true, params.publicKeyUsages));
|
|
242
|
+
return result.withErrorFormat((e) => `Failed to import ${algorithm} public key from JWK: ${e}`);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Wraps `plaintext` for the holder of `recipientPublicKey` using
|
|
246
|
+
* ECIES (ECDH P-256 + HKDF-SHA256 + AES-GCM-256). See
|
|
247
|
+
* {@link CryptoUtils.ICryptoProvider.wrapBytes | ICryptoProvider.wrapBytes}.
|
|
248
|
+
* @param plaintext - The bytes to wrap.
|
|
249
|
+
* @param recipientPublicKey - The recipient's ECDH P-256 public `CryptoKey`.
|
|
250
|
+
* @param options - HKDF salt and info; see {@link CryptoUtils.IWrapBytesOptions | IWrapBytesOptions}.
|
|
251
|
+
* @returns `Success` with the wrapped payload, or `Failure` with an error.
|
|
252
|
+
*/
|
|
253
|
+
async wrapBytes(plaintext, recipientPublicKey, options) {
|
|
254
|
+
const recipientCheck = checkEcdhP256(recipientPublicKey, 'public', 'recipient public key');
|
|
255
|
+
if (recipientCheck.isFailure()) {
|
|
256
|
+
return (0, ts_utils_1.fail)(`wrapBytes failed: ${recipientCheck.message}`);
|
|
257
|
+
}
|
|
258
|
+
const subtle = crypto.webcrypto.subtle;
|
|
259
|
+
const result = await (0, ts_utils_1.captureAsyncResult)(async () => {
|
|
260
|
+
const ephemeral = (await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [
|
|
261
|
+
'deriveKey'
|
|
262
|
+
]));
|
|
263
|
+
const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: recipientPublicKey }, ephemeral.privateKey, { name: 'HKDF' }, false, ['deriveKey']);
|
|
264
|
+
const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
265
|
+
const nonce = crypto.randomBytes(Constants.GCM_IV_SIZE);
|
|
266
|
+
const ctBuf = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, wrapKey, plaintext);
|
|
267
|
+
const ephemeralPublicKey = await subtle.exportKey('jwk', ephemeral.publicKey);
|
|
268
|
+
return {
|
|
269
|
+
ephemeralPublicKey,
|
|
270
|
+
nonce: this.toBase64(nonce),
|
|
271
|
+
ciphertext: this.toBase64(new Uint8Array(ctBuf))
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
return result.withErrorFormat((e) => `wrapBytes failed: ${e}`);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Unwraps a payload produced by `wrapBytes` using the recipient's private
|
|
278
|
+
* key. See {@link CryptoUtils.ICryptoProvider.unwrapBytes | ICryptoProvider.unwrapBytes}.
|
|
279
|
+
* @param wrapped - The wrapped payload.
|
|
280
|
+
* @param recipientPrivateKey - The recipient's ECDH P-256 private `CryptoKey`.
|
|
281
|
+
* @param options - HKDF salt and info matching the wrap call.
|
|
282
|
+
* @returns `Success` with the original `plaintext`, or `Failure` with an error.
|
|
283
|
+
*/
|
|
284
|
+
async unwrapBytes(wrapped, recipientPrivateKey, options) {
|
|
285
|
+
const recipientCheck = checkEcdhP256(recipientPrivateKey, 'private', 'recipient private key');
|
|
286
|
+
if (recipientCheck.isFailure()) {
|
|
287
|
+
return (0, ts_utils_1.fail)(`unwrapBytes failed: ${recipientCheck.message}`);
|
|
288
|
+
}
|
|
289
|
+
const nonceResult = this.fromBase64(wrapped.nonce);
|
|
290
|
+
if (nonceResult.isFailure()) {
|
|
291
|
+
return (0, ts_utils_1.fail)(`unwrapBytes failed: nonce: ${nonceResult.message}`);
|
|
292
|
+
}
|
|
293
|
+
if (nonceResult.value.length !== Constants.GCM_IV_SIZE) {
|
|
294
|
+
return (0, ts_utils_1.fail)(`unwrapBytes failed: nonce must be ${Constants.GCM_IV_SIZE} bytes (got ${nonceResult.value.length})`);
|
|
295
|
+
}
|
|
296
|
+
const ciphertextResult = this.fromBase64(wrapped.ciphertext);
|
|
297
|
+
if (ciphertextResult.isFailure()) {
|
|
298
|
+
return (0, ts_utils_1.fail)(`unwrapBytes failed: ciphertext: ${ciphertextResult.message}`);
|
|
299
|
+
}
|
|
300
|
+
if (ciphertextResult.value.length < Constants.GCM_AUTH_TAG_SIZE) {
|
|
301
|
+
return (0, ts_utils_1.fail)(`unwrapBytes failed: ciphertext must be at least ${Constants.GCM_AUTH_TAG_SIZE} bytes (got ${ciphertextResult.value.length})`);
|
|
302
|
+
}
|
|
303
|
+
const subtle = crypto.webcrypto.subtle;
|
|
304
|
+
const result = await (0, ts_utils_1.captureAsyncResult)(async () => {
|
|
305
|
+
const ephemeralPub = await subtle.importKey('jwk', wrapped.ephemeralPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
306
|
+
const hkdfBase = await subtle.deriveKey({ name: 'ECDH', public: ephemeralPub }, recipientPrivateKey, { name: 'HKDF' }, false, ['deriveKey']);
|
|
307
|
+
const wrapKey = await subtle.deriveKey({ name: 'HKDF', salt: options.salt, info: options.info, hash: 'SHA-256' }, hkdfBase, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
|
|
308
|
+
const ptBuf = await subtle.decrypt({ name: 'AES-GCM', iv: nonceResult.value }, wrapKey, ciphertextResult.value);
|
|
309
|
+
return new Uint8Array(ptBuf);
|
|
310
|
+
});
|
|
311
|
+
return result.withErrorFormat((e) => `unwrapBytes failed: ${e}`);
|
|
312
|
+
}
|
|
201
313
|
}
|
|
202
314
|
exports.NodeCryptoProvider = NodeCryptoProvider;
|
|
315
|
+
/**
|
|
316
|
+
* Verifies that `key` is an ECDH P-256 `CryptoKey` of the expected `keyType`
|
|
317
|
+
* (public or private). Used by the wrap/unwrap methods to surface a clean
|
|
318
|
+
* `Failure` instead of letting the WebCrypto deriveKey call throw a less
|
|
319
|
+
* informative error later in the pipeline. Key usages are intentionally not
|
|
320
|
+
* checked here: WebCrypto already produces a specific error if `deriveKey` is
|
|
321
|
+
* not in `usages`, and `deriveBits` is an equally valid alternative usage that
|
|
322
|
+
* an explicit check would have to track.
|
|
323
|
+
* @param key - The CryptoKey to validate.
|
|
324
|
+
* @param keyType - The required `key.type` ('public' for wrap, 'private' for unwrap).
|
|
325
|
+
* @param label - Human-readable role label included in the failure message.
|
|
326
|
+
* @returns `Success` with the key (unchanged) when the algorithm, curve, and
|
|
327
|
+
* type all match; otherwise `Failure` with `<label> must be ECDH P-256 (...)`.
|
|
328
|
+
*/
|
|
329
|
+
function checkEcdhP256(key, keyType, label) {
|
|
330
|
+
if (key.algorithm.name !== 'ECDH') {
|
|
331
|
+
return (0, ts_utils_1.fail)(`${label} must be ECDH P-256 (got algorithm '${key.algorithm.name}')`);
|
|
332
|
+
}
|
|
333
|
+
const namedCurve = key.algorithm.namedCurve;
|
|
334
|
+
if (namedCurve !== 'P-256') {
|
|
335
|
+
return (0, ts_utils_1.fail)(`${label} must be ECDH P-256 (got curve '${namedCurve}')`);
|
|
336
|
+
}
|
|
337
|
+
if (key.type !== keyType) {
|
|
338
|
+
return (0, ts_utils_1.fail)(`${label} must be a ${keyType} CryptoKey (got '${key.type}')`);
|
|
339
|
+
}
|
|
340
|
+
return (0, ts_utils_1.succeed)(key);
|
|
341
|
+
}
|
|
203
342
|
/**
|
|
204
343
|
* Singleton instance of {@link CryptoUtils.NodeCryptoProvider}.
|
|
205
344
|
* @public
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fgv/ts-extras",
|
|
3
|
-
"version": "5.1.0-
|
|
3
|
+
"version": "5.1.0-19",
|
|
4
4
|
"description": "Assorted Typescript Utilities",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "dist/ts-extras.d.ts",
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"@types/js-yaml": "~4.0.9",
|
|
87
87
|
"typedoc": "~0.28.16",
|
|
88
88
|
"typedoc-plugin-markdown": "~4.9.0",
|
|
89
|
-
"@fgv/
|
|
90
|
-
"@fgv/typedoc-compact-theme": "5.1.0-
|
|
91
|
-
"@fgv/ts-utils": "5.1.0-
|
|
92
|
-
"@fgv/
|
|
89
|
+
"@fgv/heft-dual-rig": "5.1.0-19",
|
|
90
|
+
"@fgv/typedoc-compact-theme": "5.1.0-19",
|
|
91
|
+
"@fgv/ts-utils": "5.1.0-19",
|
|
92
|
+
"@fgv/ts-utils-jest": "5.1.0-19"
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"luxon": "^3.7.2",
|
|
@@ -97,10 +97,10 @@
|
|
|
97
97
|
"papaparse": "^5.4.1",
|
|
98
98
|
"fflate": "~0.8.2",
|
|
99
99
|
"js-yaml": "~4.1.1",
|
|
100
|
-
"@fgv/ts-json-base": "5.1.0-
|
|
100
|
+
"@fgv/ts-json-base": "5.1.0-19"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
|
-
"@fgv/ts-utils": "5.1.0-
|
|
103
|
+
"@fgv/ts-utils": "5.1.0-19"
|
|
104
104
|
},
|
|
105
105
|
"repository": {
|
|
106
106
|
"type": "git",
|