@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
JavaScript
;
// 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);
}