UNPKG

react-native-quick-crypto

Version:

A fast implementation of Node's `crypto` module written in C/C++ JSI

433 lines (419 loc) 14.8 kB
"use strict"; import { NitroModules } from 'react-native-nitro-modules'; import { Buffer } from '@craftzdog/react-native-buffer'; import { CryptoKey, KeyObject, PublicKeyObject, PrivateKeyObject as PrivateKeyObjectClass } from './keys/classes'; import { binaryLikeToArrayBuffer as toAB, hasAnyNotIn, lazyDOMException, getUsagesUnion, KFormatType, KeyEncoding } from './utils'; import { ECDH } from './ecdh'; function coerceToNumeric(value) { return typeof value === 'number' ? value : -1; } export class Ed { constructor(type, config) { this.type = type; this.config = config; this.native = NitroModules.createHybridObject('EdKeyPair'); this.native.setCurve(type); } /** * Computes the Diffie-Hellman secret based on a privateKey and a publicKey. * Both keys must have the same asymmetricKeyType, which must be one of 'dh' * (for Diffie-Hellman), 'ec', 'x448', or 'x25519' (for ECDH). * * @api nodejs/node * * @param options `{ privateKey, publicKey }`, both of which are `KeyObject`s * @param callback optional `(err, secret) => void` * @returns `Buffer` if no callback, or `void` if callback is provided */ diffieHellman(options, callback) { // extract raw key bytes from KeyObject instances const privKeyObj = options.privateKey; const pubKeyObj = options.publicKey; const privateKey = privKeyObj.handle.exportKey(); const publicKey = pubKeyObj.handle.exportKey(); try { const ret = this.native.diffieHellman(privateKey, publicKey); if (!ret) { throw new Error('No secret'); } if (callback) { callback(null, Buffer.from(ret)); } else { return Buffer.from(ret); } } catch (e) { const err = e; if (callback) { callback(err, undefined); } else { throw err; } } } async generateKeyPair() { await this.native.generateKeyPair(coerceToNumeric(this.config.publicFormat), coerceToNumeric(this.config.publicType), coerceToNumeric(this.config.privateFormat), coerceToNumeric(this.config.privateType), this.config.cipher, this.config.passphrase); } generateKeyPairSync() { this.native.generateKeyPairSync(coerceToNumeric(this.config.publicFormat), coerceToNumeric(this.config.publicType), coerceToNumeric(this.config.privateFormat), coerceToNumeric(this.config.privateType), this.config.cipher, this.config.passphrase); } getPublicKey() { return this.native.getPublicKey(); } getPrivateKey() { return this.native.getPrivateKey(); } /** * Computes the Diffie-Hellman shared secret based on a privateKey and a * publicKey for key exchange * * @api \@paulmillr/noble-curves/ed25519 * * @param privateKey * @param publicKey * @returns shared secret key */ getSharedSecret(privateKey, publicKey) { return this.native.diffieHellman(toAB(privateKey), toAB(publicKey)); } async sign(message, key) { return key ? this.native.sign(toAB(message), toAB(key)) : this.native.sign(toAB(message)); } signSync(message, key) { return key ? this.native.signSync(toAB(message), toAB(key)) : this.native.signSync(toAB(message)); } async verify(signature, message, key) { return key ? this.native.verify(toAB(signature), toAB(message), toAB(key)) : this.native.verify(toAB(signature), toAB(message)); } verifySync(signature, message, key) { return key ? this.native.verifySync(toAB(signature), toAB(message), toAB(key)) : this.native.verifySync(toAB(signature), toAB(message)); } } // Node API export function diffieHellman(options, callback) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object'); } if (callback !== undefined && typeof callback !== 'function') { throw new TypeError('callback must be a function'); } const resolvedOptions = { publicKey: resolveDhKeyInput(options.publicKey, 'publicKey'), privateKey: resolveDhKeyInput(options.privateKey, 'privateKey') }; checkDiffieHellmanOptions(resolvedOptions); const privateKey = resolvedOptions.privateKey; const keyType = privateKey.asymmetricKeyType; if (keyType === 'ec') { return ecDiffieHellman(resolvedOptions, callback); } if (keyType === 'dh') { throw new Error('crypto.diffieHellman with DH KeyObjects is not supported yet — use the DiffieHellman class for now'); } const type = keyType; const ed = new Ed(type, {}); return ed.diffieHellman(resolvedOptions, callback); } function isRawKeyInput(value) { if (!value || typeof value !== 'object') return false; const obj = value; return obj.format === 'raw-public' || obj.format === 'raw-private' || obj.format === 'raw-seed'; } function resolveDhKeyInput(input, name) { if (isRawKeyInput(input)) { const expectedKeyType = name === 'publicKey' ? 'public' : 'private'; if (input.format === 'raw-public' && expectedKeyType !== 'public') { throw new Error(`Invalid format 'raw-public' for ${name}`); } if ((input.format === 'raw-private' || input.format === 'raw-seed') && expectedKeyType !== 'private') { throw new Error(`Invalid format '${input.format}' for ${name}`); } if (input.asymmetricKeyType === 'ec' && !input.namedCurve) { throw new Error(`namedCurve is required for EC raw key in ${name}`); } const handle = NitroModules.createHybridObject('KeyObjectHandle'); const keyData = toAB(input.key); if (input.format === 'raw-public') { handle.initRawPublic(input.asymmetricKeyType, keyData, input.namedCurve); return new PublicKeyObject(handle); } if (input.format === 'raw-seed') { handle.initRawSeed(input.asymmetricKeyType, keyData); return new PrivateKeyObjectClass(handle); } handle.initRawPrivate(input.asymmetricKeyType, keyData, input.namedCurve); return new PrivateKeyObjectClass(handle); } return input; } function ed_createKeyObjects(ed) { const publicKeyData = ed.getPublicKey(); const privateKeyData = ed.getPrivateKey(); const pub = KeyObject.createKeyObject('public', publicKeyData, KFormatType.DER, KeyEncoding.SPKI); const priv = KeyObject.createKeyObject('private', privateKeyData, KFormatType.DER, KeyEncoding.PKCS8); return { pub, priv }; } // Node API function ed_formatKeyPairOutput(ed, encoding) { const { publicFormat, privateFormat, cipher, passphrase } = encoding; const { pub, priv } = ed_createKeyObjects(ed); let publicKey; let privateKey; if (publicFormat == null || publicFormat === -1) { publicKey = pub; } else if (publicFormat === 'raw-public') { publicKey = Buffer.from(pub.handle.exportRawPublic()); } else { const format = publicFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; const exported = pub.handle.exportKey(format, KeyEncoding.SPKI); if (format === KFormatType.PEM) { publicKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); } else { publicKey = exported; } } if (privateFormat == null || privateFormat === -1) { privateKey = priv; } else if (privateFormat === 'raw-private') { privateKey = Buffer.from(priv.handle.exportRawPrivate()); } else if (privateFormat === 'raw-seed') { privateKey = Buffer.from(priv.handle.exportRawSeed()); } else { const format = privateFormat === KFormatType.PEM ? KFormatType.PEM : KFormatType.DER; const exported = priv.handle.exportKey(format, KeyEncoding.PKCS8, cipher, passphrase); if (format === KFormatType.PEM) { privateKey = Buffer.from(new Uint8Array(exported)).toString('utf-8'); } else { privateKey = exported; } } return { publicKey, privateKey }; } export function ed_generateKeyPair(isAsync, type, encoding, callback) { const derConfig = { ...encoding, publicFormat: KFormatType.DER, publicType: KeyEncoding.SPKI, privateFormat: KFormatType.DER, privateType: KeyEncoding.PKCS8 }; const ed = new Ed(type, derConfig); if (isAsync) { if (!callback) { throw new Error('A callback is required for async key generation.'); } ed.generateKeyPair().then(() => { const { publicKey, privateKey } = ed_formatKeyPairOutput(ed, encoding); callback(undefined, publicKey, privateKey); }).catch(err => { callback(err, undefined, undefined); }); return; } let err; try { ed.generateKeyPairSync(); } catch (e) { err = e instanceof Error ? e : new Error(String(e)); } const { publicKey, privateKey } = err ? { publicKey: undefined, privateKey: undefined } : ed_formatKeyPairOutput(ed, encoding); if (callback) { callback(err, publicKey, privateKey); return; } return [err, publicKey, privateKey]; } function ecDiffieHellman(options, callback) { const privateKey = options.privateKey; const publicKey = options.publicKey; const curveName = privateKey.namedCurve; if (!curveName) { throw new Error('Unable to determine EC curve name from private key'); } const ecdh = new ECDH(curveName); const jwkPrivate = privateKey.handle.exportJwk({}, false); if (!jwkPrivate.d) throw new Error('Invalid private key'); ecdh.setPrivateKey(Buffer.from(jwkPrivate.d, 'base64url')); const jwkPublic = publicKey.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]); try { const secret = ecdh.computeSecret(publicBytes); if (callback) { callback(null, secret); } else { return secret; } } catch (e) { const err = e; if (callback) { callback(err, undefined); } else { throw err; } } } function checkDiffieHellmanOptions(options) { const { privateKey, publicKey } = options; // Check if keys are KeyObject instances if (!privateKey || typeof privateKey !== 'object' || !('type' in privateKey)) { throw new Error('privateKey must be a KeyObject'); } if (!publicKey || typeof publicKey !== 'object' || !('type' in publicKey)) { throw new Error('publicKey must be a KeyObject'); } // type checks if (privateKey.type !== 'private') { throw new Error('privateKey must be a private KeyObject'); } if (publicKey.type !== 'public') { throw new Error('publicKey must be a public KeyObject'); } // For asymmetric keys, check if they have the asymmetricKeyType property const privateKeyAsym = privateKey; const publicKeyAsym = publicKey; // key types must match if (privateKeyAsym.asymmetricKeyType && publicKeyAsym.asymmetricKeyType && privateKeyAsym.asymmetricKeyType !== publicKeyAsym.asymmetricKeyType) { throw new Error('Keys must be asymmetric and their types must match'); } switch (privateKeyAsym.asymmetricKeyType) { // case 'dh': // TODO: uncomment when implemented case 'ec': { const privateCurve = privateKeyAsym.namedCurve; const publicCurve = publicKeyAsym.namedCurve; if (privateCurve && publicCurve && privateCurve !== publicCurve) { throw new Error('Private and public key curves do not match'); } break; } case 'x25519': case 'x448': break; default: throw new Error(`Unknown curve type: ${privateKeyAsym.asymmetricKeyType}`); } } export async function ed_generateKeyPairWebCrypto(type, extractable, keyUsages) { if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) { throw lazyDOMException(`Unsupported key usage for ${type}`, 'SyntaxError'); } const publicUsages = getUsagesUnion(keyUsages, 'verify'); const privateUsages = getUsagesUnion(keyUsages, 'sign'); if (privateUsages.length === 0) { throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); } // Request DER-encoded SPKI for public key, PKCS8 for private key const config = { publicFormat: KFormatType.DER, publicType: KeyEncoding.SPKI, privateFormat: KFormatType.DER, privateType: KeyEncoding.PKCS8 }; const ed = new Ed(type, config); await ed.generateKeyPair(); const algorithmName = type === 'ed25519' ? 'Ed25519' : 'Ed448'; const { pub, priv } = ed_createKeyObjects(ed); const publicKey = new CryptoKey(pub, { name: algorithmName }, publicUsages, true); const privateKey = new CryptoKey(priv, { name: algorithmName }, privateUsages, extractable); return { publicKey, privateKey }; } export async function x_generateKeyPairWebCrypto(type, extractable, keyUsages) { if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw lazyDOMException(`Unsupported key usage for ${type}`, 'SyntaxError'); } const publicUsages = getUsagesUnion(keyUsages); const privateUsages = getUsagesUnion(keyUsages, 'deriveKey', 'deriveBits'); if (privateUsages.length === 0) { throw lazyDOMException('Usages cannot be empty', 'SyntaxError'); } // Request DER-encoded SPKI for public key, PKCS8 for private key const config = { publicFormat: KFormatType.DER, publicType: KeyEncoding.SPKI, privateFormat: KFormatType.DER, privateType: KeyEncoding.PKCS8 }; const ed = new Ed(type, config); await ed.generateKeyPair(); const algorithmName = type === 'x25519' ? 'X25519' : 'X448'; const { pub, priv } = ed_createKeyObjects(ed); const publicKey = new CryptoKey(pub, { name: algorithmName }, publicUsages, true); const privateKey = new CryptoKey(priv, { name: algorithmName }, privateUsages, extractable); return { publicKey, privateKey }; } export function xDeriveBits(algorithm, baseKey, length) { const publicKey = algorithm.public; if (!publicKey) { throw new Error('Public key is required for X25519/X448 derivation'); } if (baseKey.algorithm.name !== publicKey.algorithm.name) { throw new Error('Keys must be of the same algorithm'); } const type = baseKey.algorithm.name.toLowerCase(); const ed = new Ed(type, {}); // Export raw keys const privateKeyBytes = baseKey.keyObject.handle.exportKey(); const publicKeyBytes = publicKey.keyObject.handle.exportKey(); const privateKeyTyped = new Uint8Array(privateKeyBytes); const publicKeyTyped = new Uint8Array(publicKeyBytes); const secret = ed.getSharedSecret(privateKeyTyped, publicKeyTyped); // If length is null, return the full secret if (length === null) { return secret; } // If length is specified, truncate const byteLength = Math.ceil(length / 8); if (secret.byteLength >= byteLength) { return secret.slice(0, byteLength); } throw new Error('Derived key is shorter than requested length'); } //# sourceMappingURL=ed.js.map