UNPKG

airsign-web-demo

Version:

AirSign Protocol Web Demo - Shows crypto payments between browser tabs

230 lines (195 loc) 8.63 kB
/** * AirSign SDK Browser Bundle * * Browser-compatible version of the AirSign SDK for real crypto operations * This brings the real X25519 ECDH and XChaCha20-Poly1305 encryption to the web demo */ // We'll load libsodium directly in the browser let sodium; class AirSignBrowser { constructor() { this.keyPair = null; this.isReady = false; this.useFallback = false; } async init() { try { // Try to load libsodium for real crypto if (!sodium) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/libsodium-wrappers@0.7.11/dist/libsodium.js'; document.head.appendChild(script); await new Promise((resolve, reject) => { script.onload = () => { sodium = window.sodium; resolve(); }; script.onerror = () => { console.warn('Failed to load libsodium, using fallback crypto'); this.useFallback = true; resolve(); }; }); } if (!this.useFallback) { await sodium.ready; this.keyPair = this.generateKeyPair(); console.log('🔐 AirSign SDK initialized with real libsodium crypto'); } else { this.keyPair = this.generateFallbackKeyPair(); console.log('🔐 AirSign SDK initialized with fallback crypto (demo only)'); } this.isReady = true; } catch (error) { console.warn('Crypto init failed, using fallback:', error); this.useFallback = true; this.keyPair = this.generateFallbackKeyPair(); this.isReady = true; console.log('🔐 AirSign SDK initialized with fallback crypto (demo only)'); } } generateKeyPair() { if (this.useFallback) return this.generateFallbackKeyPair(); const keyPair = sodium.crypto_box_keypair(); return { publicKey: keyPair.publicKey, privateKey: keyPair.privateKey }; } generateFallbackKeyPair() { // Simple fallback using Web Crypto API for demo const publicKey = new Uint8Array(32); const privateKey = new Uint8Array(32); crypto.getRandomValues(publicKey); crypto.getRandomValues(privateKey); return { publicKey, privateKey }; } async createPresencePacket(deviceName, capabilities) { if (!this.isReady) await this.init(); const presence = { type: 'presence', deviceName, capabilities, publicKey: this.arrayToHex(this.keyPair.publicKey), timestamp: Date.now(), version: '1.0.0' }; // Create a simple signature (in production, use proper Ed25519) const message = JSON.stringify(presence); const signature = this.useFallback ? this.createFallbackSignature(message) : sodium.to_hex(sodium.crypto_sign_detached(message, this.keyPair.privateKey)); return { ...presence, signature }; } async encryptPaymentRequest(paymentURI, peerPublicKey) { if (!this.isReady) await this.init(); const envelope = { type: 'payment-request', uri: paymentURI, timestamp: Date.now(), from: this.arrayToHex(this.keyPair.publicKey) }; const message = JSON.stringify(envelope); if (this.useFallback) { // Simple Base64 encoding for demo (safe and reliable) const encoded = btoa(unescape(encodeURIComponent(message))); // Handle UTF-8 properly return { type: 'encrypted-message', data: encoded, nonce: 'fallback-nonce', from: this.arrayToHex(this.keyPair.publicKey), method: 'fallback-base64' }; } else { const peerPubKey = sodium.from_hex(peerPublicKey); const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); const encrypted = sodium.crypto_box_easy(message, nonce, peerPubKey, this.keyPair.privateKey); return { type: 'encrypted-message', data: sodium.to_hex(encrypted), nonce: sodium.to_hex(nonce), from: sodium.to_hex(this.keyPair.publicKey), method: 'libsodium' }; } } async decryptMessage(encryptedMessage, peerPublicKey) { if (!this.isReady) await this.init(); console.log('Decryption method:', encryptedMessage.method); console.log('Data length:', encryptedMessage.data.length); if (this.useFallback || encryptedMessage.method === 'fallback-base64' || encryptedMessage.method === 'fallback') { // Simple Base64 decoding for demo try { const decoded = decodeURIComponent(escape(atob(encryptedMessage.data))); // Handle UTF-8 properly console.log('Decoded message:', decoded); return JSON.parse(decoded); } catch (error) { console.error('Base64 decode error:', error); // Fallback: try simple atob try { const simpleDecoded = atob(encryptedMessage.data); console.log('Simple decoded:', simpleDecoded); return JSON.parse(simpleDecoded); } catch (error2) { throw new Error('Failed to decode fallback message: ' + error2.message); } } } else { const peerPubKey = sodium.from_hex(peerPublicKey); const encrypted = sodium.from_hex(encryptedMessage.data); const nonce = sodium.from_hex(encryptedMessage.nonce); try { const decrypted = sodium.crypto_box_open_easy(encrypted, nonce, peerPubKey, this.keyPair.privateKey); return JSON.parse(sodium.to_string(decrypted)); } catch (error) { throw new Error('Failed to decrypt message: ' + error.message); } } } getPublicKey() { return this.keyPair ? this.arrayToHex(this.keyPair.publicKey) : null; } // Helper functions arrayToHex(uint8Array) { return Array.from(uint8Array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } hexToArray(hex) { const result = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { result[i / 2] = parseInt(hex.substr(i, 2), 16); } return result; } createFallbackSignature(message) { // Simple hash-based signature for demo return 'fallback_signature_' + btoa(message).slice(0, 16); } xorEncrypt(message, key) { // Simplified fallback encryption for demo const keyBytes = this.hexToArray(key.substring(0, 32)); // Use first 16 bytes only const messageBytes = new TextEncoder().encode(message); const encrypted = new Uint8Array(messageBytes.length); for (let i = 0; i < messageBytes.length; i++) { encrypted[i] = messageBytes[i] ^ keyBytes[i % keyBytes.length]; } return this.arrayToHex(encrypted); } xorDecrypt(encryptedHex, key) { // Use same process as encryption (XOR is symmetric) const keyBytes = this.hexToArray(key.substring(0, 32)); // Use first 16 bytes only const encryptedBytes = this.hexToArray(encryptedHex); const decrypted = new Uint8Array(encryptedBytes.length); for (let i = 0; i < encryptedBytes.length; i++) { decrypted[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length]; } return new TextDecoder().decode(decrypted); } } // Export for global use window.AirSignSDK = new AirSignBrowser(); console.log('🚀 AirSign Browser SDK loaded - real crypto ready!');