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
JavaScript
;
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