UNPKG

react-native-quick-crypto

Version:

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

403 lines (365 loc) 10.4 kB
import { NativeQuickCrypto } from './NativeQuickCrypto/NativeQuickCrypto'; import { lazyDOMException, type BufferLike, hasAnyNotIn, validateKeyOps, validateByteLength, validateMaxBufferLength, bufferLikeToArrayBuffer, } from './Utils'; import { type ImportFormat, type SubtleAlgorithm, type KeyUsage, CryptoKey, createSecretKey, SecretKeyObject, type JWK, type AESAlgorithm, CipherOrWrapMode, type EncryptDecryptParams, type AesGcmParams, type AesCbcParams, type AesCtrParams, type TagLength, type AESLength, type AesKeyGenParams, } from './keys'; import { generateKeyPromise } from './keygen'; // needs to match the values in cpp/webcrypto/crypto_aes.{h,cpp} export enum AESKeyVariant { AES_CTR_128, AES_CTR_192, AES_CTR_256, AES_CBC_128, AES_CBC_192, AES_CBC_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, AES_KW_128, AES_KW_192, AES_KW_256, } const kMaxCounterLength = 128; const kTagLengths: TagLength[] = [32, 64, 96, 104, 112, 120, 128]; export const kAesKeyLengths = [128, 192, 256]; export const getAlgorithmName = (name: string, length?: number) => { if (length === undefined) throw lazyDOMException( `Invalid algorithm length: ${length}`, 'SyntaxError', ); switch (name) { case 'AES-CBC': return `A${length}CBC`; case 'AES-CTR': return `A${length}CTR`; case 'AES-GCM': return `A${length}GCM`; case 'AES-KW': return `A${length}KW`; default: throw lazyDOMException(`invalid algorithm name: ${name}`, 'SyntaxError'); } }; function validateKeyLength(length?: number) { if (length !== 128 && length !== 192 && length !== 256) throw lazyDOMException(`Invalid key length: ${length}`, 'DataError'); } function getVariant(name: AESAlgorithm, length: AESLength): AESKeyVariant { switch (name) { case 'AES-CBC': switch (length) { case 128: return AESKeyVariant.AES_CBC_128; case 192: return AESKeyVariant.AES_CBC_192; case 256: return AESKeyVariant.AES_CBC_256; } // @ts-expect-error unreachable code break; case 'AES-CTR': switch (length) { case 128: return AESKeyVariant.AES_CTR_128; case 192: return AESKeyVariant.AES_CTR_192; case 256: return AESKeyVariant.AES_CTR_256; } // @ts-expect-error unreachable code break; case 'AES-GCM': switch (length) { case 128: return AESKeyVariant.AES_GCM_128; case 192: return AESKeyVariant.AES_GCM_192; case 256: return AESKeyVariant.AES_GCM_256; } // @ts-expect-error unreachable code break; case 'AES-KW': switch (length) { case 128: return AESKeyVariant.AES_KW_128; case 192: return AESKeyVariant.AES_KW_192; case 256: return AESKeyVariant.AES_KW_256; } // @ts-expect-error unreachable code break; } // @ts-expect-error unreachable code throw lazyDOMException( `Error getting variant ${name} at length: ${length}`, 'DataError', ); } function asyncAesCtrCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, { counter, length }: AesCtrParams, ): Promise<ArrayBuffer> { validateByteLength(counter, 'algorithm.counter', 16); // The length must specify an integer between 1 and 128. While // there is no default, this should typically be 64. if (length === 0 || length > kMaxCounterLength) { throw lazyDOMException( 'AES-CTR algorithm.length must be between 1 and 128', 'OperationError', ); } return NativeQuickCrypto.webcrypto.aesCipher( mode, key.keyObject.handle, data, getVariant('AES-CTR', key.algorithm.length as AESLength), bufferLikeToArrayBuffer(counter), length, ); } function asyncAesCbcCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, { iv }: AesCbcParams, ): Promise<ArrayBuffer> { validateByteLength(iv, 'algorithm.iv', 16); return NativeQuickCrypto.webcrypto.aesCipher( mode, key.keyObject.handle, data, getVariant('AES-CBC', key.algorithm.length as AESLength), bufferLikeToArrayBuffer(iv), ); } // function asyncAesKwCipher( // mode: CipherOrWrapMode, // key: CryptoKey, // data: BufferLike // ): Promise<ArrayBuffer> { // return NativeQuickCrypto.webcrypto.aesCipher( // mode, // key.keyObject.handle, // data, // getVariant('AES-KW', key.algorithm.length) // ); // } function asyncAesGcmCipher( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, { iv, additionalData, tagLength = 128 }: AesGcmParams, ) { if (!kTagLengths.includes(tagLength)) { throw lazyDOMException( `${tagLength} is not a valid AES-GCM tag length`, 'OperationError', ); } validateMaxBufferLength(iv, 'algorithm.iv'); if (additionalData !== undefined) { validateMaxBufferLength(additionalData, 'algorithm.additionalData'); } const tagByteLength = Math.floor(tagLength / 8); let length: number | undefined; let tag = new ArrayBuffer(0); switch (mode) { case CipherOrWrapMode.kWebCryptoCipherDecrypt: { // const slice = ArrayBuffer.isView(data) // ? DataView.prototype.buffer.slice // : ArrayBuffer.prototype.slice; tag = data.slice(-tagByteLength); // Refs: https://www.w3.org/TR/WebCryptoAPI/#aes-gcm-operations // // > If *plaintext* has a length less than *tagLength* bits, then `throw` // > an `OperationError`. if (tagByteLength > tag.byteLength) { throw lazyDOMException( 'The provided data is too small.', 'OperationError', ); } data = data.slice(0, -tagByteLength); break; } case CipherOrWrapMode.kWebCryptoCipherEncrypt: length = tagByteLength; break; } return NativeQuickCrypto.webcrypto.aesCipher( mode, key.keyObject.handle, data, getVariant('AES-GCM', key.algorithm.length as AESLength), bufferLikeToArrayBuffer(iv), length, bufferLikeToArrayBuffer(tag), bufferLikeToArrayBuffer(additionalData || new ArrayBuffer(0)), ); } export const aesCipher = ( mode: CipherOrWrapMode, key: CryptoKey, data: ArrayBuffer, algorithm: EncryptDecryptParams, // | WrapUnwrapParams ): Promise<ArrayBuffer> => { switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm); // case 'AES-KW': // return asyncAesKwCipher(mode, key, data); } throw new Error(`aesCipher: Unknown algorithm ${algorithm.name}`); }; export const aesGenerateKey = async ( algorithm: AesKeyGenParams, extractable: boolean, keyUsages: KeyUsage[], ): Promise<CryptoKey> => { const { name, length } = algorithm; if (!name) { throw lazyDOMException('Algorithm name is undefined', 'SyntaxError'); } if (!kAesKeyLengths.includes(length)) { throw lazyDOMException( 'AES key length must be 128, 192, or 256 bits', 'OperationError', ); } const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') { checkUsages.push('encrypt', 'decrypt'); } // const usagesSet = new SafeSet(keyUsages); if (hasAnyNotIn(keyUsages, checkUsages)) { throw lazyDOMException( `Unsupported key usage for an AES key: ${keyUsages}`, 'SyntaxError', ); } const [err, key] = await generateKeyPromise('aes', { length }); if (err) { throw lazyDOMException( `aesGenerateKey (generateKeyPromise) failed: [${err.message}]`, { name: 'OperationError', cause: err, }, ); } return new CryptoKey( key as SecretKeyObject, { name, length }, Array.from(keyUsages), extractable, ); }; export const aesImportKey = async ( algorithm: SubtleAlgorithm, format: ImportFormat, keyData: BufferLike | JWK, extractable: boolean, keyUsages: KeyUsage[], ): Promise<CryptoKey> => { const { name } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; if (name !== 'AES-KW') { checkUsages.push('encrypt', 'decrypt'); } // const usagesSet = new SafeSet(keyUsages); if (hasAnyNotIn(keyUsages, checkUsages)) { throw lazyDOMException( 'Unsupported key usage for an AES key', 'SyntaxError', ); } let keyObject: SecretKeyObject; let length: number | undefined; switch (format) { case 'raw': { const data = bufferLikeToArrayBuffer(keyData as BufferLike); validateKeyLength(data.byteLength * 8); keyObject = createSecretKey(data); break; } case 'jwk': { const data = keyData as JWK; if (!data.kty) throw lazyDOMException('Invalid keyData', 'DataError'); if (data.kty !== 'oct') throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); if ( keyUsages.length > 0 && data.use !== undefined && data.use !== 'enc' ) { throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); } validateKeyOps(data.key_ops, keyUsages); if ( data.ext !== undefined && data.ext === false && extractable === true ) { throw lazyDOMException( 'JWK "ext" Parameter and extractable mismatch', 'DataError', ); } const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle(); handle.initJwk(data); ({ length } = handle.keyDetail()); validateKeyLength(length); if (data.alg !== undefined) { if (data.alg !== getAlgorithmName(algorithm.name, length)) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError', ); } keyObject = new SecretKeyObject(handle); break; } default: throw lazyDOMException( `Unable to import AES key with format ${format}`, 'NotSupportedError', ); } if (length === undefined) { ({ length } = keyObject.handle.keyDetail()); validateKeyLength(length); } return new CryptoKey(keyObject, { name, length }, keyUsages, extractable); };