react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
798 lines (694 loc) • 20.8 kB
text/typescript
import {
type BinaryLike,
binaryLikeToArrayBuffer,
isStringOrBuffer,
type BufferLike,
type TypedArray,
} from './Utils';
import type { KeyObjectHandle } from './NativeQuickCrypto/webcrypto';
import { NativeQuickCrypto } from './NativeQuickCrypto/NativeQuickCrypto';
import type { KeyPairKey } from './Cipher';
export const kNamedCurveAliases = {
'P-256': 'prime256v1',
'P-384': 'secp384r1',
'P-521': 'secp521r1',
} as const;
export type NamedCurve = 'P-256' | 'P-384' | 'P-521';
export type ImportFormat = 'raw' | 'pkcs8' | 'spki' | 'jwk';
export type AnyAlgorithm =
| DigestAlgorithm
| HashAlgorithm
| KeyPairAlgorithm
| SecretKeyAlgorithm
| SignVerifyAlgorithm
| DeriveBitsAlgorithm
| EncryptDecryptAlgorithm
| AESAlgorithm
| 'PBKDF2'
| 'HKDF'
| 'unknown';
export type DigestAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
export type HashAlgorithm = DigestAlgorithm | 'SHA-224' | 'RIPEMD-160';
export type KeyPairType = 'rsa' | 'rsa-pss' | 'ec';
export type RSAKeyPairAlgorithm = 'RSASSA-PKCS1-v1_5' | 'RSA-PSS' | 'RSA-OAEP';
export type ECKeyPairAlgorithm = 'ECDSA' | 'ECDH';
export type CFRGKeyPairAlgorithm = 'Ed25519' | 'Ed448' | 'X25519' | 'X448';
export type AESAlgorithm = 'AES-CTR' | 'AES-CBC' | 'AES-GCM' | 'AES-KW';
export type KeyPairAlgorithm =
| RSAKeyPairAlgorithm
| ECKeyPairAlgorithm
| CFRGKeyPairAlgorithm;
export type SecretKeyAlgorithm = 'HMAC' | AESAlgorithm;
export type SecretKeyType = 'hmac' | 'aes';
export type SignVerifyAlgorithm =
| 'RSASSA-PKCS1-v1_5'
| 'RSA-PSS'
| 'ECDSA'
| 'HMAC'
| 'Ed25519'
| 'Ed448';
export type DeriveBitsAlgorithm =
| 'PBKDF2'
| 'HKDF'
| 'ECDH'
| 'X25519'
| 'X448';
export type RsaOaepParams = {
name: 'RSA-OAEP';
label?: BufferLike;
};
export type AesCbcParams = {
name: 'AES-CBC';
iv: BufferLike;
};
export type AesCtrParams = {
name: 'AES-CTR';
counter: TypedArray;
length: number;
};
export type AesGcmParams = {
name: 'AES-GCM';
iv: BufferLike;
tagLength?: TagLength;
additionalData?: BufferLike;
};
export type AesKwParams = {
name: 'AES-KW';
wrappingKey?: BufferLike;
};
export type AesKeyGenParams = {
length: AESLength;
name?: AESAlgorithm;
};
export type TagLength = 32 | 64 | 96 | 104 | 112 | 120 | 128;
export type AESLength = 128 | 192 | 256;
export type EncryptDecryptParams =
| AesCbcParams
| AesCtrParams
| AesGcmParams
| RsaOaepParams;
export type EncryptDecryptAlgorithm =
| 'RSA-OAEP'
| 'AES-CTR'
| 'AES-CBC'
| 'AES-GCM';
export type SubtleAlgorithm = {
name: AnyAlgorithm;
salt?: string;
iterations?: number;
hash?: HashAlgorithm | HashAlgorithmIdentifier;
namedCurve?: NamedCurve;
length?: number;
modulusLength?: number;
publicExponent?: number | Uint8Array;
};
export type HashAlgorithmIdentifier = {
name: HashAlgorithm;
};
export type KeyUsage =
| 'encrypt'
| 'decrypt'
| 'sign'
| 'verify'
| 'deriveKey'
| 'deriveBits'
| 'wrapKey'
| 'unwrapKey';
// On node this value is defined on the native side, for now I'm just creating it here in JS
// TODO(osp) move this into native side to make sure they always match
export enum KFormatType {
kKeyFormatDER,
kKeyFormatPEM,
kKeyFormatJWK,
}
export type KFormat = 'der' | 'pem' | 'jwk';
// Same as KFormatType, this enum needs to be defined on the native side
export enum KeyType {
Secret,
Public,
Private,
}
export type KTypePrivate = 'pkcs1' | 'pkcs8' | 'sec1';
export type KTypePublic = 'pkcs1' | 'spki';
export type KType = KTypePrivate | KTypePublic;
// Same as KFormatType, this enum needs to be defined on the native side
export enum KWebCryptoKeyFormat {
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatSPKI,
kWebCryptoKeyFormatJWK,
}
export enum WebCryptoKeyExportStatus {
OK,
INVALID_KEY_TYPE,
FAILED,
}
enum KeyInputContext {
kConsumePublic,
kConsumePrivate,
kCreatePublic,
kCreatePrivate,
}
export enum KeyEncoding {
kKeyEncodingPKCS1,
kKeyEncodingPKCS8,
kKeyEncodingSPKI,
kKeyEncodingSEC1,
}
export type DSAEncoding = 'der' | 'ieee-p1363';
export type EncodingOptions = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
key?: any;
type?: KType;
encoding?: string;
dsaEncoding?: DSAEncoding;
format?: KFormat;
padding?: number;
cipher?: string;
passphrase?: BinaryLike;
saltLength?: number;
oaepHash?: string;
oaepLabel?: BinaryLike;
};
export type AsymmetricKeyType = 'rsa' | 'rsa-pss' | 'dsa' | 'ec' | undefined;
export type JWK = {
kty?: 'AES' | 'RSA' | 'EC' | 'oct';
use?: 'sig' | 'enc';
key_ops?: KeyUsage[];
alg?: string; // TODO: enumerate these (RFC-7517)
crv?: string;
kid?: string;
x5u?: string;
x5c?: string[];
x5t?: string;
'x5t#256'?: string;
n?: string;
e?: string;
d?: string;
p?: string;
q?: string;
x?: string;
y?: string;
k?: string;
dp?: string;
dq?: string;
qi?: string;
ext?: boolean;
};
const encodingNames = {
[KeyEncoding.kKeyEncodingPKCS1]: 'pkcs1',
[KeyEncoding.kKeyEncodingPKCS8]: 'pkcs8',
[KeyEncoding.kKeyEncodingSPKI]: 'spki',
[KeyEncoding.kKeyEncodingSEC1]: 'sec1',
};
export type CryptoKeyPair = {
publicKey: KeyPairKey;
privateKey: KeyPairKey;
};
export enum CipherOrWrapMode {
kWebCryptoCipherEncrypt,
kWebCryptoCipherDecrypt,
// kWebCryptoWrapKey,
// kWebCryptoUnwrapKey,
}
function option(name: string, objName: string | undefined) {
return objName === undefined
? `options.${name}`
: `options.${objName}.${name}`;
}
function parseKeyFormat(
formatStr: string | undefined,
defaultFormat: KFormatType | undefined,
optionName?: string,
) {
if (formatStr === undefined && defaultFormat !== undefined)
return defaultFormat;
else if (formatStr === 'pem') return KFormatType.kKeyFormatPEM;
else if (formatStr === 'der') return KFormatType.kKeyFormatDER;
else if (formatStr === 'jwk') return KFormatType.kKeyFormatJWK;
throw new Error(`Invalid key format str: ${optionName}`);
// throw new ERR_INVALID_ARG_VALUE(optionName, formatStr);
}
function parseKeyType(
typeStr: string | undefined,
required: boolean,
keyType: string | undefined,
isPublic: boolean | undefined,
optionName: string,
): KeyEncoding | undefined {
if (typeStr === undefined && !required) {
return undefined;
} else if (typeStr === 'pkcs1') {
if (keyType !== undefined && keyType !== 'rsa') {
throw new Error(
`Crypto incompatible key options: ${typeStr} can only be used for RSA keys`,
);
}
return KeyEncoding.kKeyEncodingPKCS1;
} else if (typeStr === 'spki' && isPublic !== false) {
return KeyEncoding.kKeyEncodingSPKI;
} else if (typeStr === 'pkcs8' && isPublic !== true) {
return KeyEncoding.kKeyEncodingPKCS8;
} else if (typeStr === 'sec1' && isPublic !== true) {
if (keyType !== undefined && keyType !== 'ec') {
throw new Error(
`Incompatible key options ${typeStr} can only be used for EC keys`,
);
}
return KeyEncoding.kKeyEncodingSEC1;
}
throw new Error(`Invalid option ${optionName} - ${typeStr}`);
}
function parseKeyFormatAndType(
enc: EncodingOptions,
keyType?: string,
isPublic?: boolean,
objName?: string,
) {
const { format: formatStr, type: typeStr } = enc;
const isInput = keyType === undefined;
const format = parseKeyFormat(
formatStr,
isInput ? KFormatType.kKeyFormatPEM : undefined,
option('format', objName),
);
const isRequired =
(!isInput || format === KFormatType.kKeyFormatDER) &&
format !== KFormatType.kKeyFormatJWK;
const type = parseKeyType(
typeStr,
isRequired,
keyType,
isPublic,
option('type', objName),
);
return { format, type };
}
function parseKeyEncoding(
enc: EncodingOptions,
keyType?: string,
isPublic?: boolean,
objName?: string,
) {
// validateObject(enc, 'options');
const isInput = keyType === undefined;
const { format, type } = parseKeyFormatAndType(
enc,
keyType,
isPublic,
objName,
);
let cipher, passphrase, encoding;
if (isPublic !== true) {
({ cipher, passphrase, encoding } = enc);
if (!isInput) {
if (cipher != null) {
if (typeof cipher !== 'string')
throw new Error(
`Invalid argument ${option('cipher', objName)}: ${cipher}`,
);
if (
format === KFormatType.kKeyFormatDER &&
(type === KeyEncoding.kKeyEncodingPKCS1 ||
type === KeyEncoding.kKeyEncodingSEC1)
) {
throw new Error(
`Incompatible key options ${encodingNames[type]} does not support encryption`,
);
}
} else if (passphrase !== undefined) {
throw new Error(
`invalid argument ${option('cipher', objName)}: ${cipher}`,
);
}
}
if (
(isInput && passphrase !== undefined && !isStringOrBuffer(passphrase)) ||
(!isInput && cipher != null && !isStringOrBuffer(passphrase))
) {
throw new Error(
`Invalid argument value ${option('passphrase', objName)}: ${passphrase}`,
);
}
}
if (passphrase !== undefined)
passphrase = binaryLikeToArrayBuffer(passphrase, encoding);
return { format, type, cipher, passphrase };
}
function prepareAsymmetricKey(
key: BinaryLike | EncodingOptions,
ctx: KeyInputContext,
): {
format: KFormatType;
data: ArrayBuffer;
type?: KeyEncoding;
passphrase?: BinaryLike;
} {
// TODO(osp) check, KeyObject some node object
// if (isKeyObject(key)) {
// // Best case: A key object, as simple as that.
// return { data: getKeyObjectHandle(key, ctx) };
// } else
// if (isCryptoKey(key)) {
// return { data: getKeyObjectHandle(key[kKeyObject], ctx) };
// } else
if (isStringOrBuffer(key)) {
// Expect PEM by default, mostly for backward compatibility.
return {
format: KFormatType.kKeyFormatPEM,
data: binaryLikeToArrayBuffer(key),
};
} else if (typeof key === 'object') {
const { key: data, encoding } = key as EncodingOptions;
// // The 'key' property can be a KeyObject as well to allow specifying
// // additional options such as padding along with the key.
// if (isKeyObject(data)) {
// return { data: getKeyObjectHandle(data, ctx) };
// }
// else if (isCryptoKey(data))
// return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
// else if (isJwk(data) && format === 'jwk')
// return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
// Either PEM or DER using PKCS#1 or SPKI.
if (!isStringOrBuffer(data)) {
throw new Error(
'prepareAsymmetricKey: key is not a string or ArrayBuffer',
);
}
const isPublic =
ctx === KeyInputContext.kConsumePrivate ||
ctx === KeyInputContext.kCreatePrivate
? false
: undefined;
return {
data: binaryLikeToArrayBuffer(data, encoding),
...parseKeyEncoding(key as EncodingOptions, undefined, isPublic),
};
}
throw new Error('[prepareAsymetricKey] Invalid argument key: ${key}');
}
// TODO(osp) any here is a node KeyObject
export function preparePrivateKey(key: BinaryLike | EncodingOptions) {
return prepareAsymmetricKey(key, KeyInputContext.kConsumePrivate);
}
// TODO(osp) any here is a node KeyObject
export function preparePublicOrPrivateKey(key: BinaryLike | EncodingOptions) {
return prepareAsymmetricKey(key, KeyInputContext.kConsumePublic);
}
// Parses the public key encoding based on an object. keyType must be undefined
// when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding.
export function parsePublicKeyEncoding(
enc: EncodingOptions,
keyType: string | undefined,
objName?: string,
) {
return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName);
}
// Parses the private key encoding based on an object. keyType must be undefined
// when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding.
export function parsePrivateKeyEncoding(
enc: EncodingOptions,
keyType: string | undefined,
objName?: string,
) {
return parseKeyEncoding(enc, keyType, false, objName);
}
// function getKeyObjectHandle(key: any, ctx: KeyInputContext) {
// if (ctx === KeyInputContext.kConsumePublic) {
// throw new Error(
// 'Invalid argument type for "key". Need ArrayBuffer, TypeArray, KeyObject, CryptoKey, string'
// );
// }
// if (key.type !== 'private') {
// if (
// ctx === KeyInputContext.kConsumePrivate ||
// ctx === KeyInputContext.kCreatePublic
// )
// throw new Error(`Invalid KeyObject type: ${key.type}, expected 'public'`);
// if (key.type !== 'public') {
// throw new Error(
// `Invalid KeyObject type: ${key.type}, expected 'private' or 'public'`
// );
// }
// }
// return key.handle;
// }
function prepareSecretKey(
key: BinaryLike,
encoding?: string,
bufferOnly = false,
): ArrayBuffer {
try {
if (!bufferOnly) {
// TODO: maybe use `key.constructor.name === 'KeyObject'` ?
if (key instanceof KeyObject) {
if (key.type !== 'secret')
throw new Error(
`invalid KeyObject type: ${key.type}, expected 'secret'`,
);
return key.handle.export();
}
// TODO: maybe use `key.constructor.name === 'CryptoKey'` ?
else if (key instanceof CryptoKey) {
if (key.type !== 'secret')
throw new Error(
`invalid CryptoKey type: ${key.type}, expected 'secret'`,
);
return key.keyObject.handle.export();
}
}
if (key instanceof ArrayBuffer) {
return key;
}
return binaryLikeToArrayBuffer(key, encoding);
} catch (error) {
throw new Error(
'Invalid argument type for "key". Need ArrayBuffer, TypedArray, KeyObject, CryptoKey, string',
{ cause: error },
);
}
}
export function createSecretKey(
key: BinaryLike,
encoding?: string,
): SecretKeyObject {
const k = prepareSecretKey(key, encoding, true);
const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle();
handle.init(KeyType.Secret, k);
return new SecretKeyObject(handle);
}
export function createPublicKey(
key: BinaryLike | EncodingOptions,
): PublicKeyObject {
const { format, type, data, passphrase } = prepareAsymmetricKey(
key,
KeyInputContext.kCreatePublic,
);
const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle();
if (format === KFormatType.kKeyFormatJWK) {
handle.init(KeyType.Public, data);
} else {
handle.init(KeyType.Public, data, format, type, passphrase);
}
return new PublicKeyObject(handle);
}
export const createPrivateKey = (
key: BinaryLike | EncodingOptions,
): PrivateKeyObject => {
const { format, type, data, passphrase } = prepareAsymmetricKey(
key,
KeyInputContext.kCreatePrivate,
);
const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle();
if (format === KFormatType.kKeyFormatJWK) {
handle.init(KeyType.Private, data);
} else {
handle.init(KeyType.Private, data, format, type, passphrase);
}
return new PrivateKeyObject(handle);
};
// const isKeyObject = (obj: any): obj is KeyObject => {
// return obj != null && obj.keyType !== undefined;
// };
export class CryptoKey {
keyObject: KeyObject;
keyAlgorithm: SubtleAlgorithm;
keyUsages: KeyUsage[];
keyExtractable: boolean;
constructor(
keyObject: KeyObject,
keyAlgorithm: SubtleAlgorithm,
keyUsages: KeyUsage[],
keyExtractable: boolean,
) {
this.keyObject = keyObject;
this.keyAlgorithm = keyAlgorithm;
this.keyUsages = keyUsages;
this.keyExtractable = keyExtractable;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
inspect(_depth: number, _options: any): any {
throw new Error('CryptoKey.inspect is not implemented');
// if (depth < 0) return this;
// const opts = {
// ...options,
// depth: options.depth == null ? null : options.depth - 1,
// };
// return `CryptoKey ${inspect(
// {
// type: this.type,
// extractable: this.extractable,
// algorithm: this.algorithm,
// usages: this.usages,
// },
// opts
// )}`;
}
get type() {
// if (!(this instanceof CryptoKey)) throw new Error('Invalid CryptoKey');
return this.keyObject.type;
}
get extractable() {
return this.keyExtractable;
}
get algorithm() {
return this.keyAlgorithm;
}
get usages() {
return this.keyUsages;
}
}
class KeyObject {
handle: KeyObjectHandle;
type: 'public' | 'secret' | 'private' | 'unknown' = 'unknown';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export(_options?: EncodingOptions): ArrayBuffer {
return new ArrayBuffer(0);
}
constructor(type: string, handle: KeyObjectHandle) {
if (type !== 'secret' && type !== 'public' && type !== 'private')
throw new Error(`invalid KeyObject type: ${type}`);
this.handle = handle;
this.type = type;
}
// get type(): string {
// return this.type;
// }
// static from(key) {
// if (!isCryptoKey(key))
// throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
// return key[kKeyObject];
// }
// equals(otherKeyObject) {
// if (!isKeyObject(otherKeyObject)) {
// throw new ERR_INVALID_ARG_TYPE(
// 'otherKeyObject',
// 'KeyObject',
// otherKeyObject
// );
// }
// return (
// otherKeyObject.type === this.type &&
// this[kHandle].equals(otherKeyObject[kHandle])
// );
// }
}
export class SecretKeyObject extends KeyObject {
constructor(handle: KeyObjectHandle) {
super('secret', handle);
}
// get symmetricKeySize() {
// return this[kHandle].getSymmetricKeySize();
// }
export(options?: EncodingOptions) {
if (options !== undefined) {
if (options.format === 'jwk') {
throw new Error('SecretKey export for jwk is not implemented');
// return this.handle.exportJwk({}, false);
}
}
return this.handle.export();
}
}
// const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
// const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails');
// function normalizeKeyDetails(details = {}) {
// if (details.publicExponent !== undefined) {
// return {
// ...details,
// publicExponent: bigIntArrayToUnsignedBigInt(
// new Uint8Array(details.publicExponent)
// ),
// };
// }
// return details;
// }
class AsymmetricKeyObject extends KeyObject {
constructor(type: string, handle: KeyObjectHandle) {
super(type, handle);
}
private _asymmetricKeyType?: AsymmetricKeyType;
get asymmetricKeyType(): AsymmetricKeyType {
if (!this._asymmetricKeyType) {
this._asymmetricKeyType = this.handle.getAsymmetricKeyType();
}
return this._asymmetricKeyType;
}
// get asymmetricKeyDetails() {
// switch (this._asymmetricKeyType) {
// case 'rsa':
// case 'rsa-pss':
// case 'dsa':
// case 'ec':
// return (
// this[kAsymmetricKeyDetails] ||
// (this[kAsymmetricKeyDetails] = normalizeKeyDetails(
// this[kHandle].keyDetail({})
// ))
// );
// default:
// return {};
// }
// }
}
export class PublicKeyObject extends AsymmetricKeyObject {
constructor(handle: KeyObjectHandle) {
super('public', handle);
}
export(options: EncodingOptions) {
if (options?.format === 'jwk') {
throw new Error('PublicKey export for jwk is not implemented');
// return this.handle.exportJwk({}, false);
}
const { format, type } = parsePublicKeyEncoding(
options,
this.asymmetricKeyType,
);
return this.handle.export(format, type);
}
}
export class PrivateKeyObject extends AsymmetricKeyObject {
constructor(handle: KeyObjectHandle) {
super('private', handle);
}
export(options: EncodingOptions) {
if (options?.format === 'jwk') {
if (options.passphrase !== undefined) {
throw new Error('jwk does not support encryption');
}
throw new Error('PrivateKey export for jwk is not implemented');
// return this.handle.exportJwk({}, false);
}
const { format, type, cipher, passphrase } = parsePrivateKeyEncoding(
options,
this.asymmetricKeyType,
);
return this.handle.export(format, type, cipher, passphrase);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isCryptoKey = (obj: any): boolean => {
return obj !== null && obj?.keyObject !== undefined;
};