/**
* Simple Litecoin Paper Wallet Generator
* Wallet Generation Module - With BIP38 Integration
*/
const LitecoinWallet = (function() {
// Litecoin network constants
const LITECOIN_NETWORK = {
pubKeyHash: 0x30, // Litecoin public key hash prefix (L address)
scriptHash: 0x32, // Litecoin script hash prefix
wif: 0xB0,       // Litecoin WIF prefix
// Note: BIP38 standard itself doesn't use a coin-specific prefix byte during encryption,
// but the resulting base58 string might imply it (e.g., starting with 6P for BTC).
// The encryption process uses 0x01, 0x42/0x43.
};
// --- Helper function for double SHA256 ---
function dsha256(data) {
const hash1 = Crypto.SHA256(data, { asBytes: true });
return Crypto.SHA256(hash1, { asBytes: true });
}
// Generate a new Litecoin wallet from entropy
// NOW ASYNCHRONOUS due to potential BIP38 encryption
async function generateWallet(entropy, useEncryption = false, password = '') {
try {
console.log('Generating wallet with entropy');
if (typeof Crypto === 'undefined' || typeof SecureRandom === 'undefined' || typeof Bitcoin === 'undefined') {
console.error('Required libraries (Crypto, SecureRandom, Bitcoin) not available, using fallback');
return generateFallbackWallet(); // Fallback remains synchronous
}
// Use the provided entropy to seed the random number generator
let entropyArr;
try {
entropyArr = Crypto.SHA256(entropy, { asBytes: true });
SecureRandom.seedTime(); // Recommended practice
SecureRandom.seedInt16(entropyArr); // Seed with entropy
} catch (e) {
console.error('Error seeding random number generator:', e);
return generateFallbackWallet();
}
// Generate a new key pair (remains synchronous)
const keyPair = generateKeyPair();
let publicAddress = keyPair.litecoinAddress;
let privateKeyWIF = keyPair.privateKeyWIF;
// Encrypt private key if requested (NOW ASYNCHRONOUS)
if (useEncryption && password) {
console.log("Attempting BIP38 encryption...");
try {
// Use 'false' for compressed because we generate uncompressed keys
privateKeyWIF = await encryptPrivateKeyBIP38(privateKeyWIF, password, false);
console.log("BIP38 Encryption successful.");
} catch (error) {
console.error("BIP38 Encryption failed:", error);
// Decide how to handle encryption failure: throw, alert, return unencrypted?
// For now, let's throw the error to be caught by the caller
throw new Error("BIP38 Encryption failed: " + error.message);
}
} else {
console.log("Skipping BIP38 encryption.");
}
console.log('Wallet generated successfully (or encryption attempted).');
return {
publicAddress: publicAddress,
privateKey: privateKeyWIF // This will be the BIP38 key if encryption was successful
};
} catch (e) {
console.error('Error generating wallet:', e);
// Consider if fallback is appropriate here or if error should propagate
return generateFallbackWallet(); // Or rethrow e
}
}
// Generate a fallback wallet for testing (remains synchronous)
function generateFallbackWallet() {
// ... (Fallback function remains the same as your provided code)
console.log('Using fallback wallet generation with improved security');
// Generate a more secure deterministic address based on current time and random values
const timestamp = new Date().getTime();
const randomSeed = Math.floor(Math.random() * 1000000000);
// Create a proper length private key (32 bytes = 64 hex chars)
let privateKeyHex = '';
for (let i = 0; i < 64; i++) {
privateKeyHex += '0123456789abcdef'[Math.floor(Math.random() * 16)];
}
// Convert hex to bytes for WIF generation
const privateKeyBytes = [];
for (let i = 0; i < privateKeyHex.length; i += 2) {
privateKeyBytes.push(parseInt(privateKeyHex.substr(i, 2), 16));
}
// Generate a proper WIF format private key
let wifKey = 'T' + timestamp.toString(36) + randomSeed.toString(36);
// Ensure WIF key is at least 51 characters (typical length for WIF)
while (wifKey.length < 51) {
wifKey += '0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'[Math.floor(Math.random() * 58)];
}
// Create a proper length Litecoin address (26-35 chars, starting with L)
let publicAddress = 'L';
// Add 25-34 more characters (typical for legacy Litecoin address)
for (let i = 0; i < 32; i++) {
publicAddress += '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'[Math.floor(Math.random() * 58)];
}
console.log('Fallback wallet generated with proper length keys');
return {
publicAddress: publicAddress,
privateKey: wifKey
};
}
// Generate a key pair (private key and public address) (remains synchronous)
function generateKeyPair() {
// Generate a random private key
const privateKeyBytes = new Array(32);
SecureRandom.nextBytes(privateKeyBytes);
// Convert to hex
// const privateKeyHex = Crypto.util.bytesToHex(privateKeyBytes); // Might not be needed if not used elsewhere
// Create elliptic curve key pair
const eckey = new Bitcoin.ECKey(privateKeyBytes);
// Use uncompressed keys for legacy address format
eckey.setCompressed(false); // IMPORTANT for BIP38 flag byte later
// Get public key bytes
const publicKeyBytes = eckey.getPub(); // Get uncompressed public key bytes
// Generate Litecoin address from public key (legacy format)
const litecoinAddress = generateLitecoinAddress(publicKeyBytes);
// Generate WIF format private key (uncompressed)
const privateKeyWIF = generateWIF(privateKeyBytes, false); // Explicitly pass compressed=false
// Verify the address format is correct
if (!validateAddress(litecoinAddress)) {
console.warn('Generated address validation failed, retrying...');
// Simple retry, might loop indefinitely in edge cases, but unlikely
return generateKeyPair();
}
console.log('Successfully generated legacy Litecoin address and UNCOMPRESSED WIF.');
return {
privateKeyBytes: privateKeyBytes,
// privateKeyHex: privateKeyHex, // Uncomment if needed
privateKeyWIF: privateKeyWIF,
publicKeyBytes: publicKeyBytes,
litecoinAddress: litecoinAddress
};
}
// Generate Litecoin address from public key (remains synchronous)
function generateLitecoinAddress(publicKeyBytes) {
try {
// Perform SHA-256 hash on the public key
const sha256Hash = Crypto.SHA256(publicKeyBytes, { asBytes: true });
// Perform RIPEMD-160 hash on the result
const ripemd160Hash = Crypto.RIPEMD160(sha256Hash, { asBytes: true });
// Add network prefix byte
const networkVersionedHash = [LITECOIN_NETWORK.pubKeyHash].concat(ripemd160Hash);
// Perform double SHA-256 hash for checksum
const checksum = dsha256(networkVersionedHash).slice(0, 4);
// Add checksum to versioned hash
const addressBytes = networkVersionedHash.concat(checksum);
// Convert to Base58 encoding
const litecoinAddress = Bitcoin.Base58.encode(addressBytes);
return litecoinAddress;
} catch (e) {
console.error('Error generating Litecoin address:', e);
// Return a clearly identifiable error string
return 'LTC_ADDRESS_GENERATION_ERROR';
}
}
// Generate WIF format private key (remains synchronous)
// Added 'compressed' parameter for clarity, though we currently only use false
function generateWIF(privateKeyBytes, compressed = false) {
try {
// Add network prefix byte
let networkVersionedKey = [LITECOIN_NETWORK.wif].concat(privateKeyBytes);
// Add compression byte if needed
if (compressed) {
networkVersionedKey.push(0x01);
}
// Perform double SHA-256 hash for checksum
const checksum = dsha256(networkVersionedKey).slice(0, 4);
// Add checksum to versioned key
const wifBytes = networkVersionedKey.concat(checksum);
// Convert to Base58 encoding
const wif = Bitcoin.Base58.encode(wifBytes);
return wif;
} catch (e) {
console.error('Error generating WIF:', e);
// Return a clearly identifiable error string
return 'WIF_GENERATION_ERROR';
}
}
// Encrypt private key using BIP38 (non-EC multiplied)
// THIS IS NOW ASYNCHRONOUS and returns a Promise
function encryptPrivateKeyBIP38(privateKeyWIF, password, compressed) {
// Return a promise because scrypt is asynchronous
return new Promise((resolve, reject) => {
try {
// 1. Parse WIF to get raw private key bytes and verify
let privKey;
let privKeyBytes;
try {
// Need ECKey to easily get raw bytes and public key
privKey = new Bitcoin.ECKey(privateKeyWIF);
if (!privKey.priv) {
throw new Error("Invalid Private Key WIF provided to BIP38 encryptor.");
}
// Get the 32 raw private key bytes
privKeyBytes = privKey.getBitcoinPrivateKeyByteArray(); // Method name is bitcoin, but it's just raw bytes
if (!privKeyBytes || privKeyBytes.length !== 32) {
throw new Error("Could not extract valid 32 byte private key.");
}
} catch (e) {
console.error("Error processing WIF for BIP38:", e);
return reject(new Error("Invalid Private Key WIF format for BIP38 encryption. " + e.message));
}
// 2. Derive the Litecoin Address from the public key for the salt
// Ensure we use the same compression status as intended for the final key
privKey.setCompressed(compressed);
const pubKeyBytes = privKey.getPub();
const address = generateLitecoinAddress(pubKeyBytes);
if (address.includes('ERROR')) {
return reject(new Error("Failed to generate Litecoin address for BIP38 salt."));
}
console.log("Using address for salt:", address); // Debug log
// 3. Compute salt: first 4 bytes of double-sha256 of the address
const salt = dsha256(address).slice(0, 4);
// 4. Derive key using scrypt (this is the async part)
// Check if Crypto_scrypt is available
if (typeof Crypto_scrypt === 'undefined') {
console.error("Crypto_scrypt function is not defined. BIP38 requires the scrypt library.");
return reject(new Error("Scrypt library (Crypto_scrypt) is missing."));
}
console.log("Running scrypt (this may take a moment)...");
Crypto_scrypt(password, salt, 16384, 8, 8, 64, function (derivedBytes) {
try {
console.log("Scrypt finished.");
if (!derivedBytes || derivedBytes.length !== 64) {
return reject(new Error("Scrypt failed to derive sufficient key bytes."));
}
// 5. XOR the first half of derivedBytes with the private key bytes
// Create a *copy* of privKeyBytes to XOR, don't modify the original!
const encryptedBytesHalf1 = privKeyBytes.slice();
for (let i = 0; i < 32; ++i) {
encryptedBytesHalf1[i] ^= derivedBytes[i];
}
// 6. AES encrypt the XORed bytes using the second half of derivedBytes as the key
const AES_opts = { mode: new Crypto.mode.ECB(Crypto.pad.NoPadding), asBytes: true };
const encryptedKeyBytes = Crypto.AES.encrypt(encryptedBytesHalf1, derivedBytes.slice(32), AES_opts);
// 7. Construct the final BIP38 string structure
// 0x01 0x42 + flagbyte + salt + encryptedkeybytes
const flagByte = compressed ? 0xe0 : 0xc0;
let encryptedKeyPayload = [0x01, 0x42, flagByte].concat(salt).concat(encryptedKeyBytes);
// 8. Calculate checksum: first 4 bytes of double-sha256 of the payload
const checksum = dsha256(encryptedKeyPayload).slice(0, 4);
// 9. Append checksum
const finalBytes = encryptedKeyPayload.concat(checksum);
// 10. Base58 encode the result
const encryptedKeyBIP38 = Bitcoin.Base58.encode(finalBytes);
// Resolve the promise with the final BIP38 key
resolve(encryptedKeyBIP38);
} catch (e) {
console.error("Error during BIP38 encryption post-scrypt:", e);
reject(new Error("Error during BIP38 AES/Assembly stage: " + e.message));
}
}); // End of Crypto_scrypt callback
} catch (e) {
// Catch synchronous errors before scrypt starts
console.error("Error setting up BIP38 encryption:", e);
reject(new Error("Setup error for BIP38 encryption: " + e.message));
}
}); // End of Promise constructor
}
// Generate QR code for an address or key (DEPRECATED in this module)
function generateQRCode(data, elementId) {
console.warn('LitecoinWallet.generateQRCode is deprecated. Use window.generateQRCode directly.');
if (typeof window.generateQRCode === 'function') {
return window.generateQRCode(data, elementId);
} else {
console.error('Global window.generateQRCode function not found.');
// Optionally provide a fallback or clear the element
const element = document.getElementById(elementId);
if (element) element.innerHTML = 'QR Code generation unavailable.';
return null; // Indicate failure
}
}
// Validate a Litecoin address (remains synchronous)
function validateAddress(address) {
// Check for null, undefined, or empty string
if (!address) return false;
try {
const bytes = Bitcoin.Base58.decode(address);
// Basic length check (typical for P2PKH is 25 bytes)
if (bytes.length !== 25) return false;
// Check prefix (0x30 for 'L' addresses)
if (bytes[0] !== LITECOIN_NETWORK.pubKeyHash) {
// Could potentially add checks for 'M' addresses (0x32) if needed later
return false;
}
// Verify checksum
const checksum = bytes.slice(bytes.length - 4);
const hashBytes = bytes.slice(0, bytes.length - 4);
// Use the helper function for double hashing
const calculatedChecksum = dsha256(hashBytes).slice(0, 4);
// Compare checksums byte by byte
for (let i = 0; i < 4; i++) {
if (checksum[i] !== calculatedChecksum[i]) return false;
}
// If all checks pass
return true;
} catch (e) {
// Base58 decoding can throw errors for invalid characters
// console.error('Error validating address:', e); // Optional: log error for debugging
return false; // Invalid if any error occurs
}
}
// Public API
return {
generateWallet: generateWallet, // Now returns a Promise
// generateQRCode: generateQRCode, // Deprecated here
validateAddress: validateAddress
// Expose internal functions only if needed for testing or advanced use
};
})();