Skip to content

Commit 050b394

Browse files
committed
Begin working on createPrivateKey
1 parent b1546c4 commit 050b394

36 files changed

+1114
-157
lines changed

.github/secret_scanning.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,28 @@ paths-ignore:
33
- "src/workerd/api/node/crypto_dh-test.js"
44
- "src/workerd/jsg/url-test-corpus-success.h"
55
- "src/workerd/api/node/tests/crypto_x509-test.js"
6+
- "src/workerd/api/node/tests/fixtures/dh_private.pem"
7+
- "src/workerd/api/node/tests/fixtures/dsa_private_pkcs8.pem"
8+
- "src/workerd/api/node/tests/fixtures/ed25519_private.pem"
9+
- "src/workerd/api/node/tests/fixtures/rsa_private_encrypted.pem"
10+
- "src/workerd/api/node/tests/fixtures/rsa_pss_private_2048_sha1_sha1_20.pem"
11+
- "src/workerd/api/node/tests/fixtures/dsa_private_1025.pem"
12+
- "src/workerd/api/node/tests/fixtures/ec_p256_private.pem"
13+
- "src/workerd/api/node/tests/fixtures/ed448_private.pem"
14+
- "src/workerd/api/node/tests/fixtures/rsa_private.pem"
15+
- "src/workerd/api/node/tests/fixtures/rsa_pss_private_2048_sha256_sha256_16.pem"
16+
- "src/workerd/api/node/tests/fixtures/dsa_private_encrypted_1025.pem"
17+
- "src/workerd/api/node/tests/fixtures/ec_p384_private.pem"
18+
- "src/workerd/api/node/tests/fixtures/rsa_private_2048.pem"
19+
- "src/workerd/api/node/tests/fixtures/rsa_private_pkcs8_bad.pem"
20+
- "src/workerd/api/node/tests/fixtures/rsa_pss_private_2048_sha512_sha256_20.pem"
21+
- "src/workerd/api/node/tests/fixtures/dsa_private_encrypted.pem"
22+
- "src/workerd/api/node/tests/fixtures/ec_p521_private.pem"
23+
- "src/workerd/api/node/tests/fixtures/rsa_private_4096.pem"
24+
- "src/workerd/api/node/tests/fixtures/rsa_private_pkcs8.pem"
25+
- "src/workerd/api/node/tests/fixtures/x25519_private.pem"
26+
- "src/workerd/api/node/tests/fixtures/dsa_private.pem"
27+
- "src/workerd/api/node/tests/fixtures/ec_secp256k1_private.pem"
28+
- "src/workerd/api/node/tests/fixtures/rsa_private_b.pem"
29+
- "src/workerd/api/node/tests/fixtures/rsa_pss_private_2048.pem"
30+
- "src/workerd/api/node/tests/fixtures/x448_private.pem"

