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; } }