UNPKG

react-native-quick-crypto

Version:

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

629 lines (573 loc) 17.1 kB
import { Buffer as CraftzdogBuffer } from '@craftzdog/react-native-buffer'; import { Buffer as SafeBuffer } from 'safe-buffer'; import type { KeyUsage } from './keys'; import { type CipherKey } from 'crypto'; // @types/node export type BufferLike = | ArrayBuffer | CraftzdogBuffer | SafeBuffer | ArrayBufferView; export type BinaryLike = | string | ArrayBuffer | CraftzdogBuffer | SafeBuffer | TypedArray | DataView; export type BinaryLikeNode = CipherKey | BinaryLike; export type BinaryToTextEncoding = 'base64' | 'base64url' | 'hex' | 'binary'; export type CharacterEncoding = 'utf8' | 'utf-8' | 'utf16le' | 'latin1'; export type LegacyCharacterEncoding = 'ascii' | 'binary' | 'ucs2' | 'ucs-2'; export type Encoding = | BinaryToTextEncoding | CharacterEncoding | LegacyCharacterEncoding; // TODO(osp) should buffer be part of the Encoding type? export type CipherEncoding = Encoding | 'buffer'; // These are for shortcomings in @types/node // Here we use "*Type" instead of "*Types" like node does. export type CipherType = | 'aes128' | 'aes192' | 'aes256' | CipherCBCType | CipherCFBType | CipherCTRType | CipherDESType | CipherECBType | CipherGCMType | CipherOFBType; export type CipherCBCType = 'aes-128-cbc' | 'aes-192-cbc' | 'aes-256-cbc'; export type CipherCFBType = | 'aes-128-cfb' | 'aes-192-cfb' | 'aes-256-cfb' | 'aes-128-cfb1' | 'aes-192-cfb1' | 'aes-256-cfb1' | 'aes-128-cfb8' | 'aes-192-cfb8' | 'aes-256-cfb8'; export type CipherCTRType = 'aes-128-ctr' | 'aes-192-ctr' | 'aes-256-ctr'; export type CipherDESType = | 'des' | 'des3' | 'des-cbc' | 'des-ecb' | 'des-ede' | 'des-ede-cbc' | 'des-ede3' | 'des-ede3-cbc'; export type CipherECBType = 'aes-128-ecb' | 'aes-192-ecb' | 'aes-256-ecb'; export type CipherGCMType = 'aes-128-gcm' | 'aes-192-gcm' | 'aes-256-gcm'; export type CipherOFBType = 'aes-128-ofb' | 'aes-192-ofb' | 'aes-256-ofb'; export type TypedArray = | Uint8Array | Uint8ClampedArray | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; export type ABV = TypedArray | DataView | ArrayBufferLike | CraftzdogBuffer; type DOMName = | string | { name: string; cause: unknown; }; // Mimics node behavior for default global encoding let defaultEncoding: CipherEncoding = 'buffer'; export function setDefaultEncoding(encoding: CipherEncoding) { defaultEncoding = encoding; } export function getDefaultEncoding(): CipherEncoding { return defaultEncoding; } export const kEmptyObject = Object.freeze(Object.create(null)); // Should be used by Cipher (or any other module that requires valid encodings) // function slowCases(enc: string) { // switch (enc.length) { // case 4: // if (enc === 'UTF8') return 'utf8'; // if (enc === 'ucs2' || enc === 'UCS2') return 'utf16le'; // enc = `${enc}`.toLowerCase(); // if (enc === 'utf8') return 'utf8'; // if (enc === 'ucs2') return 'utf16le'; // break; // case 3: // if (enc === 'hex' || enc === 'HEX' || `${enc}`.toLowerCase() === 'hex') // return 'hex'; // break; // case 5: // if (enc === 'ascii') return 'ascii'; // if (enc === 'ucs-2') return 'utf16le'; // if (enc === 'UTF-8') return 'utf8'; // if (enc === 'ASCII') return 'ascii'; // if (enc === 'UCS-2') return 'utf16le'; // enc = `${enc}`.toLowerCase(); // if (enc === 'utf-8') return 'utf8'; // if (enc === 'ascii') return 'ascii'; // if (enc === 'ucs-2') return 'utf16le'; // break; // case 6: // if (enc === 'base64') return 'base64'; // if (enc === 'latin1' || enc === 'binary') return 'latin1'; // if (enc === 'BASE64') return 'base64'; // if (enc === 'LATIN1' || enc === 'BINARY') return 'latin1'; // enc = `${enc}`.toLowerCase(); // if (enc === 'base64') return 'base64'; // if (enc === 'latin1' || enc === 'binary') return 'latin1'; // break; // case 7: // if ( // enc === 'utf16le' || // enc === 'UTF16LE' || // `${enc}`.toLowerCase() === 'utf16le' // ) // return 'utf16le'; // break; // case 8: // if ( // enc === 'utf-16le' || // enc === 'UTF-16LE' || // `${enc}`.toLowerCase() === 'utf-16le' // ) // return 'utf16le'; // break; // case 9: // if ( // enc === 'base64url' || // enc === 'BASE64URL' || // `${enc}`.toLowerCase() === 'base64url' // ) // return 'base64url'; // break; // default: // if (enc === '') return 'utf8'; // } // } // // Return undefined if there is no match. // // Move the "slow cases" to a separate function to make sure this function gets // // inlined properly. That prioritizes the common case. // export function normalizeEncoding(enc?: string) { // if (enc == null || enc === 'utf8' || enc === 'utf-8') return 'utf8'; // return slowCases(enc); // } /** * Converts supplied argument to an ArrayBuffer. Note this does not copy the * data so it is faster than toArrayBuffer. Not copying is important for * functions like randomFill which need to be able to write to the underlying * buffer. * @param buf * @returns ArrayBuffer */ export function abvToArrayBuffer(buffer: ABV): ArrayBuffer { if (CraftzdogBuffer.isBuffer(buffer) || ArrayBuffer.isView(buffer)) { return buffer.buffer as ArrayBuffer; } return buffer as ArrayBuffer; } /** * Converts supplied argument to an ArrayBuffer. Note this copies data if the * supplied buffer has the .slice() method, so can be a bit slow. * @param buf * @returns ArrayBuffer */ export function toArrayBuffer( buf: CraftzdogBuffer | SafeBuffer | ArrayBufferView, ): ArrayBuffer { if (CraftzdogBuffer.isBuffer(buf) || ArrayBuffer.isView(buf)) { if (buf?.buffer?.slice) { return buf.buffer.slice( buf.byteOffset, buf.byteOffset + buf.byteLength, ) as ArrayBuffer; } else { throw new Error('This implementation of buffer does not implement slice'); } } const ab = new ArrayBuffer(buf.length); const view = new Uint8Array(ab); for (let i = 0; i < buf.length; ++i) { view[i] = SafeBuffer.isBuffer(buf) ? buf.readUInt8(i) : buf[i]!; } return ab; } export function bufferLikeToArrayBuffer(buf: BufferLike): ArrayBuffer { if (CraftzdogBuffer.isBuffer(buf) || SafeBuffer.isBuffer(buf)) { return toArrayBuffer(buf); } if (ArrayBuffer.isView(buf)) { return toArrayBuffer(buf); } return buf; } export function binaryLikeToArrayBuffer( input: BinaryLikeNode, // CipherKey adds compat with node types encoding: string = 'utf-8', ): ArrayBuffer { // string if (typeof input === 'string') { if (encoding === 'buffer') { throw new Error( 'Cannot create a buffer from a string with a buffer encoding', ); } const buffer = CraftzdogBuffer.from(input, encoding); return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength, ); } // Buffer if (CraftzdogBuffer.isBuffer(input) || SafeBuffer.isBuffer(input)) { return toArrayBuffer(input); } // ArrayBufferView // TODO add further binary types to BinaryLike, UInt8Array and so for have this array as property if (ArrayBuffer.isView(input)) { return toArrayBuffer(input); } // ArrayBuffer if (input instanceof ArrayBuffer) { return input; } // if (!(input instanceof ArrayBuffer)) { // try { // // this is a strange fallback case and input is unknown at this point // const buffer = Buffer.from(input as unknown as string); // return buffer.buffer.slice( // buffer.byteOffset, // buffer.byteOffset + buffer.byteLength // ); // } catch(e: unknown) { // console.log('throwing 1'); // const err = e as Error; // throw new Error(err.message); // } // } // TODO: handle if input is KeyObject? throw new Error('input could not be converted to ArrayBuffer'); } export function ab2str(buf: ArrayBuffer, encoding: string = 'hex') { return CraftzdogBuffer.from(buf).toString(encoding); } export function validateString(str: unknown, name?: string): str is string { const isString = typeof str === 'string'; if (!isString) { throw new Error(`${name} is not a string`); } return isString; } export function validateFunction(f: unknown): boolean { return f !== null && typeof f === 'function'; } export function isStringOrBuffer(val: unknown): val is string | ArrayBuffer { return ( typeof val === 'string' || ArrayBuffer.isView(val) || val instanceof ArrayBuffer ); } export function validateObject<T>( value: unknown, name: string, options?: { allowArray: boolean; allowFunction: boolean; nullable: boolean; } | null, ): value is T { const useDefaultOptions = options == null; const allowArray = useDefaultOptions ? false : options.allowArray; const allowFunction = useDefaultOptions ? false : options.allowFunction; const nullable = useDefaultOptions ? false : options.nullable; if ( (!nullable && value === null) || (!allowArray && Array.isArray(value)) || (typeof value !== 'object' && (!allowFunction || typeof value !== 'function')) ) { throw new Error(`${name} is not a valid object $${value}`); } return true; } export function validateInt32( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, name: string, min = -2147483648, max = 2147483647, ) { // The defaults for min and max correspond to the limits of 32-bit integers. if (typeof value !== 'number') { throw new Error(`Invalid argument - ${name} is not a number: ${value}`); } if (!Number.isInteger(value)) { throw new Error( `Argument out of range - ${name} out of integer range: ${value}`, ); } if (value < min || value > max) { throw new Error( `Invalid argument - ${name} out of range >= ${min} && <= ${max}: ${value}`, ); } } export function validateUint32( value: number, name: string, positive?: boolean, ) { if (typeof value !== 'number') { // throw new ERR_INVALID_ARG_TYPE(name, 'number', value); throw new Error(`Invalid argument - ${name} is not a number: ${value}`); } if (!Number.isInteger(value)) { // throw new ERR_OUT_OF_RANGE(name, 'an integer', value); throw new Error( `Argument out of range - ${name} out of integer range: ${value}`, ); } const min = positive ? 1 : 0; // 2 ** 32 === 4294967296 const max = 4294967295; if (value < min || value > max) { // throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value); throw new Error( `Invalid argument - ${name} out of range >= ${min} && <= ${max}: ${value}`, ); } } export function hasAnyNotIn(set: string[], checks: string[]) { for (const s of set) { if (!checks.includes(s)) { return true; } } return false; } export function lazyDOMException(message: string, domName: DOMName): Error { let cause = ''; if (typeof domName !== 'string') { cause = `\nCaused by: ${domName.cause}`; } return new Error(`[${domName}]: ${message}${cause}`); } // from lib/internal/crypto/util.js // The maximum buffer size that we'll support in the WebCrypto impl const kMaxBufferLength = 2 ** 31 - 1; // // The EC named curves that we currently support via the Web Crypto API. // const kNamedCurveAliases = { // 'P-256': 'prime256v1', // 'P-384': 'secp384r1', // 'P-521': 'secp521r1', // }; // const kAesKeyLengths = [128, 192, 256]; // // These are the only hash algorithms we currently support via // // the Web Crypto API. // const kHashTypes = ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']; export const validateMaxBufferLength = ( data: BinaryLike | BufferLike, name: string, ): void => { const length = typeof data === 'string' || data instanceof SafeBuffer ? data.length : data.byteLength; if (length > kMaxBufferLength) { throw lazyDOMException( `${name} must be less than ${kMaxBufferLength + 1} bits`, 'OperationError', ); } }; export const validateBitLength = ( length: number, name: string, required: boolean = false, ) => { if (length !== undefined || required) { // validateNumber(length, name); if (length < 0) throw new Error(`${name} > 0`); if (length % 8) { throw lazyDOMException( `${name}'s length (${length}) must be a multiple of 8`, 'InvalidArgument', ); } } }; export const validateByteLength = ( buf: BufferLike, name: string, target: number, ) => { if ( (SafeBuffer.isBuffer(buf) && buf.length !== target) || (buf as CraftzdogBuffer | ArrayBuffer | ArrayBufferView).byteLength !== target ) { throw lazyDOMException( `${name} must contain exactly ${target} bytes`, 'OperationError', ); } }; export const getUsagesUnion = (usageSet: KeyUsage[], ...usages: KeyUsage[]) => { const newset: KeyUsage[] = []; for (let n = 0; n < usages.length; n++) { if (!usages[n] || usages[n] === undefined) continue; if (usageSet.includes(usages[n] as KeyUsage)) newset.push(usages[n] as KeyUsage); } return newset; }; const kKeyOps: { [key in KeyUsage]: number; } = { sign: 1, verify: 2, encrypt: 3, decrypt: 4, wrapKey: 5, unwrapKey: 6, deriveKey: 7, deriveBits: 8, }; export const validateKeyOps = ( keyOps: KeyUsage[] | undefined, usagesSet: KeyUsage[], ) => { if (keyOps === undefined) return; if (!Array.isArray(keyOps)) { throw lazyDOMException('keyData.key_ops', 'InvalidArgument'); } let flags = 0; for (let n = 0; n < keyOps.length; n++) { const op: KeyUsage = keyOps[n] as KeyUsage; const op_flag = kKeyOps[op]; // Skipping unknown key ops if (op_flag === undefined) continue; // Have we seen it already? if so, error if (flags & (1 << op_flag)) throw lazyDOMException('Duplicate key operation', 'DataError'); flags |= 1 << op_flag; // TODO(@jasnell): RFC7517 section 4.3 strong recommends validating // key usage combinations. Specifically, it says that unrelated key // ops SHOULD NOT be used together. We're not yet validating that here. } if (usagesSet !== undefined) { for (const use of usagesSet) { if (!keyOps.includes(use)) { throw lazyDOMException( 'Key operations and usage mismatch', 'DataError', ); } } } }; // In WebCrypto, the publicExponent option in RSA is represented as a // WebIDL "BigInteger"... that is, a Uint8Array that allows an arbitrary // number of leading zero bits. Our conventional APIs for reading // an unsigned int from a Buffer are not adequate. The implementation // here is adapted from the chromium implementation here: // https://github.com/chromium/chromium/blob/HEAD/third_party/blink/public/platform/web_crypto_algorithm_params.h, but ported to JavaScript // Returns undefined if the conversion was unsuccessful. export const bigIntArrayToUnsignedInt = ( input: Uint8Array, ): number | undefined => { let result = 0; for (let n = 0; n < input.length; ++n) { const n_reversed = input.length - n - 1; if (n_reversed >= 4 && input[n]) return; // Too large // @ts-expect-error - input[n] is possibly undefined result |= input[n] << (8 * n_reversed); } return result; }; // TODO: these used to be shipped by crypto-browserify in quickcrypto v0.6 // could instead fetch from OpenSSL if needed and handle breaking changes export const getHashes = () => [ 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'md5', 'rmd160', 'sha224WithRSAEncryption', 'RSA-SHA224', 'sha256WithRSAEncryption', 'RSA-SHA256', 'sha384WithRSAEncryption', 'RSA-SHA384', 'sha512WithRSAEncryption', 'RSA-SHA512', 'RSA-SHA1', 'ecdsa-with-SHA1', 'sha256', 'sha224', 'sha384', 'sha512', 'DSA-SHA', 'DSA-SHA1', 'DSA', 'DSA-WITH-SHA224', 'DSA-SHA224', 'DSA-WITH-SHA256', 'DSA-SHA256', 'DSA-WITH-SHA384', 'DSA-SHA384', 'DSA-WITH-SHA512', 'DSA-SHA512', 'DSA-RIPEMD160', 'ripemd160WithRSA', 'RSA-RIPEMD160', 'md5WithRSAEncryption', 'RSA-MD5', ]; // TODO: these used to be shipped by crypto-browserify in quickcrypto v0.6 // could instead fetch from OpenSSL if needed and handle breaking changes export const getCiphers = () => [ 'des-ecb', 'des', 'des-cbc', 'des3', 'des-ede3-cbc', 'des-ede3', 'des-ede-cbc', 'des-ede', 'aes-128-ecb', 'aes-192-ecb', 'aes-256-ecb', 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', 'aes128', 'aes192', 'aes256', 'aes-128-cfb', 'aes-192-cfb', 'aes-256-cfb', 'aes-128-cfb8', 'aes-192-cfb8', 'aes-256-cfb8', 'aes-128-cfb1', 'aes-192-cfb1', 'aes-256-cfb1', 'aes-128-ofb', 'aes-192-ofb', 'aes-256-ofb', 'aes-128-ctr', 'aes-192-ctr', 'aes-256-ctr', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm', ]; export * from './Hashnames';