react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
369 lines (344 loc) • 13.8 kB
JavaScript
import { Buffer as CraftzdogBuffer } from '@craftzdog/react-native-buffer';
import { Buffer as SafeBuffer } from 'safe-buffer';
// @types/node
// TODO(osp) should buffer be part of the Encoding type?
// These are for shortcomings in @types/node
// Here we use "*Type" instead of "*Types" like node does.
// Mimics node behavior for default global encoding
let defaultEncoding = 'buffer';
export function setDefaultEncoding(encoding) {
defaultEncoding = encoding;
}
export function getDefaultEncoding() {
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) {
if (CraftzdogBuffer.isBuffer(buffer) || ArrayBuffer.isView(buffer)) {
return buffer.buffer;
}
return buffer;
}
/**
* 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) {
if (CraftzdogBuffer.isBuffer(buf) || ArrayBuffer.isView(buf)) {
if (buf?.buffer?.slice) {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
} 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) {
if (CraftzdogBuffer.isBuffer(buf) || SafeBuffer.isBuffer(buf)) {
return toArrayBuffer(buf);
}
if (ArrayBuffer.isView(buf)) {
return toArrayBuffer(buf);
}
return buf;
}
export function binaryLikeToArrayBuffer(input,
// CipherKey adds compat with node types
encoding = 'utf-8') {
// 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, encoding = 'hex') {
return CraftzdogBuffer.from(buf).toString(encoding);
}
export function validateString(str, name) {
const isString = typeof str === 'string';
if (!isString) {
throw new Error(`${name} is not a string`);
}
return isString;
}
export function validateFunction(f) {
return f !== null && typeof f === 'function';
}
export function isStringOrBuffer(val) {
return typeof val === 'string' || ArrayBuffer.isView(val) || val instanceof ArrayBuffer;
}
export function validateObject(value, name, options) {
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, name, 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, name, positive) {
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, checks) {
for (const s of set) {
if (!checks.includes(s)) {
return true;
}
}
return false;
}
export function lazyDOMException(message, domName) {
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, name) => {
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, name, required = 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, name, target) => {
if (SafeBuffer.isBuffer(buf) && buf.length !== target || buf.byteLength !== target) {
throw lazyDOMException(`${name} must contain exactly ${target} bytes`, 'OperationError');
}
};
export const getUsagesUnion = (usageSet, ...usages) => {
const newset = [];
for (let n = 0; n < usages.length; n++) {
if (!usages[n] || usages[n] === undefined) continue;
if (usageSet.includes(usages[n])) newset.push(usages[n]);
}
return newset;
};
const kKeyOps = {
sign: 1,
verify: 2,
encrypt: 3,
decrypt: 4,
wrapKey: 5,
unwrapKey: 6,
deriveKey: 7,
deriveBits: 8
};
export const validateKeyOps = (keyOps, usagesSet) => {
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 = keyOps[n];
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 => {
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';
//# sourceMappingURL=Utils.js.map
;