UNPKG

react-native-quick-crypto

Version:

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

648 lines (611 loc) 17.5 kB
import { Buffer as SBuffer } from 'safe-buffer'; import { type ImportFormat, type SubtleAlgorithm, type KeyUsage, CryptoKey, KWebCryptoKeyFormat, createSecretKey, type AnyAlgorithm, type JWK, type CryptoKeyPair, CipherOrWrapMode, type EncryptDecryptParams, type AesKeyGenParams, } from './keys'; import { hasAnyNotIn, type BufferLike, type BinaryLike, lazyDOMException, normalizeHashName, HashContext, validateMaxBufferLength, bufferLikeToArrayBuffer, } from './Utils'; import { ecImportKey, ecExportKey, ecGenerateKey, ecdsaSignVerify } from './ec'; import { pbkdf2DeriveBits } from './pbkdf2'; import { asyncDigest } from './Hash'; import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName, } from './aes'; import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa'; import { normalizeAlgorithm, type Operation } from './Algorithms'; import { hmacImportKey } from './mac'; const exportKeySpki = async ( key: CryptoKey, ): Promise<ArrayBuffer | unknown> => { switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': if (key.type === 'public') { return rsaExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); } break; case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'public') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); } break; // case 'Ed25519': // // Fall through // case 'Ed448': // // Fall through // case 'X25519': // // Fall through // case 'X448': // if (key.type === 'public') { // return cfrgExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); // } // break; } throw new Error( `Unable to export a spki ${key.algorithm.name} ${key.type} key`, ); }; const exportKeyPkcs8 = async ( key: CryptoKey, ): Promise<ArrayBuffer | unknown> => { switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': if (key.type === 'private') { return rsaExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); } break; case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'private') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); } break; // case 'Ed25519': // // Fall through // case 'Ed448': // // Fall through // case 'X25519': // // Fall through // case 'X448': // if (key.type === 'private') { // return cfrgExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8); // } // break; } throw new Error( `Unable to export a pkcs8 ${key.algorithm.name} ${key.type} key`, ); }; const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => { switch (key.algorithm.name) { case 'ECDSA': // Fall through case 'ECDH': if (key.type === 'public') { return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw); } break; // case 'Ed25519': // // Fall through // case 'Ed448': // // Fall through // case 'X25519': // // Fall through // case 'X448': // if (key.type === 'public') { // return require('internal/crypto/cfrg') // .cfrgExportKey(key, kWebCryptoKeyFormatRaw); // } // break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'HMAC': return key.keyObject.export(); } throw lazyDOMException( `Unable to export a raw ${key.algorithm.name} ${key.type} key`, 'InvalidAccessError', ); }; const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => { const jwk = key.keyObject.handle.exportJwk( { key_ops: key.usages, ext: key.extractable, }, true, ); switch (key.algorithm.name) { case 'RSASSA-PKCS1-v1_5': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsa); return jwk; case 'RSA-PSS': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsaPss); return jwk; case 'RSA-OAEP': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkRsaOaep); return jwk; case 'HMAC': jwk.alg = normalizeHashName(key.algorithm.hash, HashContext.JwkHmac); return jwk; case 'ECDSA': // Fall through case 'ECDH': jwk.crv ||= key.algorithm.namedCurve; return jwk; // case 'X25519': // // Fall through // case 'X448': // jwk.crv ||= key.algorithm.name; // return jwk; // case 'Ed25519': // // Fall through // case 'Ed448': // jwk.crv ||= key.algorithm.name; // return jwk; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': jwk.alg = getAlgorithmName(key.algorithm.name, key.algorithm.length); return jwk; // case 'HMAC': // jwk.alg = normalizeHashName( // key.algorithm.hash.name, // normalizeHashName.kContextJwkHmac); // return jwk; default: // Fall through } throw lazyDOMException( `JWK export not yet supported: ${key.algorithm.name}`, 'NotSupportedError', ); }; const importGenericSecretKey = async ( { name, length }: SubtleAlgorithm, format: ImportFormat, keyData: BufferLike | BinaryLike, extractable: boolean, keyUsages: KeyUsage[], ): Promise<CryptoKey> => { if (extractable) { throw new Error(`${name} keys are not extractable`); } if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } switch (format) { case 'raw': { if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) { throw new Error(`Unsupported key usage for a ${name} key`); } const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8; // The Web Crypto spec allows for key lengths that are not multiples of // 8. We don't. Our check here is stricter than that defined by the spec // in that we require that algorithm.length match keyData.length * 8 if // algorithm.length is specified. if (length !== undefined && length !== checkLength) { throw new Error('Invalid key length'); } const keyObject = createSecretKey(keyData as BinaryLike); return new CryptoKey(keyObject, { name }, keyUsages, false); } } throw new Error(`Unable to import ${name} key with format ${format}`); }; // const checkCryptoKeyUsages = (key: CryptoKey) => { // if ( // (key.type === 'secret' || key.type === 'private') && // key.usages.length === 0 // ) { // throw lazyDOMException( // 'Usages cannot be empty when creating a key.', // 'SyntaxError' // ); // } // }; const checkCryptoKeyPairUsages = (pair: CryptoKeyPair) => { if ( !(pair.privateKey instanceof Buffer) && pair.privateKey && Object.prototype.hasOwnProperty.call(pair.privateKey, 'keyUsages') ) { const priv = pair.privateKey as CryptoKey; if (priv.usages.length > 0) { return; } } console.log(pair.privateKey); throw lazyDOMException( 'Usages cannot be empty when creating a key.', 'SyntaxError', ); }; const signVerify = ( algorithm: SubtleAlgorithm, key: CryptoKey, data: BufferLike, signature?: BufferLike, ): ArrayBuffer | boolean => { const usage: Operation = signature === undefined ? 'sign' : 'verify'; algorithm = normalizeAlgorithm(algorithm, usage); if (!key.usages.includes(usage) || algorithm.name !== key.algorithm.name) { throw lazyDOMException( `Unable to use this key to ${usage}`, 'InvalidAccessError', ); } switch (algorithm.name) { // case 'RSA-PSS': // // Fall through // case 'RSASSA-PKCS1-v1_5': // return require('internal/crypto/rsa').rsaSignVerify( // key, // data, // algorithm, // signature // ); case 'ECDSA': return ecdsaSignVerify(key, data, algorithm, signature); // case 'Ed25519': // // Fall through // case 'Ed448': // return require('internal/crypto/cfrg').eddsaSignVerify( // key, // data, // algorithm, // signature // ); // case 'HMAC': // return require('internal/crypto/mac').hmacSignVerify( // key, // data, // algorithm, // signature // ); } throw lazyDOMException( `Unrecognized algorithm name '${algorithm}' for '${usage}'`, 'NotSupportedError', ); }; const cipherOrWrap = async ( mode: CipherOrWrapMode, algorithm: EncryptDecryptParams, // | WrapUnwrapParams, key: CryptoKey, data: ArrayBuffer, op: Operation, ): Promise<ArrayBuffer> => { // We use a Node.js style error here instead of a DOMException because // the WebCrypto spec is not specific what kind of error is to be thrown // in this case. Both Firefox and Chrome throw simple TypeErrors here. // The key algorithm and cipher algorithm must match, and the // key must have the proper usage. if ( key.algorithm.name !== algorithm.name || !key.usages.includes(op as KeyUsage) ) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError', ); } // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. validateMaxBufferLength(data, 'data'); switch (algorithm.name) { case 'RSA-OAEP': return rsaCipher(mode, key, data, algorithm); case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': return aesCipher(mode, key, data, algorithm); // case 'AES-KW': // if (op === 'wrapKey' || op === 'unwrapKey') { // return aesCipher(mode, key, data, algorithm); // } } // @ts-expect-error unreachable code throw lazyDOMException( `Unrecognized algorithm name '${algorithm}' for '${op}'`, 'NotSupportedError', ); }; export class Subtle { async decrypt( algorithm: EncryptDecryptParams, key: CryptoKey, data: BufferLike, ): Promise<ArrayBuffer> { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); return cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedAlgorithm as EncryptDecryptParams, key, bufferLikeToArrayBuffer(data), 'decrypt', ); } async digest( algorithm: SubtleAlgorithm | AnyAlgorithm, data: BufferLike, ): Promise<ArrayBuffer> { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); return asyncDigest(normalizedAlgorithm, data); } async deriveBits( algorithm: SubtleAlgorithm, baseKey: CryptoKey, length: number, ): Promise<ArrayBuffer> { if (!baseKey.keyUsages.includes('deriveBits')) { throw new Error('baseKey does not have deriveBits usage'); } if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch'); switch (algorithm.name) { // case 'X25519': // // Fall through // case 'X448': // // Fall through // case 'ECDH': // return require('internal/crypto/diffiehellman') // .ecdhDeriveBits(algorithm, baseKey, length); // case 'HKDF': // return require('internal/crypto/hkdf') // .hkdfDeriveBits(algorithm, baseKey, length); case 'PBKDF2': return pbkdf2DeriveBits(algorithm, baseKey, length); } throw new Error( `'subtle.deriveBits()' for ${algorithm.name} is not implemented.`, ); } async encrypt( algorithm: EncryptDecryptParams, key: CryptoKey, data: BufferLike, ): Promise<ArrayBuffer> { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); return cipherOrWrap( CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm as EncryptDecryptParams, key, bufferLikeToArrayBuffer(data), 'encrypt', ); } async exportKey( format: ImportFormat, key: CryptoKey, ): Promise<ArrayBuffer | JWK> { if (!key.extractable) throw new Error('key is not extractable'); switch (format) { case 'spki': return (await exportKeySpki(key)) as ArrayBuffer; case 'pkcs8': return (await exportKeyPkcs8(key)) as ArrayBuffer; case 'jwk': return exportKeyJWK(key) as JWK; case 'raw': return exportKeyRaw(key) as ArrayBuffer; } } async generateKey( algorithm: SubtleAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise<CryptoKey | CryptoKeyPair> { algorithm = normalizeAlgorithm(algorithm, 'generateKey'); let result: CryptoKey | CryptoKeyPair; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = await rsaKeyGenerate(algorithm, extractable, keyUsages); break; // case 'Ed25519': // // Fall through // case 'Ed448': // // Fall through // case 'X25519': // // Fall through // case 'X448': // resultType = 'CryptoKeyPair'; // result = await cfrgGenerateKey(algorithm, extractable, keyUsages); // break; case 'ECDSA': // Fall through case 'ECDH': result = await ecGenerateKey(algorithm, extractable, keyUsages); checkCryptoKeyPairUsages(result); break; // case 'HMAC': // result = await hmacGenerateKey(algorithm, extractable, keyUsages); // break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': result = await aesGenerateKey( algorithm as AesKeyGenParams, extractable, keyUsages, ); break; default: throw new Error( `'subtle.generateKey()' is not implemented for ${algorithm.name}. Unrecognized algorithm name`, ); } return result; } async importKey( format: ImportFormat, data: BufferLike | BinaryLike | JWK, algorithm: SubtleAlgorithm | AnyAlgorithm, extractable: boolean, keyUsages: KeyUsage[], ): Promise<CryptoKey> { const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); let result: CryptoKey; switch (normalizedAlgorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = rsaImportKey( format, data as BufferLike | JWK, normalizedAlgorithm, extractable, keyUsages, ); break; case 'ECDSA': // Fall through case 'ECDH': result = ecImportKey( format, data, normalizedAlgorithm, extractable, keyUsages, ); break; // case 'Ed25519': // // Fall through // case 'Ed448': // // Fall through // case 'X25519': // // Fall through // case 'X448': // result = await require('internal/crypto/cfrg').cfrgImportKey( // format, // keyData, // algorithm, // extractable, // keyUsages // ); // break; case 'HMAC': result = await hmacImportKey( normalizedAlgorithm, format, data as BufferLike | JWK, extractable, keyUsages, ); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': result = await aesImportKey( normalizedAlgorithm, format, data as BufferLike | JWK, extractable, keyUsages, ); break; // case 'HKDF': // // Fall through case 'PBKDF2': result = await importGenericSecretKey( normalizedAlgorithm, format, data as BufferLike | BinaryLike, extractable, keyUsages, ); break; default: throw new Error( `"subtle.importKey()" is not implemented for ${normalizedAlgorithm.name}`, ); } if ( (result.type === 'secret' || result.type === 'private') && result.usages.length === 0 ) { throw new Error( `Usages cannot be empty when importing a ${result.type} key.`, ); } return result; } async sign( algorithm: SubtleAlgorithm, key: CryptoKey, data: BufferLike, ): Promise<ArrayBuffer> { return signVerify(algorithm, key, data) as ArrayBuffer; } async verify( algorithm: SubtleAlgorithm, key: CryptoKey, signature: BufferLike, data: BufferLike, ): Promise<ArrayBuffer> { return signVerify(algorithm, key, data, signature) as ArrayBuffer; } } export const subtle = new Subtle();