react-native-quick-crypto
Version:
A fast implementation of Node's `crypto` module written in C/C++ JSI
370 lines (329 loc) • 10.9 kB
text/typescript
import { Buffer } from '@craftzdog/react-native-buffer';
import { NitroModules } from 'react-native-nitro-modules';
import type {
AsymmetricKeyType,
EncodingOptions,
JWK,
KeyDetail,
KeyObjectHandle,
KeyUsage,
SubtleAlgorithm,
} from '../utils';
import { KeyType, KFormatType, KeyEncoding, getSortedUsages } from '../utils';
import { parsePrivateKeyEncoding, parsePublicKeyEncoding } from './utils';
export class CryptoKey {
keyObject: KeyObject;
keyAlgorithm: SubtleAlgorithm;
keyUsages: KeyUsage[];
keyExtractable: boolean;
get [Symbol.toStringTag](): string {
return 'CryptoKey';
}
constructor(
keyObject: KeyObject,
keyAlgorithm: SubtleAlgorithm,
keyUsages: KeyUsage[],
keyExtractable: boolean,
) {
this.keyObject = keyObject;
this.keyAlgorithm = keyAlgorithm;
// Frozen so external code can't mutate `key.usages` (per WebCrypto spec).
this.keyUsages = Object.freeze(getSortedUsages(keyUsages)) as KeyUsage[];
this.keyExtractable = keyExtractable;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
inspect(_depth: number, _options: unknown): unknown {
throw new Error('CryptoKey.inspect is not implemented');
// if (depth < 0) return this;
// const opts = {
// ...options,
// depth: options.depth == null ? null : options.depth - 1,
// };
// return `CryptoKey ${inspect(
// {
// type: this.type,
// extractable: this.extractable,
// algorithm: this.algorithm,
// usages: this.usages,
// },
// opts
// )}`;
}
get type() {
// if (!(this instanceof CryptoKey)) throw new Error('Invalid CryptoKey');
return this.keyObject.type;
}
get extractable() {
return this.keyExtractable;
}
get algorithm() {
return this.keyAlgorithm;
}
get usages() {
return this.keyUsages;
}
}
export class KeyObject {
handle: KeyObjectHandle;
type: 'public' | 'secret' | 'private';
get [Symbol.toStringTag](): string {
return 'KeyObject';
}
export(options: { format: 'pem' } & EncodingOptions): string | Buffer;
export(options?: { format: 'der' } & EncodingOptions): Buffer;
export(options?: { format: 'jwk' } & EncodingOptions): JWK;
export(options?: EncodingOptions): string | Buffer | JWK;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export(_options?: EncodingOptions): string | Buffer | JWK {
// This is a placeholder and should be overridden by subclasses.
throw new Error('export() must be implemented by subclasses');
}
equals(otherKeyObject: KeyObject): boolean {
if (!(otherKeyObject instanceof KeyObject)) {
throw new TypeError('otherKeyObject must be a KeyObject');
}
return this.handle.keyEquals(otherKeyObject.handle);
}
constructor(type: string, handle: KeyObjectHandle);
constructor(type: string, key: ArrayBuffer);
constructor(type: string, handleOrKey: KeyObjectHandle | ArrayBuffer) {
if (type !== 'secret' && type !== 'public' && type !== 'private')
throw new Error(`invalid KeyObject type: ${type}`);
if (handleOrKey instanceof ArrayBuffer) {
this.handle = NitroModules.createHybridObject('KeyObjectHandle');
let keyType: KeyType;
switch (type) {
case 'public':
keyType = KeyType.PUBLIC;
break;
case 'private':
keyType = KeyType.PRIVATE;
break;
case 'secret':
keyType = KeyType.SECRET;
break;
default:
// Should not happen
throw new Error('invalid key type');
}
this.handle.init(keyType, handleOrKey);
} else {
this.handle = handleOrKey;
}
this.type = type as 'public' | 'secret' | 'private';
}
static from(key: CryptoKey): KeyObject {
if (!(key instanceof CryptoKey)) {
throw new TypeError(
`The "key" argument must be an instance of CryptoKey. Received ${typeof key}`,
);
}
return key.keyObject;
}
toCryptoKey(
algorithm: SubtleAlgorithm,
extractable: boolean,
keyUsages: KeyUsage[],
): CryptoKey {
return new CryptoKey(this, algorithm, keyUsages, extractable);
}
static createKeyObject(
type: string,
key: ArrayBuffer,
format?: KFormatType,
encoding?: KeyEncoding,
passphrase?: ArrayBuffer,
): KeyObject {
if (type !== 'secret' && type !== 'public' && type !== 'private')
throw new Error(`invalid KeyObject type: ${type}`);
const handle = NitroModules.createHybridObject(
'KeyObjectHandle',
) as KeyObjectHandle;
let keyType: KeyType;
switch (type) {
case 'public':
keyType = KeyType.PUBLIC;
break;
case 'private':
keyType = KeyType.PRIVATE;
break;
case 'secret':
keyType = KeyType.SECRET;
break;
default:
throw new Error('invalid key type');
}
handle.init(keyType, key, format, encoding, passphrase);
// For asymmetric keys, return the appropriate subclass
if (type === 'public' || type === 'private') {
try {
handle.getAsymmetricKeyType();
// If we get here, it's an asymmetric key - return the appropriate subclass
if (type === 'public') {
return new PublicKeyObject(handle);
} else {
return new PrivateKeyObject(handle);
}
} catch {
// Not an asymmetric key, fall through to regular KeyObject
}
}
// For secret keys, return SecretKeyObject
if (type === 'secret') {
return new SecretKeyObject(handle);
}
// Return regular KeyObject for symmetric keys or if asymmetric detection failed
return new KeyObject(type, handle);
}
getAsymmetricKeyType(): undefined {
return undefined;
}
getAsymmetricKeyDetails(): undefined {
return undefined;
}
}
export class SecretKeyObject extends KeyObject {
constructor(handle: KeyObjectHandle) {
super('secret', handle);
}
get symmetricKeySize(): number {
return this.handle.getSymmetricKeySize();
}
export(options: { format: 'pem' } & EncodingOptions): never;
export(options: { format: 'der' } & EncodingOptions): Buffer;
export(options: { format: 'jwk' } & EncodingOptions): never;
export(options?: EncodingOptions): Buffer;
export(options?: EncodingOptions): Buffer {
if (options?.format === 'pem' || options?.format === 'jwk') {
throw new Error(
`SecretKey export for ${options.format} is not supported`,
);
}
const key = this.handle.exportKey();
return Buffer.from(key);
}
}
// const kAsymmetricKeyType = Symbol('kAsymmetricKeyType');
// const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails');
// function normalizeKeyDetails(details = {}) {
// if (details.publicExponent !== undefined) {
// return {
// ...details,
// publicExponent: bigIntArrayToUnsignedBigInt(
// new Uint8Array(details.publicExponent)
// ),
// };
// }
// return details;
// }
export class AsymmetricKeyObject extends KeyObject {
constructor(type: string, handle: KeyObjectHandle) {
super(type, handle);
}
private _asymmetricKeyType?: AsymmetricKeyType;
get asymmetricKeyType(): AsymmetricKeyType {
if (!this._asymmetricKeyType) {
this._asymmetricKeyType = this.handle.getAsymmetricKeyType();
}
return this._asymmetricKeyType;
}
private _asymmetricKeyDetails?: KeyDetail;
get asymmetricKeyDetails() {
if (!this._asymmetricKeyDetails) {
this._asymmetricKeyDetails = this.handle.keyDetail();
}
return this._asymmetricKeyDetails;
}
get namedCurve(): string | undefined {
return this.asymmetricKeyDetails?.namedCurve;
}
}
export class PublicKeyObject extends AsymmetricKeyObject {
constructor(handle: KeyObjectHandle) {
super('public', handle);
}
export(options: { format: 'pem' } & EncodingOptions): string;
export(options: { format: 'der' } & EncodingOptions): Buffer;
export(options: { format: 'jwk' } & EncodingOptions): JWK;
export(options: { format: 'raw-public' } & EncodingOptions): Buffer;
export(options: EncodingOptions): string | Buffer | JWK {
if (options?.format === 'jwk') {
return this.handle.exportJwk({}, false);
}
if (options?.format === 'raw-public') {
if (this.asymmetricKeyType === 'ec') {
const pointType = options.type ?? 'uncompressed';
if (pointType !== 'compressed' && pointType !== 'uncompressed') {
throw new Error(
`Invalid options.type for raw-public EC export: ${pointType}`,
);
}
return Buffer.from(
this.handle.exportECPublicRaw(pointType === 'compressed'),
);
}
return Buffer.from(this.handle.exportRawPublic());
}
const { format, type } = parsePublicKeyEncoding(
options,
this.asymmetricKeyType,
);
if (typeof format !== 'number') {
throw new Error(`Unexpected format ${format} after raw-format dispatch`);
}
const key = this.handle.exportKey(
format,
typeof type === 'string' ? undefined : type,
);
const buffer = Buffer.from(key);
if (options?.format === 'pem') {
return buffer.toString('utf-8');
}
return buffer;
}
}
export class PrivateKeyObject extends AsymmetricKeyObject {
constructor(handle: KeyObjectHandle) {
super('private', handle);
}
export(options: { format: 'pem' } & EncodingOptions): string;
export(options: { format: 'der' } & EncodingOptions): Buffer;
export(options: { format: 'jwk' } & EncodingOptions): JWK;
export(options: { format: 'raw-private' } & EncodingOptions): Buffer;
export(options: { format: 'raw-seed' } & EncodingOptions): Buffer;
export(options: EncodingOptions): string | Buffer | JWK {
if (options?.format === 'jwk') {
if (options.passphrase !== undefined) {
throw new Error('jwk does not support encryption');
}
return this.handle.exportJwk({}, false);
}
if (options?.format === 'raw-private') {
if (this.asymmetricKeyType === 'ec') {
return Buffer.from(this.handle.exportECPrivateRaw());
}
return Buffer.from(this.handle.exportRawPrivate());
}
if (options?.format === 'raw-seed') {
return Buffer.from(this.handle.exportRawSeed());
}
const { format, type, cipher, passphrase } = parsePrivateKeyEncoding(
options,
this.asymmetricKeyType,
);
if (typeof format !== 'number') {
throw new Error(`Unexpected format ${format} after raw-format dispatch`);
}
const key = this.handle.exportKey(
format,
typeof type === 'string' ? undefined : type,
cipher,
passphrase,
);
const buffer = Buffer.from(key);
if (options?.format === 'pem') {
return buffer.toString('utf-8');
}
return buffer;
}
}