airsign-web-demo
Version:
AirSign Protocol Web Demo - Shows crypto payments between browser tabs
230 lines (195 loc) • 8.63 kB
JavaScript
/**
* 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!');