react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
384 lines (347 loc) • 11.1 kB
text/typescript
import {
AsymmetricKeyObject,
CryptoKey,
KeyObject,
SecretKeyObject,
PublicKeyObject,
PrivateKeyObject,
} from './classes';
import { generateKeyPair, generateKeyPairSync } from './generateKeyPair';
import {
createSign,
createVerify,
sign,
verify,
Sign,
Verify,
} from './signVerify';
import {
publicEncrypt,
publicDecrypt,
privateEncrypt,
privateDecrypt,
} from './publicCipher';
import {
isCryptoKey,
parseKeyEncoding,
parsePrivateKeyEncoding,
parsePublicKeyEncoding,
} from './utils';
import { NitroModules } from 'react-native-nitro-modules';
import type { BinaryLike, JWK, KeyObjectHandle } from '../utils';
import {
binaryLikeToArrayBuffer as toAB,
isStringOrBuffer,
KFormatType,
KeyEncoding,
KeyType,
} from '../utils';
import { randomBytes } from '../random';
interface KeyInputObject {
key: BinaryLike | KeyObject | CryptoKey | JWK;
format?: 'pem' | 'der' | 'jwk' | 'raw-public' | 'raw-private' | 'raw-seed';
type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1';
passphrase?: BinaryLike;
encoding?: BufferEncoding;
asymmetricKeyType?: string;
namedCurve?: string;
}
type KeyInput = BinaryLike | KeyInputObject | KeyObject | CryptoKey;
function isRawFormat(
format: string | undefined,
): format is 'raw-public' | 'raw-private' | 'raw-seed' {
return (
format === 'raw-public' || format === 'raw-private' || format === 'raw-seed'
);
}
function createPublicKeyFromRaw(input: KeyInputObject): PublicKeyObject {
if (input.format !== 'raw-public') {
throw new Error('Invalid format for createPublicKey raw import');
}
if (typeof input.asymmetricKeyType !== 'string') {
throw new Error('options.asymmetricKeyType is required for raw key import');
}
if (input.asymmetricKeyType === 'ec' && !input.namedCurve) {
throw new Error('options.namedCurve is required for EC raw key import');
}
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
handle.initRawPublic(
input.asymmetricKeyType,
toAB(input.key as BinaryLike),
input.namedCurve,
);
return new PublicKeyObject(handle);
}
function createPrivateKeyFromRaw(input: KeyInputObject): PrivateKeyObject {
if (input.format !== 'raw-private' && input.format !== 'raw-seed') {
throw new Error('Invalid format for createPrivateKey raw import');
}
if (typeof input.asymmetricKeyType !== 'string') {
throw new Error('options.asymmetricKeyType is required for raw key import');
}
if (input.asymmetricKeyType === 'ec' && !input.namedCurve) {
throw new Error('options.namedCurve is required for EC raw key import');
}
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
if (input.format === 'raw-seed') {
handle.initRawSeed(input.asymmetricKeyType, toAB(input.key as BinaryLike));
} else {
handle.initRawPrivate(
input.asymmetricKeyType,
toAB(input.key as BinaryLike),
input.namedCurve,
);
}
return new PrivateKeyObject(handle);
}
function createSecretKey(key: BinaryLike): SecretKeyObject {
const keyBuffer = toAB(key);
return KeyObject.createKeyObject('secret', keyBuffer) as SecretKeyObject;
}
function prepareAsymmetricKey(
key: KeyInput,
isPublic: boolean,
): {
data: ArrayBuffer;
format?: 'pem' | 'der';
type?: 'pkcs1' | 'pkcs8' | 'spki' | 'sec1';
passphrase?: ArrayBuffer;
} {
if (key instanceof KeyObject) {
if (isPublic) {
// createPublicKey can accept either a public key or extract public from private
if (key.type === 'secret') {
throw new Error('Cannot create public key from secret key');
}
// Export as SPKI (public key format) - works for both public and private keys
const exported = key.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI);
return { data: exported, format: 'der', type: 'spki' };
} else {
// createPrivateKey requires a private key
if (key.type !== 'private') {
throw new Error('Key must be a private key');
}
const exported = key.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8);
return { data: exported, format: 'der', type: 'pkcs8' };
}
}
if (isCryptoKey(key)) {
const cryptoKey = key as CryptoKey;
return prepareAsymmetricKey(cryptoKey.keyObject, isPublic);
}
if (isStringOrBuffer(key)) {
// Detect PEM format from string content
const isPem = typeof key === 'string' && key.includes('-----BEGIN');
return { data: toAB(key), format: isPem ? 'pem' : undefined };
}
if (typeof key === 'object' && 'key' in key) {
const keyObj = key as KeyInputObject;
const { key: data, format, type, passphrase } = keyObj;
const passphraseAB =
passphrase !== undefined ? toAB(passphrase) : undefined;
if (data instanceof KeyObject) {
return prepareAsymmetricKey(data, isPublic);
}
if (isCryptoKey(data)) {
return prepareAsymmetricKey((data as CryptoKey).keyObject, isPublic);
}
if (!isStringOrBuffer(data)) {
throw new Error('Invalid key data type');
}
// For PEM format with string data, convert to ArrayBuffer
if (
(format === 'pem' ||
(typeof data === 'string' && data.includes('-----BEGIN'))) &&
typeof data === 'string'
) {
return {
data: toAB(data),
format: 'pem',
type,
passphrase: passphraseAB,
};
}
// Filter to only 'pem' or 'der' — JWK and raw formats are handled
// separately via dedicated paths.
const filteredFormat: 'pem' | 'der' | undefined =
format === 'pem' || format === 'der' ? format : undefined;
return {
data: toAB(data),
format: filteredFormat,
type,
passphrase: passphraseAB,
};
}
throw new Error('Invalid key input');
}
function createPublicKey(key: KeyInput): PublicKeyObject {
if (typeof key === 'object' && 'key' in key && isRawFormat(key.format)) {
if (key.format !== 'raw-public') {
throw new Error(
`Invalid format ${key.format} for createPublicKey — only 'raw-public' is allowed`,
);
}
return createPublicKeyFromRaw(key as KeyInputObject);
}
if (typeof key === 'object' && 'key' in key && key.format === 'jwk') {
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
const keyType = handle.initJwk(key.key as JWK);
if (keyType === undefined) {
throw new Error('Failed to import JWK');
}
if (keyType === KeyType.PRIVATE) {
// Extract public from private
const exported = handle.exportKey(KFormatType.DER, KeyEncoding.SPKI);
const pubHandle =
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
pubHandle.init(
KeyType.PUBLIC,
exported,
KFormatType.DER,
KeyEncoding.SPKI,
);
return new PublicKeyObject(pubHandle);
}
return new PublicKeyObject(handle);
}
const { data, format, type, passphrase } = prepareAsymmetricKey(key, true);
// Map format string to KFormatType enum
let kFormat: KFormatType | undefined;
if (format === 'pem') kFormat = KFormatType.PEM;
else if (format === 'der') kFormat = KFormatType.DER;
// Map type string to KeyEncoding enum
let kType: KeyEncoding | undefined;
if (type === 'spki') kType = KeyEncoding.SPKI;
else if (type === 'pkcs1') kType = KeyEncoding.PKCS1;
return KeyObject.createKeyObject(
'public',
data,
kFormat,
kType,
passphrase,
) as PublicKeyObject;
}
function createPrivateKey(key: KeyInput): PrivateKeyObject {
if (typeof key === 'object' && 'key' in key && isRawFormat(key.format)) {
if (key.format === 'raw-public') {
throw new Error("Invalid format 'raw-public' for createPrivateKey");
}
return createPrivateKeyFromRaw(key as KeyInputObject);
}
if (typeof key === 'object' && 'key' in key && key.format === 'jwk') {
const handle =
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
const keyType = handle.initJwk(key.key as JWK);
if (keyType === undefined || keyType !== KeyType.PRIVATE) {
throw new Error('Failed to import private key from JWK');
}
return new PrivateKeyObject(handle);
}
const { data, format, type, passphrase } = prepareAsymmetricKey(key, false);
// Map format string to KFormatType enum
let kFormat: KFormatType | undefined;
if (format === 'pem') kFormat = KFormatType.PEM;
else if (format === 'der') kFormat = KFormatType.DER;
// Map type string to KeyEncoding enum
let kType: KeyEncoding | undefined;
if (type === 'pkcs8') kType = KeyEncoding.PKCS8;
else if (type === 'pkcs1') kType = KeyEncoding.PKCS1;
else if (type === 'sec1') kType = KeyEncoding.SEC1;
return KeyObject.createKeyObject(
'private',
data,
kFormat,
kType,
passphrase,
) as PrivateKeyObject;
}
export interface GenerateKeyOptions {
length: number;
}
function generateKeySync(
type: 'aes' | 'hmac',
options: GenerateKeyOptions,
): SecretKeyObject {
if (typeof type !== 'string') {
throw new TypeError('The "type" argument must be a string');
}
if (typeof options !== 'object' || options === null) {
throw new TypeError('The "options" argument must be an object');
}
const { length } = options;
if (typeof length !== 'number' || !Number.isInteger(length)) {
throw new TypeError('The "options.length" property must be an integer');
}
switch (type) {
case 'hmac':
if (length < 8 || length > 2 ** 31 - 1) {
throw new RangeError(
'The "options.length" property must be >= 8 and <= 2147483647',
);
}
break;
case 'aes':
if (length !== 128 && length !== 192 && length !== 256) {
throw new RangeError(
'The "options.length" property must be 128, 192, or 256',
);
}
break;
default:
throw new TypeError(
`The "type" argument must be 'aes' or 'hmac'. Received '${type}'`,
);
}
const keyBytes = length / 8;
const keyMaterial = randomBytes(keyBytes);
return createSecretKey(keyMaterial);
}
function generateKey(
type: 'aes' | 'hmac',
options: GenerateKeyOptions,
callback: (err: Error | null, key?: SecretKeyObject) => void,
): void {
if (typeof callback !== 'function') {
throw new TypeError('The "callback" argument must be a function');
}
try {
const key = generateKeySync(type, options);
process.nextTick(callback, null, key);
} catch (err) {
process.nextTick(callback, err as Error);
}
}
export {
// Node Public API
createSecretKey,
createPublicKey,
createPrivateKey,
CryptoKey,
generateKey,
generateKeySync,
generateKeyPair,
generateKeyPairSync,
AsymmetricKeyObject,
KeyObject,
createSign,
createVerify,
sign,
verify,
Sign,
Verify,
publicEncrypt,
publicDecrypt,
privateEncrypt,
privateDecrypt,
// Node Internal API
parsePublicKeyEncoding,
parsePrivateKeyEncoding,
parseKeyEncoding,
SecretKeyObject,
PublicKeyObject,
PrivateKeyObject,
isCryptoKey,
};