UNPKG

@tonyboyle/solana-wallet-universal-links-generator

Version:

A minimal, stateless TypeScript SDK for generating deep links to mobile Solana wallets

166 lines (165 loc) 7.37 kB
"use strict"; // Encryption utilities for Universal Solana Wallet Adapter // Based on Solflare's encryption specification: https://docs.solflare.com/solflare/technical/deeplinks/encryption // And Phantom's documentation: https://docs.phantom.com/phantom-deeplinks/provider-methods/signtransaction var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateKeyPair = generateKeyPair; exports.generateNonce = generateNonce; exports.encrypt = encrypt; exports.decrypt = decrypt; exports.encryptPayload = encryptPayload; exports.decryptResponse = decryptResponse; exports.encodeMessageForSigning = encodeMessageForSigning; const tweetnacl_1 = require("tweetnacl"); const naclUtil = __importStar(require("tweetnacl-util")); const bs58_1 = __importDefault(require("bs58")); const buffer_1 = require("buffer"); /** * Generate a new x25519 keypair for encryption * Recommended to create a new keypair for every session */ function generateKeyPair() { const keyPair = tweetnacl_1.box.keyPair(); return { publicKey: bs58_1.default.encode(keyPair.publicKey), // Base58 for dapp_encryption_public_key privateKey: naclUtil.encodeBase64(keyPair.secretKey) // Base64 for internal encryption (32 bytes) }; } /** * Generate a random nonce for encryption */ function generateNonce() { return bs58_1.default.encode((0, tweetnacl_1.randomBytes)(24)); // Match Phantom's 24-byte nonce } /** * Encrypt data using Diffie-Hellman key exchange * * @param data - Data to encrypt (can be any type, will be JSON stringified) * @param dappPrivateKey - Your app's private key (base64 encoded) * @param walletPublicKey - Wallet's public key (base58 encoded) * @param nonce - Nonce for encryption (base58 encoded) * @returns Encrypted data and nonce */ function encrypt(data, dappPrivateKey, walletPublicKey, nonce) { // Decode base58 wallet public key to bytes for encryption const walletPublicKeyBytes = bs58_1.default.decode(walletPublicKey); // Decode base64 dapp private key to bytes const dappPrivateKeyBytes = naclUtil.decodeBase64(dappPrivateKey); // Create shared secret using Diffie-Hellman const sharedSecret = tweetnacl_1.box.before(walletPublicKeyBytes, dappPrivateKeyBytes); // Convert data to Uint8Array for encryption (match Phantom's method) const dataString = typeof data === 'string' ? data : JSON.stringify(data); const dataBytes = buffer_1.Buffer.from(dataString); // Encrypt the data const encrypted = tweetnacl_1.box.after(dataBytes, bs58_1.default.decode(nonce), sharedSecret); return { encrypted: bs58_1.default.encode(encrypted), nonce }; } /** * Decrypt data using Diffie-Hellman key exchange * * @param encryptedData - Encrypted data (base58 encoded) * @param nonce - Nonce used for encryption (base58 encoded) * @param dappPrivateKey - Your app's private key (base64 encoded) * @param walletPublicKey - Wallet's public key (base58 encoded) * @param dappPublicKey - Your app's public key (base58 encoded, optional) * @returns Decrypted data */ function decrypt(encryptedData, nonce, dappPrivateKey, walletPublicKey, dappPublicKey) { // Decode base58 wallet public key to bytes for decryption const walletPublicKeyBytes = bs58_1.default.decode(walletPublicKey); // Create shared secret using Diffie-Hellman const dappPrivateKeyBytes = naclUtil.decodeBase64(dappPrivateKey); const sharedSecret = tweetnacl_1.box.before(walletPublicKeyBytes, dappPrivateKeyBytes); // Decrypt the data const decrypted = tweetnacl_1.box.open.after(bs58_1.default.decode(encryptedData), bs58_1.default.decode(nonce), sharedSecret); if (!decrypted) { throw new Error('Failed to decrypt data - invalid key or corrupted data'); } // Convert decrypted bytes back to string const decryptedString = naclUtil.encodeUTF8(decrypted); // Try to parse as JSON, but return as string if it's not valid JSON try { return JSON.parse(decryptedString); } catch { return decryptedString; } } /** * Encrypt a payload for sending to wallet * This is a convenience function that handles the encryption workflow * According to Phantom docs, payload should be "an encrypted JSON string" * * @param payload - Can be an object (will be JSON stringified) or a string * @param dappPrivateKey - Your app's private key (base64 encoded) * @param walletPublicKey - Wallet's public key (base58 encoded) * @returns Encrypted string that can be used as the payload parameter */ function encryptPayload(payload, dappPrivateKey, walletPublicKey) { const nonce = generateNonce(); const result = encrypt(payload, dappPrivateKey, walletPublicKey, nonce); // Return the encrypted data as a string that can be used in URL parameters return result.encrypted; } /** * Decrypt response data from wallet * This is a convenience function that handles the decryption workflow */ function decryptResponse(encryptedData, dappPrivateKey, walletPublicKey) { // For responses, we need to extract the nonce from the data // This assumes the nonce is prepended to the encrypted data // You may need to adjust this based on how wallets format their responses // For now, we'll assume the nonce is provided separately // In practice, you'll need to handle this based on the wallet's response format throw new Error('decryptResponse requires nonce - implement based on wallet response format'); } /** * Helper function to encode a message for signing * Converts a string message to base58 encoding as required by wallets * * @param message - The message to encode (string) * @returns Base58-encoded message string */ function encodeMessageForSigning(message) { const messageBytes = naclUtil.decodeUTF8(message); return bs58_1.default.encode(messageBytes); }