react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
250 lines (227 loc) • 6.82 kB
text/typescript
import { NitroModules } from 'react-native-nitro-modules';
import type { RsaCipher } from '../specs/rsaCipher.nitro';
import type { BinaryLike } from '../utils';
import {
binaryLikeToArrayBuffer as toAB,
isStringOrBuffer,
KFormatType,
KeyEncoding,
} from '../utils';
import { isCryptoKey } from './utils';
import { KeyObject, CryptoKey } from './classes';
import { constants } from '../constants';
interface PublicCipherOptions {
key: BinaryLike | KeyObject | CryptoKey;
padding?: number;
oaepHash?: string;
oaepLabel?: BinaryLike;
}
type PublicCipherInput =
| BinaryLike
| KeyObject
| CryptoKey
| PublicCipherOptions;
interface PrivateCipherOptions {
key: BinaryLike | KeyObject | CryptoKey;
padding?: number;
oaepHash?: string;
oaepLabel?: BinaryLike;
}
type PrivateCipherInput =
| BinaryLike
| KeyObject
| CryptoKey
| PrivateCipherOptions;
function preparePublicCipherKey(
key: PublicCipherInput,
isEncrypt: boolean,
): {
keyHandle: KeyObject;
padding?: number;
oaepHash?: string;
oaepLabel?: ArrayBuffer;
} {
let keyObj: KeyObject;
let padding: number | undefined;
let oaepHash: string | undefined;
let oaepLabel: ArrayBuffer | undefined;
if (key instanceof KeyObject) {
if (isEncrypt && key.type !== 'public') {
throw new Error('publicEncrypt requires a public key');
}
// publicDecrypt accepts both public and private keys (Node.js behavior)
// A private key contains the public components needed for verify_recover
keyObj = key;
} else if (isCryptoKey(key)) {
const cryptoKey = key as CryptoKey;
keyObj = cryptoKey.keyObject;
} else if (isStringOrBuffer(key)) {
const data = toAB(key);
const isPem = typeof key === 'string' && key.includes('-----BEGIN');
const isPrivatePem =
typeof key === 'string' && key.includes('-----BEGIN PRIVATE');
// publicDecrypt accepts both public and private keys (Node.js behavior)
if (!isEncrypt && isPrivatePem) {
keyObj = KeyObject.createKeyObject(
'private',
data,
KFormatType.PEM,
KeyEncoding.PKCS8,
);
} else {
keyObj = KeyObject.createKeyObject(
'public',
data,
isPem ? KFormatType.PEM : KFormatType.DER,
KeyEncoding.SPKI,
);
}
} else if (typeof key === 'object' && 'key' in key) {
const options = key as PublicCipherOptions;
const result = preparePublicCipherKey(options.key, isEncrypt);
keyObj = result.keyHandle;
padding = options.padding;
oaepHash = options.oaepHash;
if (options.oaepLabel) {
oaepLabel = toAB(options.oaepLabel);
}
} else {
throw new Error('Invalid key input');
}
return { keyHandle: keyObj, padding, oaepHash, oaepLabel };
}
export function publicEncrypt(
key: PublicCipherInput,
buffer: BinaryLike,
): Buffer {
const { keyHandle, padding, oaepHash, oaepLabel } = preparePublicCipherKey(
key,
true,
);
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
const paddingMode = padding ?? constants.RSA_PKCS1_OAEP_PADDING;
const hashAlgorithm = oaepHash || 'SHA-256';
try {
const encrypted = rsaCipher.encrypt(
keyHandle.handle,
data,
paddingMode,
hashAlgorithm,
oaepLabel,
);
return Buffer.from(encrypted);
} catch (error) {
throw new Error(`publicEncrypt failed: ${(error as Error).message}`);
}
}
export function publicDecrypt(
key: PublicCipherInput,
buffer: BinaryLike,
): Buffer {
const { keyHandle, padding } = preparePublicCipherKey(key, false);
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
const paddingMode = padding ?? constants.RSA_PKCS1_PADDING;
try {
const decrypted = rsaCipher.publicDecrypt(
keyHandle.handle,
data,
paddingMode,
);
return Buffer.from(decrypted);
} catch (error) {
throw new Error(`publicDecrypt failed: ${(error as Error).message}`);
}
}
function preparePrivateCipherKey(
key: PrivateCipherInput,
isEncrypt: boolean,
): {
keyHandle: KeyObject;
padding?: number;
oaepHash?: string;
oaepLabel?: ArrayBuffer;
} {
let keyObj: KeyObject;
let padding: number | undefined;
let oaepHash: string | undefined;
let oaepLabel: ArrayBuffer | undefined;
if (key instanceof KeyObject) {
if (isEncrypt && key.type !== 'private') {
throw new Error('privateEncrypt requires a private key');
}
if (!isEncrypt && key.type !== 'private') {
throw new Error('privateDecrypt requires a private key');
}
keyObj = key;
} else if (isCryptoKey(key)) {
const cryptoKey = key as CryptoKey;
keyObj = cryptoKey.keyObject;
} else if (isStringOrBuffer(key)) {
const data = toAB(key);
const isPem = typeof key === 'string' && key.includes('-----BEGIN');
keyObj = KeyObject.createKeyObject(
'private',
data,
isPem ? KFormatType.PEM : KFormatType.DER,
KeyEncoding.PKCS8,
);
} else if (typeof key === 'object' && 'key' in key) {
const options = key as PrivateCipherOptions;
const result = preparePrivateCipherKey(options.key, isEncrypt);
keyObj = result.keyHandle;
padding = options.padding;
oaepHash = options.oaepHash;
if (options.oaepLabel) {
oaepLabel = toAB(options.oaepLabel);
}
} else {
throw new Error('Invalid key input');
}
return { keyHandle: keyObj, padding, oaepHash, oaepLabel };
}
export function privateEncrypt(
key: PrivateCipherInput,
buffer: BinaryLike,
): Buffer {
const { keyHandle, padding } = preparePrivateCipherKey(key, true);
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
const paddingMode = padding ?? constants.RSA_PKCS1_PADDING;
try {
const encrypted = rsaCipher.privateEncrypt(
keyHandle.handle,
data,
paddingMode,
);
return Buffer.from(encrypted);
} catch (error) {
throw new Error(`privateEncrypt failed: ${(error as Error).message}`);
}
}
export function privateDecrypt(
key: PrivateCipherInput,
buffer: BinaryLike,
): Buffer {
const { keyHandle, padding, oaepHash, oaepLabel } = preparePrivateCipherKey(
key,
false,
);
const rsaCipher: RsaCipher = NitroModules.createHybridObject('RsaCipher');
const data = toAB(buffer);
const paddingMode = padding ?? constants.RSA_PKCS1_OAEP_PADDING;
const hashAlgorithm = oaepHash || 'SHA-256';
try {
const decrypted = rsaCipher.privateDecrypt(
keyHandle.handle,
data,
paddingMode,
hashAlgorithm,
oaepLabel,
);
return Buffer.from(decrypted);
} catch (error) {
throw new Error(`privateDecrypt failed: ${(error as Error).message}`);
}
}