react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
358 lines (342 loc) • 13.5 kB
JavaScript
;
import { NitroModules } from 'react-native-nitro-modules';
import { CryptoKey, KeyObject, PublicKeyObject, PrivateKeyObject } from './keys/classes';
import { bufferLikeToArrayBuffer, getUsagesUnion, hasAnyNotIn, kNamedCurveAliases, lazyDOMException, normalizeHashName, validateJwkStructure, HashContext, KeyEncoding, KFormatType } from './utils';
import { Buffer } from '@craftzdog/react-native-buffer';
import { ECDH } from './ecdh';
class EcUtils {
static get native() {
if (!this._native) {
this._native = NitroModules.createHybridObject('EcKeyPair');
}
return this._native;
}
static getSupportedCurves() {
return this.native.getSupportedCurves();
}
}
export function getCurves() {
return EcUtils.getSupportedCurves();
}
export class Ec {
constructor(curve) {
this.native = NitroModules.createHybridObject('EcKeyPair');
this.native.setCurve(curve);
}
async generateKeyPair() {
await this.native.generateKeyPair();
return {
publicKey: this.native.getPublicKey(),
privateKey: this.native.getPrivateKey()
};
}
generateKeyPairSync() {
this.native.generateKeyPairSync();
return {
publicKey: this.native.getPublicKey(),
privateKey: this.native.getPrivateKey()
};
}
}
// WebCrypto API - only P-256, P-384, P-521 allowed per spec
export function ecImportKey(format, keyData, algorithm, extractable, keyUsages) {
const {
name,
namedCurve
} = algorithm;
if (!namedCurve || !kNamedCurveAliases[namedCurve]) {
throw lazyDOMException('Unrecognized namedCurve', 'NotSupportedError');
}
if (format === 'jwk') {
const jwk = keyData;
if (!jwk || typeof jwk !== 'object') {
throw lazyDOMException('Invalid keyData', 'DataError');
}
if (jwk.kty !== 'EC') {
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
}
if (jwk.crv !== namedCurve) {
throw lazyDOMException('JWK "crv" does not match the requested algorithm', 'DataError');
}
const expectedUse = name === 'ECDH' ? 'enc' : 'sig';
validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
if (jwk.alg !== undefined) {
let expectedAlg;
if (name === 'ECDSA') {
expectedAlg = namedCurve === 'P-256' ? 'ES256' : namedCurve === 'P-384' ? 'ES384' : namedCurve === 'P-521' ? 'ES512' : undefined;
} else if (name === 'ECDH') {
expectedAlg = 'ECDH-ES';
}
if (expectedAlg && jwk.alg !== expectedAlg) {
throw lazyDOMException('JWK "alg" does not match the requested algorithm', 'DataError');
}
}
const isPublicJwk = jwk.d === undefined;
const validUsagesJwk = name === 'ECDSA' ? isPublicJwk ? ['verify'] : ['sign'] : isPublicJwk ? [] : ['deriveKey', 'deriveBits'];
if (hasAnyNotIn(keyUsages, validUsagesJwk)) {
throw lazyDOMException(`Unsupported key usage for a ${name} key`, 'SyntaxError');
}
const handle = NitroModules.createHybridObject('KeyObjectHandle');
let keyType;
try {
keyType = handle.initJwk(jwk, namedCurve);
} catch (err) {
throw lazyDOMException('Invalid keyData', {
name: 'DataError',
cause: err
});
}
if (keyType === undefined) {
throw lazyDOMException('Invalid keyData', 'DataError');
}
let keyObject;
if (keyType === 1) {
keyObject = new PublicKeyObject(handle);
} else if (keyType === 2) {
keyObject = new PrivateKeyObject(handle);
} else {
throw lazyDOMException('Invalid keyData', 'DataError');
}
return new CryptoKey(keyObject, algorithm, keyUsages, extractable);
}
if (format !== 'spki' && format !== 'pkcs8' && format !== 'raw') {
throw lazyDOMException(`Unsupported format: ${format}`, 'NotSupportedError');
}
const expectedKeyType = format === 'spki' || format === 'raw' ? 'public' : 'private';
const isPublicKey = expectedKeyType === 'public';
let validUsages;
if (name === 'ECDSA') {
validUsages = isPublicKey ? ['verify'] : ['sign'];
} else if (name === 'ECDH') {
validUsages = isPublicKey ? [] : ['deriveKey', 'deriveBits'];
} else {
throw lazyDOMException('Unsupported algorithm', 'NotSupportedError');
}
if (hasAnyNotIn(keyUsages, validUsages)) {
throw lazyDOMException(`Unsupported key usage for a ${name} key`, 'SyntaxError');
}
// Convert keyData to ArrayBuffer
const keyBuffer = bufferLikeToArrayBuffer(keyData);
// Create KeyObject directly using the appropriate format
let keyObject;
if (format === 'raw') {
// Raw format is only for public keys - use specialized EC raw import
const handle = NitroModules.createHybridObject('KeyObjectHandle');
const curveAlias = kNamedCurveAliases[namedCurve];
// Only throw if initialization explicitly fails
if (!handle.initECRaw(curveAlias, keyBuffer)) {
throw lazyDOMException('Failed to import EC raw key', 'DataError');
}
keyObject = new PublicKeyObject(handle);
} else {
// Use standard DER import for spki/pkcs8
keyObject = KeyObject.createKeyObject(expectedKeyType, keyBuffer, KFormatType.DER, format === 'spki' ? KeyEncoding.SPKI : KeyEncoding.PKCS8);
// Validate the imported curve matches the requested algorithm.namedCurve.
const expectedAlias = kNamedCurveAliases[namedCurve];
if (keyObject.handle.keyDetail().namedCurve !== expectedAlias) {
throw lazyDOMException('Named curve mismatch', 'DataError');
}
}
// Verify the public/private point lies on the named curve.
if (!keyObject.handle.checkEcKeyData()) {
throw lazyDOMException('Invalid keyData', 'DataError');
}
return new CryptoKey(keyObject, algorithm, keyUsages, extractable);
}
// Node API
export const ecdsaSignVerify = (key, data, {
hash
}, signature) => {
const isSign = signature === undefined;
const expectedKeyType = isSign ? 'private' : 'public';
if (key.type !== expectedKeyType) {
throw lazyDOMException(`Key must be a ${expectedKeyType} key`, 'InvalidAccessError');
}
const hashName = typeof hash === 'string' ? hash : hash?.name;
if (!hashName) {
throw lazyDOMException('Hash algorithm is required for ECDSA', 'InvalidAccessError');
}
// Normalize hash algorithm name to WebCrypto format for C++ layer
const normalizedHashName = normalizeHashName(hashName, HashContext.WebCrypto);
// Create EC instance with the curve from the key
const namedCurve = key.algorithm.namedCurve;
const ec = new Ec(namedCurve);
// Extract and import the actual key data from the CryptoKey
// Export in DER format with appropriate encoding
const encoding = key.type === 'private' ? KeyEncoding.PKCS8 : KeyEncoding.SPKI;
const keyData = key.keyObject.handle.exportKey(KFormatType.DER, encoding);
const keyBuffer = bufferLikeToArrayBuffer(keyData);
ec.native.importKey('der', keyBuffer, key.algorithm.name, key.extractable, key.usages);
const dataBuffer = bufferLikeToArrayBuffer(data);
if (isSign) {
// Sign operation
return ec.native.sign(dataBuffer, normalizedHashName);
} else {
// Verify operation
const signatureBuffer = bufferLikeToArrayBuffer(signature);
return ec.native.verify(dataBuffer, signatureBuffer, normalizedHashName);
}
};
// WebCrypto API - only P-256, P-384, P-521 allowed per spec
export async function ec_generateKeyPair(name, namedCurve, extractable, keyUsages,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options // TODO: Implement format options support
) {
// validation checks
if (!Object.keys(kNamedCurveAliases).includes(namedCurve || '')) {
throw lazyDOMException(`Unrecognized namedCurve '${namedCurve}'`, 'NotSupportedError');
}
switch (name) {
case 'ECDSA':
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
throw lazyDOMException('Unsupported key usage for an ECDSA key', 'SyntaxError');
}
break;
case 'ECDH':
if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException('Unsupported key usage for an ECDH key', 'SyntaxError');
}
// Fall through
}
const ec = new Ec(namedCurve);
await ec.generateKeyPair();
let publicUsages = [];
let privateUsages = [];
switch (name) {
case 'ECDSA':
publicUsages = getUsagesUnion(keyUsages, 'verify');
privateUsages = getUsagesUnion(keyUsages, 'sign');
break;
case 'ECDH':
publicUsages = [];
privateUsages = getUsagesUnion(keyUsages, 'deriveKey', 'deriveBits');
break;
}
const keyAlgorithm = {
name,
namedCurve: namedCurve
};
// Export keys directly from the EC key pair using the internal EVP_PKEY
// These methods export in DER format (SPKI for public, PKCS8 for private)
const publicKeyData = ec.native.getPublicKey();
const privateKeyData = ec.native.getPrivateKey();
const pub = KeyObject.createKeyObject('public', publicKeyData, KFormatType.DER, KeyEncoding.SPKI);
const publicKey = new CryptoKey(pub, keyAlgorithm, publicUsages, true);
// All keys are now exported in PKCS8 format for consistency
const priv = KeyObject.createKeyObject('private', privateKeyData, KFormatType.DER, KeyEncoding.PKCS8);
const privateKey = new CryptoKey(priv, keyAlgorithm, privateUsages, extractable);
return {
publicKey,
privateKey
};
}
function ec_prepareKeyGenParams(options) {
if (!options) {
throw new Error('Options are required for EC key generation');
}
const {
namedCurve
} = options;
if (!namedCurve) {
throw new Error('namedCurve is required for EC key generation');
}
return new Ec(namedCurve);
}
function ec_formatKeyPairOutput(ec, encoding) {
const {
publicFormat,
publicType,
privateFormat,
privateType,
cipher,
passphrase
} = encoding;
const publicKeyData = ec.native.getPublicKey();
const privateKeyData = ec.native.getPrivateKey();
const pub = KeyObject.createKeyObject('public', publicKeyData, KFormatType.DER, KeyEncoding.SPKI);
const priv = KeyObject.createKeyObject('private', privateKeyData, KFormatType.DER, KeyEncoding.PKCS8);
let publicKey;
let privateKey;
if (publicFormat === -1) {
publicKey = pub;
} else if (publicFormat === 'raw-public') {
publicKey = Buffer.from(pub.handle.exportECPublicRaw(publicType === 'compressed'));
} else {
const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
const keyEncoding = publicType === KeyEncoding.SPKI ? KeyEncoding.SPKI : KeyEncoding.SPKI;
const exported = pub.handle.exportKey(format, keyEncoding);
if (format === KFormatType.PEM) {
publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8');
} else {
publicKey = exported;
}
}
if (privateFormat === -1) {
privateKey = priv;
} else if (privateFormat === 'raw-private') {
privateKey = Buffer.from(priv.handle.exportECPrivateRaw());
} else {
const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER;
const keyEncoding = privateType === KeyEncoding.PKCS8 ? KeyEncoding.PKCS8 : privateType === KeyEncoding.SEC1 ? KeyEncoding.SEC1 : KeyEncoding.PKCS8;
const exported = priv.handle.exportKey(format, keyEncoding, cipher, passphrase);
if (format === KFormatType.PEM) {
privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8');
} else {
privateKey = exported;
}
}
return {
publicKey,
privateKey
};
}
export async function ec_generateKeyPairNode(options, encoding) {
const ec = ec_prepareKeyGenParams(options);
await ec.generateKeyPair();
return ec_formatKeyPairOutput(ec, encoding);
}
export function ec_generateKeyPairNodeSync(options, encoding) {
const ec = ec_prepareKeyGenParams(options);
ec.generateKeyPairSync();
return ec_formatKeyPairOutput(ec, encoding);
}
export function ecDeriveBits(algorithm, baseKey, length) {
const publicKey = algorithm.public;
if (!publicKey) {
throw new Error('Public key is required for ECDH derivation');
}
if (baseKey.algorithm.name !== publicKey.algorithm.name) {
throw new Error('Keys must be of the same algorithm');
}
if (baseKey.algorithm.namedCurve !== publicKey.algorithm.namedCurve) {
throw new Error('Keys must use the same curve');
}
const namedCurve = baseKey.algorithm.namedCurve;
if (!namedCurve) {
throw new Error('Curve name is missing');
}
const opensslCurve = kNamedCurveAliases[namedCurve];
const ecdh = new ECDH(opensslCurve);
const jwkPrivate = baseKey.keyObject.handle.exportJwk({}, false);
if (!jwkPrivate.d) throw new Error('Invalid private key');
const privateBytes = Buffer.from(jwkPrivate.d, 'base64url');
ecdh.setPrivateKey(privateBytes);
const jwkPublic = publicKey.keyObject.handle.exportJwk({}, false);
if (!jwkPublic.x || !jwkPublic.y) throw new Error('Invalid public key');
const x = Buffer.from(jwkPublic.x, 'base64url');
const y = Buffer.from(jwkPublic.y, 'base64url');
const publicBytes = Buffer.concat([Buffer.from([0x04]), x, y]);
const secret = ecdh.computeSecret(publicBytes);
const secretBuf = Buffer.from(secret);
// If length is null, return full secret
if (length === null) {
return secretBuf.buffer;
}
// If length is specified, truncate
const byteLength = Math.ceil(length / 8);
if (secretBuf.byteLength >= byteLength) {
return secretBuf.buffer.slice(secretBuf.byteOffset, secretBuf.byteOffset + byteLength);
}
throw new Error('Derived key is shorter than requested length');
}
//# sourceMappingURL=ec.js.map