WORKSPACE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ http_archive(
8282

8383
http_archive(
8484
name = "ncrypto",
85-
sha256 = "d0e4eadf1947cfa842012ba1f25d15946984af76549395f6c646c1b6d8aced9c",
85+
sha256 = "fe8998fff6624df31437e66203070d06878a124f79174ce531ed323fcc6f4a24",
8686
strip_prefix = "ncrypto-initial-impl",
8787
type = "tgz",
8888
url = "https://github.com/nodejs/ncrypto/archive/refs/heads/initial-impl.tar.gz",

src/node/internal/crypto.d.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,25 @@ export interface AsymmetricKeyDetails {
261261
namedCurve?: string;
262262
}
263263

264+
// The user-provided options passed to createPrivateKey or createPublicKey.
265+
// This will be processed into an InnerCreateAsymmetricKeyOptions.
264266
export interface CreateAsymmetricKeyOptions {
265267
key: string | ArrayBuffer | ArrayBufferView | JsonWebKey;
266268
format?: AsymmetricKeyFormat;
267269
type?: PublicKeyEncoding | PrivateKeyEncoding;
268-
passphrase?: string | Uint8Array;
270+
passphrase?: string | Uint8Array | Buffer;
269271
encoding?: string;
270272
}
271273

274+
// The processed key options. The key property will be one of either
275+
// an ArrayBuffer, an ArrayBufferView, a JWK, or a CryptoKey. The
276+
// format and type options will be validated to known good values,
277+
// and the passphrase will either be undefined or an ArrayBufferView.
272278
export interface InnerCreateAsymmetricKeyOptions {
273-
key?: ArrayBuffer | ArrayBufferView | JsonWebKey | CryptoKey;
274-
format?: AsymmetricKeyFormat;
275-
type?: PublicKeyEncoding | PrivateKeyEncoding;
276-
passphrase?: Uint8Array;
279+
key: ArrayBuffer | ArrayBufferView | JsonWebKey;
280+
format: AsymmetricKeyFormat;
281+
type: PublicKeyEncoding | PrivateKeyEncoding | undefined;
282+
passphrase: Buffer | ArrayBuffer | ArrayBufferView | undefined;
277283
}
278284

279285
export interface GenerateKeyOptions {

src/node/internal/crypto_keys.ts

Lines changed: 140 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,31 @@
2929
import { Buffer } from 'node-internal:internal_buffer';
3030

3131
import {
32-
KeyData,
33-
KeyObjectType,
34-
KeyExportResult,
35-
SecretKeyType,
36-
SecretKeyExportOptions,
37-
PublicKeyExportOptions,
38-
PrivateKeyExportOptions,
39-
ExportOptions,
40-
AsymmetricKeyDetails,
41-
AsymmetricKeyType,
42-
CreateAsymmetricKeyOptions,
43-
GenerateKeyOptions,
44-
GenerateKeyPairOptions,
45-
InnerExportOptions,
46-
// TODO(soon): Uncomment these once createPrivateKey/createPublicKey are implemented.
47-
// JsonWebKey,
48-
// InnerCreateAsymmetricKeyOptions,
32+
type KeyData,
33+
type KeyObjectType,
34+
type KeyExportResult,
35+
type SecretKeyType,
36+
type SecretKeyExportOptions,
37+
type PublicKeyExportOptions,
38+
type PrivateKeyExportOptions,
39+
type ExportOptions,
40+
type AsymmetricKeyDetails,
41+
type AsymmetricKeyType,
42+
type CreateAsymmetricKeyOptions,
43+
type GenerateKeyOptions,
44+
type GenerateKeyPairOptions,
45+
type InnerExportOptions,
46+
type InnerCreateAsymmetricKeyOptions,
47+
// type AsymmetricKeyFormat,
48+
// type PrivateKeyEncoding,
49+
// type PublicKeyEncoding,
50+
type JsonWebKey,
4951
default as cryptoImpl,
5052
} from 'node-internal:crypto';
5153

5254
import {
5355
arrayBufferToUnsignedBigInt,
56+
getArrayBufferOrView,
5457
kHandle,
5558
} from 'node-internal:crypto_util';
5659

@@ -64,13 +67,26 @@ import {
6467
} from 'node-internal:internal_types';
6568

6669
import {
70+
// ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
71+
// ERR_CRYPTO_INVALID_JWK,
6772
ERR_INVALID_ARG_TYPE,
68-
ERR_METHOD_NOT_IMPLEMENTED,
69-
// TODO(soon): Uncomment these once createPrivateKey/createPublicKey are implemented.
7073
// ERR_INVALID_ARG_VALUE,
74+
ERR_METHOD_NOT_IMPLEMENTED,
7175
} from 'node-internal:internal_errors';
7276

73-
import { validateObject, validateString } from 'node-internal:validators';
77+
import {
78+
validateObject,
79+
validateOneOf,
80+
validateString,
81+
} from 'node-internal:validators';
82+
83+
// Key input contexts.
84+
enum KeyContext {
85+
kConsumePublic,
86+
kConsumePrivate,
87+
kCreatePublic,
88+
kCreatePrivate,
89+
}
7490

7591
// In Node.js, the definition of KeyObject is a bit complicated because
7692
// KeyObject instances in Node.js can be transferred via postMessage() and
@@ -81,6 +97,14 @@ import { validateObject, validateString } from 'node-internal:validators';
8197
// existed first. We're, however, going to layer our KeyObject on top of
8298
// CryptoKey with a few augmentations.
8399

100+
let isKeyObject: (obj: any) => boolean;
101+
102+
function isStringOrBuffer(val: any) {
103+
return (
104+
typeof val === 'string' || isArrayBufferView(val) || isAnyArrayBuffer(val)
105+
);
106+
}
107+
84108
export abstract class KeyObject {
85109
[kHandle]: CryptoKey;
86110

@@ -191,6 +215,12 @@ export abstract class KeyObject {
191215
get [Symbol.toStringTag]() {
192216
return 'KeyObject';
193217
}
218+
219+
static {
220+
isKeyObject = function (obj: any): obj is KeyObject {
221+
return obj[kHandle] !== undefined;
222+
};
223+
}
194224
}
195225

196226
abstract class AsymmetricKeyObject extends KeyObject {
@@ -300,97 +330,91 @@ export function createSecretKey(
300330
return KeyObject.from(cryptoImpl.createSecretKey(key)) as SecretKeyObject;
301331
}
302332

303-
// TODO(soon): Fully implement createPrivateKey/createPublicKey. These are the
304-
// equivalent of the WebCrypto API's importKey() method but operate synchronously
305-
// and support a range of options not currently supported by WebCrypto. Implementing
306-
// these will require either duplicating or significantly refactoring the current
307-
// import key logic that supports Web Crypto now as the import logic is spread out
308-
// over several locations and makes a number of assumptions that Web Crypto is being
309-
// used.
310-
//
311-
// For now, users can use Web Crypto to import a CryptoKey then convert that into
312-
// a KeyObject using KeyObject.from().
313-
//
314-
// const kPrivateKey = Symbol('privateKey');
315-
// const kPublicKey = Symbol('publicKey');
316-
317-
// function validateAsymmetricKeyOptions(
318-
// key: CreateAsymmetricKeyOptions | KeyData | CryptoKey | KeyObject,
319-
// type: Symbol) {
320-
// validateKeyData(key, 'key', { allowObject: true });
321-
// let inner : InnerCreateAsymmetricKeyOptions = {};
322-
// inner.format = 'pem';
323-
// if (typeof key === 'string') {
324-
// inner.key = Buffer.from(key as string);
325-
// } else if (isArrayBufferView(key)) {
326-
// inner.key = key as ArrayBufferView;
327-
// } else if (isArrayBuffer(key)) {
328-
// inner.key = key as ArrayBuffer;
329-
// } else if (isSharedArrayBuffer(key)) {
330-
// inner.key = key as SharedArrayBuffer;
331-
// } else if (type === kPublicKey && key instanceof KeyObject) {
332-
// // Covers deriving public key from a private key.
333-
// if (key.type !== 'private') {
334-
// throw new ERR_INVALID_ARG_VALUE('key', key, 'must be a private key');
335-
// }
336-
// inner.key = (key as KeyObject)[kHandle];
337-
// } else if (type === kPublicKey && key instanceof CryptoKey) {
338-
// // Covers deriving public key from a private key.
339-
// if ((key as CryptoKey).type !== 'private') {
340-
// throw new ERR_INVALID_ARG_VALUE('key', key, 'must be a private key');
341-
// }
342-
// inner.key = key as CryptoKey;
343-
// } else {
344-
// const options = key as CreateAsymmetricKeyOptions;
345-
// if (typeof options.key === 'string') {
346-
// inner.key = Buffer.from(options.key as string, options.encoding);
347-
// } else if (isArrayBufferView(options.key)) {
348-
// inner.key = options.key as ArrayBufferView;
349-
// } else if (isArrayBuffer(options.key)) {
350-
// inner.key = options.key as ArrayBuffer;
351-
// } else if (isSharedArrayBuffer(options.key)) {
352-
// inner.key = options.key as SharedArrayBuffer;
353-
// } else if (type === kPublicKey && key instanceof KeyObject) {
354-
// if ((options.key as KeyObject).type !== 'private') {
355-
// throw new ERR_INVALID_ARG_VALUE('options.key', options.key, 'must be a private key');
356-
// }
357-
// inner.key = (options.key as KeyObject)[kHandle];
358-
// } else if (type === kPublicKey && key instanceof CryptoKey) {
359-
// if ((options.key as CryptoKey).type !== 'private') {
360-
// throw new ERR_INVALID_ARG_VALUE('options.key', options.key, 'must be a private key');
361-
// }
362-
// inner.key = options.key as CryptoKey;
363-
// } else {
364-
// inner.key = key as JsonWebKey;
365-
// }
366-
// validateKeyData(inner.key, 'options.key', { allowObject: true });
367-
368-
// if (options.format !== undefined) {
369-
// validateString(options.format, 'options.format');
370-
// inner.format = options.format;
371-
// }
372-
// if (options.type !== undefined) {
373-
// validateString(options.type, 'options.type');
374-
// inner.type = options.type;
375-
// }
376-
// if (options.passphrase !== undefined) {
377-
// if (typeof options.passphrase === 'string') {
378-
// inner.passphrase = Buffer.from(options.passphrase, options.encoding);
379-
// } else {
380-
// if (!isUint8Array(options.passphrase)) {
381-
// throw new ERR_INVALID_ARG_TYPE('options.passphrase', [
382-
// 'string', 'Uint8Array'
383-
// ], options.passphrase);
384-
// }
385-
// inner.passphrase = options.passphrase;
386-
// }
387-
// if (inner.passphrase.byteLength > 1024) {
388-
// throw new ERR_INVALID_ARG_VALUE('options.passphrase', options.passphrase.length, '<= 1024');
389-
// }
390-
// }
391-
// }
392-
// return inner;
393-
// }
333+
function prepareAsymmetricKey(
334+
key: CreateAsymmetricKeyOptions | KeyData,
335+
ctx: KeyContext
336+
): InnerCreateAsymmetricKeyOptions {
337+
// Safety check... key should not be undefined or null here.
338+
if ((key as any) == null) {
339+
throw new ERR_INVALID_ARG_TYPE(
340+
'key',
341+
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'string', 'object'],
342+
key
343+
);
344+
}
345+
346+
let normalized: CreateAsymmetricKeyOptions;
347+
if (
348+
isStringOrBuffer(key) ||
349+
isAnyArrayBuffer(key) ||
350+
isArrayBufferView(key)
351+
) {
352+
normalized = { key, format: 'pem' } as CreateAsymmetricKeyOptions;
353+
} else {
354+
normalized = key as CreateAsymmetricKeyOptions;
355+
}
356+
357+
const {
358+
key: data,
359+
encoding = 'utf8',
360+
format = 'pem',
361+
type,
362+
passphrase,
363+
} = normalized;
364+
365+
// The key data must be specified. The value has to be one of either a
366+
// string, an ArrayBuffer, an ArrayBufferView, or a JWK object.
367+
if ((data as any) == null || isKeyObject(data) || data instanceof CryptoKey) {
368+
throw new ERR_INVALID_ARG_TYPE(
369+
'options.key',
370+
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'string', 'object'],
371+
data
372+
);
373+
}
374+
375+
if (isStringOrBuffer(data)) {
376+
// When the key data is a string or buffer, the format must be
377+
// one of either pem or der.
378+
validateOneOf(format, 'format', ['pem', 'der']);
379+
if (type !== undefined) {
380+
if (ctx == KeyContext.kCreatePrivate) {
381+
// When the key data is a string or buffer, the type must be
382+
// one of either pkcs1, pkcs8, or sec1.
383+
validateOneOf(type, 'type', ['pkcs1', 'pkcs8', 'sec1']);
384+
} else if (ctx == KeyContext.kCreatePublic) {
385+
validateOneOf(type, 'type', ['pkcs1', 'spki']);
386+
}
387+
}
388+
return {
389+
key: getArrayBufferOrView(data, 'key', encoding),
390+
format,
391+
type,
392+
passphrase:
393+
passphrase != null
394+
? getArrayBufferOrView(passphrase, 'passphrase', encoding)
395+
: undefined,
396+
};
397+
}
398+
399+
// Final type check. The key data at this point has to be an object that
400+
// we will interpret as a JWK.
401+
if (typeof data !== 'object') {
402+
throw new ERR_INVALID_ARG_TYPE(
403+
'key',
404+
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', 'string', 'object'],
405+
key
406+
);
407+
}
408+
409+
// At this point we ignore all remaining options and assume the key is a
410+
// JSON Web Key.
411+
return {
412+
key: data as JsonWebKey,
413+
format: 'jwk',
414+
type: undefined,
415+
passphrase: undefined,
416+
};
417+
}
394418

395419
export function createPrivateKey(key: string): PrivateKeyObject;
396420
export function createPrivateKey(
@@ -400,16 +424,12 @@ export function createPrivateKey(
400424
key: CreateAsymmetricKeyOptions
401425
): PrivateKeyObject;
402426
export function createPrivateKey(
403-
_key: CreateAsymmetricKeyOptions | KeyData
427+
key: CreateAsymmetricKeyOptions | KeyData
404428
): PrivateKeyObject {
405-
// The options here are fairly complex. The key data can be a string,
406-
// ArrayBuffer, or ArrayBufferView. The first argument can be one of
407-
// these or an object with a key property that is one of these. If the
408-
// key data is a string, then it will be decoded using an encoding
409-
// (defaults to UTF8).
410-
throw new ERR_METHOD_NOT_IMPLEMENTED('crypto.createPrivateKey');
411-
// return KeyObject.from(cryptoImpl.createPrivateKey(
412-
// validateAsymmetricKeyOptions(key, kPrivateKey))) as PrivateKeyObject;
429+
const cryptoKey = cryptoImpl.createPrivateKey(
430+
prepareAsymmetricKey(key, KeyContext.kCreatePrivate)
431+
);
432+
return KeyObject.from(cryptoKey) as PrivateKeyObject;
413433
}
414434

415435
export function createPublicKey(key: string): PublicKeyObject;
@@ -424,14 +444,6 @@ export function createPublicKey(
424444
export function createPublicKey(
425445
_key: CreateAsymmetricKeyOptions | KeyData | CryptoKey | KeyObject
426446
): PublicKeyObject {
427-
// The options here are a bit complicated. The key material itself can
428-
// either be a string, ArrayBuffer, or ArrayBufferView. It is also
429-
// possible to pass a private key in the form of either a CryptoKey
430-
// or KeyObject. The first argument can be one of these, or an object
431-
// whose key value is one of these. If the key data is a string, then
432-
// it will be decoded using an encoding (defaults to UTF8). If a
433-
// CryptoKey or KeyObject is passed, it will be used to derived the
434-
// public key.
435447
throw new ERR_METHOD_NOT_IMPLEMENTED('crypto.createPublicKey');
436448
// return KeyObject.from(cryptoImpl.createPublicKey(
437449
// validateAsymmetricKeyOptions(key, kPublicKey))) as PublicKeyObject;

0 commit comments

Comments
 (0)