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
text/typescript
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';