UNPKG

airsign-sdk-core

Version:

AirSign Protocol Core SDK - secure nearby crypto & data exchange

245 lines (244 loc) 8.47 kB
/** * Cryptographic operations for AirSign Protocol * * Provides X25519 ECDH key exchange, XChaCha20-Poly1305 AEAD encryption, * and signature verification using libsodium and noble-crypto libraries. */ import sodium from 'libsodium-wrappers'; // import * as secp256k1 from '@noble/secp256k1'; // import * as ed25519 from '@noble/ed25519'; import { AirSignError, ErrorCode } from './types.js'; // Ensure libsodium is initialized let sodiumReady = false; /** * Initialize the cryptographic library (libsodium) * Must be called before using any crypto functions */ async function ensureSodiumReady() { if (!sodiumReady) { await sodium.ready; sodiumReady = true; } } /** * Generate an ephemeral X25519 keypair for ECDH key exchange * * @returns Promise resolving to a new ephemeral keypair * @throws {AirSignError} If key generation fails */ export async function generateEphemeralKeypair() { try { await ensureSodiumReady(); const keypair = sodium.crypto_box_keypair(); return { privateKey: keypair.privateKey, publicKey: sodium.to_base64(keypair.publicKey) }; } catch (error) { throw new AirSignError('Failed to generate ephemeral keypair', ErrorCode.CRYPTO_ERROR, { error }); } } /** * Derive shared secret using X25519 ECDH * * @param ourPrivateKey - Our private key (Uint8Array) * @param theirPublicKey - Their public key (base64 string) * @returns Promise resolving to shared secret key * @throws {AirSignError} If key derivation fails */ export async function deriveSharedKey(ourPrivateKey, theirPublicKey) { try { await ensureSodiumReady(); if (ourPrivateKey.length !== 32) { throw new Error('Private key must be 32 bytes'); } // Convert base64 public key to Uint8Array const theirPublicKeyBytes = sodium.from_base64(theirPublicKey); if (theirPublicKeyBytes.length !== 32) { throw new Error('Public key must be 32 bytes'); } // Perform X25519 ECDH and derive key using HKDF const sharedSecret = sodium.crypto_scalarmult(ourPrivateKey, theirPublicKeyBytes); // Use HKDF to derive a proper symmetric key const derivedKey = sodium.crypto_kdf_derive_from_key(32, // 32 byte key 1, // subkey id 'airsign1', // context (8 bytes) sharedSecret); return derivedKey; } catch (error) { throw new AirSignError('Failed to derive shared key', ErrorCode.CRYPTO_ERROR, { error }); } } /** * Encrypt a message using XChaCha20-Poly1305 AEAD * * @param key - 32-byte encryption key * @param plaintext - Object to encrypt * @returns Promise resolving to encrypted message with nonce * @throws {AirSignError} If encryption fails */ export async function encryptMessage(key, plaintext) { try { await ensureSodiumReady(); if (key.length !== 32) { throw new Error('Encryption key must be 32 bytes'); } const plaintextBytes = new TextEncoder().encode(JSON.stringify(plaintext)); const nonce = sodium.randombytes_buf(24); // XChaCha20 uses 24-byte nonces const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintextBytes, null, // No additional data null, // Secret nonce (not used) nonce, key); return { ciphertext: sodium.to_base64(ciphertext), nonce: sodium.to_base64(nonce) }; } catch (error) { throw new AirSignError('Failed to encrypt message', ErrorCode.CRYPTO_ERROR, { error }); } } /** * Decrypt a message using XChaCha20-Poly1305 AEAD * * @param key - 32-byte decryption key * @param ciphertext - Base64 encoded ciphertext * @param nonce - Base64 encoded nonce * @returns Promise resolving to decrypted object * @throws {AirSignError} If decryption fails */ export async function decryptMessage(key, ciphertext, nonce) { try { await ensureSodiumReady(); if (key.length !== 32) { throw new Error('Decryption key must be 32 bytes'); } const ciphertextBytes = sodium.from_base64(ciphertext); const nonceBytes = sodium.from_base64(nonce); const plaintextBytes = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, // Secret nonce (not used) ciphertextBytes, null, // No additional data nonceBytes, key); const plaintextString = new TextDecoder().decode(plaintextBytes); return JSON.parse(plaintextString); } catch (error) { throw new AirSignError('Failed to decrypt message', ErrorCode.CRYPTO_ERROR, { error }); } } /** * Simple string encryption for testing and demos * * @param message - Plain text message to encrypt * @param key - 32-byte encryption key * @returns Base64 encoded encrypted message with embedded nonce */ export async function encryptString(message, key) { const result = await encryptMessage(key, { data: message }); return `${result.nonce}:${result.ciphertext}`; } /** * Simple string decryption for testing and demos * * @param encryptedMessage - Base64 encoded encrypted message with embedded nonce * @param key - 32-byte decryption key * @returns Decrypted plain text message */ export async function decryptString(encryptedMessage, key) { const [nonce, ciphertext] = encryptedMessage.split(':'); if (!nonce || !ciphertext) { throw new AirSignError('Invalid encrypted message format', ErrorCode.CRYPTO_ERROR); } const result = await decryptMessage(key, ciphertext, nonce); return result.data; } /** * Hash a message envelope for signature verification * * @param envelope - Message envelope to hash * @returns Promise resolving to SHA-256 hash */ export async function hashMessageEnvelope(envelope) { try { await ensureSodiumReady(); // Create deterministic string representation const canonical = JSON.stringify({ type: envelope.type, id: envelope.id, payload: envelope.payload, meta: envelope.meta }); const messageBytes = new TextEncoder().encode(canonical); return sodium.crypto_hash(messageBytes); } catch (error) { throw new AirSignError('Failed to hash message envelope', ErrorCode.CRYPTO_ERROR, { error }); } } /** * Verify a sender signature on a message * * @param messageHash - Hash of the message to verify * @param signatureHex - Hex-encoded signature * @param publicKeyHex - Hex-encoded public key * @param scheme - Signature scheme ('secp256k1' or 'ed25519') * @returns Promise resolving to true if signature is valid * @throws {AirSignError} If verification fails */ export async function verifySenderSignature(_messageHash, _signatureHex, _publicKeyHex, scheme) { // TODO: Implement signature verification with @noble/secp256k1 and @noble/ed25519 // For now, return false as verification is not implemented console.warn(`Signature verification not implemented for ${scheme}`); return false; /* try { const signature = hexToBytes(signatureHex); const publicKey = hexToBytes(publicKeyHex); if (scheme === 'secp256k1') { return secp256k1.verify(signature, messageHash, publicKey); } else if (scheme === 'ed25519') { return ed25519.verify(signature, messageHash, publicKey); } else { throw new Error(`Unsupported signature scheme: ${scheme}`); } } catch (error) { throw new AirSignError( 'Failed to verify signature', ErrorCode.INVALID_SIGNATURE, { error, scheme } ); } */ } /** * Securely clear sensitive data from memory * * @param data - Sensitive data to clear */ export function secureClear(data) { try { sodium.memzero(data); } catch { // Fallback: overwrite with random data for (let i = 0; i < data.length; i++) { data[i] = Math.floor(Math.random() * 256); } } } // Helper functions /* function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } return bytes; } */ export function bytesToHex(bytes) { return Array.from(bytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } //# sourceMappingURL=crypto.js.map