import { Platform } from 'react-native'; import { Buffer } from 'buffer'; import { DPoPCryptoProvider, DPoPKeyPair } from '@explorins/pers-sdk/core'; // Lazy-loaded crypto module - initialized on first use to avoid native module timing issues let _crypto: any = null; let _cryptoInitialized = false; /** * Lazily load the crypto module. * * Supports customer pre-initialization via global.__QuickCrypto for environments * where native module timing is unpredictable (e.g., certain React Native setups). * * @returns The crypto module or null if unavailable */ function getCrypto(): any { if (_cryptoInitialized) { return _crypto; } _cryptoInitialized = true; if (Platform.OS === 'web') { // On Web, we shouldn't be using this provider (Core SDK has WebDPoPProvider) _crypto = { generateKeyPair: () => { throw new Error('ReactNativeDPoPProvider not supported on Web'); }, randomUUID: () => globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random()}` }; return _crypto; } // Check for customer pre-initialized global (for native module timing issues) if (typeof global !== 'undefined' && (global as any).__QuickCrypto) { _crypto = (global as any).__QuickCrypto; return _crypto; } // Standard lazy load try { _crypto = require('react-native-quick-crypto').default || require('react-native-quick-crypto'); } catch (e) { console.warn('ReactNativeDPoPProvider: Failed to load react-native-quick-crypto.', e); console.warn('Tip: If you encounter timing issues, pre-initialize in index.js: global.__QuickCrypto = require("react-native-quick-crypto")'); _crypto = null; } return _crypto; } export class ReactNativeDPoPProvider implements DPoPCryptoProvider { /** * Generates a new key pair (ES256 recommended) * * Uses WebCrypto API (crypto.subtle) for cross-platform compatibility. * Falls back to Node.js crypto API on iOS if needed. */ async generateKeyPair(options?: { extractable?: boolean }): Promise { const crypto = getCrypto(); if (!crypto) { throw new Error('Crypto module not available. Install react-native-quick-crypto or pre-initialize global.__QuickCrypto'); } // Try WebCrypto API first (works on both iOS and Android) if (crypto.subtle) { try { const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, // extractable - required for JWK export ['sign', 'verify'] ); const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey); const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); return { publicKey: publicKeyJwk, privateKey: privateKeyJwk }; } catch (err) { console.warn('[DPoP] WebCrypto API failed, trying Node.js crypto API', err); } } // Fallback: Node.js crypto API (works on iOS) return new Promise((resolve, reject) => { crypto.generateKeyPair('ec', { namedCurve: 'P-256' }, (err: any, publicKey: any, privateKey: any) => { if (err) return reject(err); try { const pJwk = publicKey.export({ format: 'jwk' }); const prJwk = privateKey.export({ format: 'jwk' }); resolve({ publicKey: pJwk, privateKey: prJwk }); } catch (exportErr) { reject(exportErr); } }); }); } async signProof(payload: Record, keyPair: DPoPKeyPair): Promise { const crypto = getCrypto(); if (!crypto) { throw new Error('Crypto module not available for signing'); } // 1. Construct Header const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: keyPair.publicKey }; // 2. Add Claims (iat/jti) const finalPayload = { ...payload, iat: payload.iat || Math.floor(Date.now() / 1000), jti: payload.jti || (crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`) }; // 3. Encode const b64Header = this.base64Url(JSON.stringify(header)); const b64Payload = this.base64Url(JSON.stringify(finalPayload)); const signingInput = `${b64Header}.${b64Payload}`; // 4. Sign - try WebCrypto first, fallback to Node.js crypto if (crypto.subtle) { try { const privateKey = await crypto.subtle.importKey( 'jwk', keyPair.privateKey, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign'] ); const signatureBuffer = await crypto.subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, privateKey, Buffer.from(signingInput) ); // WebCrypto returns signature in IEEE P1363 format (r || s), which is what we need for JWT return `${signingInput}.${this.base64UrlBuffer(Buffer.from(signatureBuffer))}`; } catch (err) { console.warn('[DPoP] WebCrypto sign failed, trying Node.js crypto', err); } } // Fallback: Node.js crypto API const sign = crypto.createSign('SHA256'); sign.update(signingInput); const privateKeyObj = crypto.createPrivateKey({ key: keyPair.privateKey as any, format: 'jwk' }); const signature = sign.sign(privateKeyObj); return `${signingInput}.${this.base64UrlBuffer(signature)}`; } async hashAccessToken(accessToken: string): Promise { const crypto = getCrypto(); if (!crypto) { throw new Error('Crypto module not available for hashing'); } // Try WebCrypto first if (crypto.subtle) { try { const hashBuffer = await crypto.subtle.digest('SHA-256', Buffer.from(accessToken)); return this.base64UrlBuffer(Buffer.from(hashBuffer)); } catch (err) { // Fallback to Node.js crypto } } const hash = crypto.createHash('sha256').update(accessToken).digest(); return this.base64UrlBuffer(hash); } // --- Helpers --- private base64Url(str: string): string { return this.base64UrlBuffer(Buffer.from(str)); } private base64UrlBuffer(buf: Buffer | Uint8Array): string { const buffer = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); return buffer.toString('base64') .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); } }