react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
286 lines (253 loc) • 8.73 kB
text/typescript
import { Buffer as CraftzdogBuffer } from '@craftzdog/react-native-buffer';
import { Buffer as SafeBuffer } from 'safe-buffer';
import { NitroModules } from 'react-native-nitro-modules';
import type { Utils } from '../specs/utils.nitro';
import type { ABV, BinaryLikeNode, BufferLike } from './types';
import { Platform } from 'react-native';
type UtilsWithStringConverter = Utils & {
bufferToString(
buffer: ArrayBuffer,
encoding: string,
start?: number,
end?: number,
): string;
stringToBuffer(str: string, encoding: string): ArrayBuffer;
};
const utils =
NitroModules.createHybridObject<UtilsWithStringConverter>('Utils');
const isHermes =
(global as { HermesInternal?: unknown }).HermesInternal != null;
// v0.78.0, https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450
// Use jsi::String::getStringData() rather than jsi::String::utf16()
const canGetU16StringFromJsiString = !(
Platform.constants.reactNativeVersion.major == 0 &&
Platform.constants.reactNativeVersion.minor < 78
);
// v0.79.0, https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2
const canCreateJsiStringFromUtf16 = !(
Platform.constants.reactNativeVersion.major == 0 &&
Platform.constants.reactNativeVersion.minor < 79
);
const baseNativeEncodings = [
'hex',
'base64',
'base64url',
'utf8',
'utf-8',
'latin1',
'binary',
'ascii',
];
const nativeStringToBufferEncodings = new Set<string>(baseNativeEncodings);
const nativeBufferToStringEncodings = new Set<string>(baseNativeEncodings);
// The fast and lossless paths for utf16le are only available on Hermes
if (isHermes) {
if (canGetU16StringFromJsiString) {
nativeStringToBufferEncodings.add('utf16le');
}
if (canCreateJsiStringFromUtf16) {
nativeBufferToStringEncodings.add('utf16le');
}
}
// WebCrypto / Web IDL §BufferSource: SharedArrayBuffer-backed inputs must
// be rejected. Concurrent writes from another worker during async crypto
// can corrupt computations or leak intermediate state, so even copying
// the source isn't safe (the copy itself races). Reject at conversion.
// See Node's `lib/internal/webidl.js` BufferSource converter (commit
// bee10872588) — it throws TypeError, matching the WebIDL spec.
//
// We apply this guard to *every* conversion helper, not just the ones
// reached from `subtle.*`. That's deliberately stricter than Node, whose
// classic APIs (`createHash().update`, `createHmac().update`,
// `createCipheriv().update`, etc.) accept SAB-backed views. The TOCTOU
// concern is the same on either side of the WebCrypto / classic line, so
// we prefer the safer default everywhere.
export function rejectSharedArrayBuffer(buf: unknown): void {
if (typeof SharedArrayBuffer === 'undefined') return;
if (buf instanceof SharedArrayBuffer) {
throw new TypeError('SharedArrayBuffer is not a supported BufferSource');
}
if (
ArrayBuffer.isView(buf) &&
(buf as ArrayBufferView).buffer instanceof SharedArrayBuffer
) {
throw new TypeError(
'View on a SharedArrayBuffer is not a supported BufferSource',
);
}
}
/**
* Returns the underlying ArrayBuffer of a Buffer / TypedArray view **without
* copying**, ignoring `byteOffset`/`byteLength`. The full backing storage is
* exposed.
*
* Only use this when the caller separately tracks `byteOffset`/`byteLength`
* and the native receiver needs to write back into the original memory
* (e.g. `randomFill`). For data that will be read by native crypto, use
* `binaryLikeToArrayBuffer`/`toArrayBuffer` instead — those return only the
* view's region and won't leak unrelated bytes from the backing buffer.
*/
export const abvToArrayBuffer = (buf: ABV) => {
rejectSharedArrayBuffer(buf);
if (CraftzdogBuffer.isBuffer(buf)) {
return buf.buffer as ArrayBuffer;
}
if (ArrayBuffer.isView(buf)) {
return buf.buffer as ArrayBuffer;
}
return buf as ArrayBuffer;
};
/**
* Converts supplied argument to an ArrayBuffer. Note this copies data
* only when the supplied view represents a subrange of the underlying
* ArrayBuffer; otherwise the backing buffer is returned directly
* (aliased — do not mutate after passing).
* @param buf
* @returns ArrayBuffer
*/
export function toArrayBuffer(
buf: CraftzdogBuffer | SafeBuffer | ArrayBufferView,
): ArrayBuffer {
if (CraftzdogBuffer.isBuffer(buf) || ArrayBuffer.isView(buf)) {
if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
return buf.buffer as ArrayBuffer;
} else {
return buf.buffer.slice(
buf.byteOffset,
buf.byteOffset + buf.byteLength,
) as ArrayBuffer;
}
}
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 {
rejectSharedArrayBuffer(buf);
// Buffer
if (CraftzdogBuffer.isBuffer(buf) || SafeBuffer.isBuffer(buf)) {
return toArrayBuffer(buf);
}
// ArrayBufferView
if (ArrayBuffer.isView(buf)) {
return toArrayBuffer(buf);
}
// If buf is already an ArrayBuffer, return it.
if (buf instanceof ArrayBuffer) {
return buf;
}
throw new TypeError(
'Input must be a Buffer, ArrayBufferView, or ArrayBuffer.',
);
}
export function binaryLikeToArrayBuffer(
input: BinaryLikeNode, // CipherKey adds compat with node types
encoding: string = 'utf-8',
): ArrayBuffer {
rejectSharedArrayBuffer(input);
// string
if (typeof input === 'string') {
if (encoding === 'buffer') {
throw new Error(
'Cannot create a buffer from a string with a buffer encoding',
);
}
if (nativeStringToBufferEncodings.has(encoding)) {
return utils.stringToBuffer(input, 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);
// }
// }
// KeyObject — duck-typed via Symbol.toStringTag to avoid circular dependency
// with keys/classes. The type assertion must match KeyObjectHandle.exportKey().
if (
typeof input === 'object' &&
input != null &&
Object.prototype.toString.call(input) === '[object KeyObject]'
) {
return (
input as { handle: { exportKey(): ArrayBuffer } }
).handle.exportKey();
}
throw new Error(
'Invalid argument type for "key". Need ArrayBuffer, TypedArray, KeyObject, CryptoKey, string',
);
}
export function ab2str(
buf: ArrayBuffer,
encoding: string = 'hex',
start?: number,
end?: number,
): string {
if (nativeBufferToStringEncodings.has(encoding)) {
return bufferToString(buf, encoding, start, end);
}
return CraftzdogBuffer.from(buf).toString(encoding, start, end);
}
/** Native C++ buffer-to-string with arguments normalization*/
export function bufferToString(
buf: ArrayBuffer,
encoding: string = 'hex',
start?: number,
end?: number,
): string {
// https://github.com/nodejs/node/blob/v24.15.0/lib/buffer.js#L915-L928
if (start === undefined || start < 0) {
start = 0;
} else if (start >= buf.byteLength) {
return '';
} else {
start = Math.trunc(start) || 0;
}
if (end === undefined || end > buf.byteLength) {
end = buf.byteLength;
} else {
end = Math.trunc(end) || 0;
}
if (end <= start) {
return '';
}
return utils.bufferToString(buf, encoding, start, end);
}
/** Native C++ string-to-buffer — exposed for benchmarking */
export function stringToBuffer(
str: string,
encoding: string = 'utf-8',
): ArrayBuffer {
return utils.stringToBuffer(str, encoding);
}
export const kEmptyObject = Object.freeze(Object.create(null));
export * from './noble';