diff --git a/scripts/global.js b/scripts/global.js index 37b054f29..df39c8f92 100644 --- a/scripts/global.js +++ b/scripts/global.js @@ -20,7 +20,13 @@ import { strCurrency, } from './settings.js'; import { createAndSendTransaction } from './transactions.js'; -import { createAlert, confirmPopup, sanitizeHTML, MAP_B58 } from './misc.js'; +import { + createAlert, + confirmPopup, + sanitizeHTML, + MAP_B58, + isBase64, +} from './misc.js'; import { cChainParams, COIN, MIN_PASS_LENGTH } from './chain_params.js'; import { decrypt } from './aes-gcm.js'; @@ -854,14 +860,20 @@ export function accessOrImportWallet() { doms.domPrivKey.focus(); } } - +/** + * An event function triggered apon private key UI input changes + * + * Useful for adjusting the input types or displaying password prompts depending on the import scheme + */ export function onPrivateKeyChanged() { if (hasEncryptedWallet()) return; - // Check whether the length of the string is 128 bytes (that's the length of ciphered plain texts) + // Check whether the string is Base64 (would likely be an MPW-encrypted import) // and it doesn't have any spaces (would be a mnemonic seed) const fContainsSpaces = doms.domPrivKey.value.includes(' '); doms.domPrivKeyPassword.hidden = - doms.domPrivKey.value.length !== 128 && !fContainsSpaces; + (doms.domPrivKey.value.length < 128 || + !isBase64(doms.domPrivKey.value)) && + !fContainsSpaces; doms.domPrivKeyPassword.placeholder = fContainsSpaces ? 'Optional Passphrase' @@ -870,8 +882,12 @@ export function onPrivateKeyChanged() { doms.domPrivKey.setAttribute('type', fContainsSpaces ? 'text' : 'password'); } +/** + * Imports a wallet using the GUI input, handling decryption via UI + */ export async function guiImportWallet() { - const fEncrypted = doms.domPrivKey.value.length === 128; + const fEncrypted = + doms.domPrivKey.value.length >= 128 && isBase64(doms.domPrivKey.value); // If we are in testnet: prompt an import if (cChainParams.current.isTestnet) return importWallet(); @@ -890,6 +906,8 @@ export async function guiImportWallet() { localStorage.setItem('encwif', strPrivKey); return importWallet({ newWif: strDecWIF, + // Save the public key to disk for future View Only mode post-decryption + fSavePublicKey: true, }); } } diff --git a/scripts/index.js b/scripts/index.js index 4655827c5..88bf9ef95 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -52,4 +52,3 @@ export { Masternode }; export { getNetwork } from './network.js'; const toggleNetwork = () => getNetwork().toggle(); export { toggleNetwork }; - diff --git a/scripts/misc.js b/scripts/misc.js index 61715f1ce..51ef51839 100644 --- a/scripts/misc.js +++ b/scripts/misc.js @@ -196,6 +196,35 @@ export function sanitizeHTML(text) { return element.innerHTML; } +/** + * Check if a string is valid Base64 encoding + * @param {string} str - String to check + * @returns {boolean} + */ +export function isBase64(str) { + const base64Regex = /^[A-Za-z0-9+/=]+$/; + + // Check if the string contains only Base64 characters: + if (!base64Regex.test(str)) { + return false; + } + + // Check if the length is a multiple of 4 (required for Base64): + if (str.length % 4 !== 0) { + return false; + } + + // Try decoding the Base64 string to check for errors: + try { + atob(str); + } catch (e) { + return false; + } + + // The string is likely Base64-encoded: + return true; +} + /** * An artificial sleep function to pause code execution * diff --git a/scripts/wallet.js b/scripts/wallet.js index 43a654e66..2c8b03622 100644 --- a/scripts/wallet.js +++ b/scripts/wallet.js @@ -473,12 +473,22 @@ export function deriveAddress({ pkBytes, publicKey, output = 'ENCODED' }) { return bs58.encode(pubKeyPreBase); } -// Wallet Import +/** + * Import a wallet (with it's private, public or encrypted data) + * @param {object} options + * @param {string | Array} options.newWif - The import data (if omitted, the UI input is accessed) + * @param {boolean} options.fRaw - Whether the import data is raw bytes or encoded (WIF, xpriv, seed) + * @param {boolean} options.isHardwareWallet - Whether the import is from a Hardware wallet or not + * @param {boolean} options.skipConfirmation - Whether to skip the import UI confirmation or not + * @param {boolean} options.fSavePublicKey - Whether to save the derived public key to disk (for View Only mode) + * @returns {Promise} + */ export async function importWallet({ newWif = false, fRaw = false, isHardwareWallet = false, skipConfirmation = false, + fSavePublicKey = false, } = {}) { const strImportConfirm = "Do you really want to import a new address? If you haven't saved the last private key, the wallet will be LOST forever."; @@ -596,6 +606,11 @@ export async function importWallet({ } } + // If allowed and requested, save the public key to disk for future View Only mode + if (fSavePublicKey && !masterKey.isHardwareWallet) { + localStorage.setItem('publicKey', await masterKey.keyToExport); + } + // For non-HD wallets: hide the 'new address' button, since these are essentially single-address MPW wallets if (!masterKey.isHD) doms.domNewAddress.style.display = 'none'; @@ -612,17 +627,28 @@ export async function importWallet({ ); jdenticon.update('#identicon'); - // Hide the encryption warning if the user pasted the private key - // Or in Testnet mode or is using a hardware wallet or is view-only mode + // Hide the encryption prompt if the user is in Testnet mode + // ... or is using a hardware wallet, or is view-only mode. if ( !( - newWif || cChainParams.current.isTestnet || isHardwareWallet || masterKey.isViewOnly ) - ) - doms.domGenKeyWarning.style.display = 'block'; + ) { + if ( + // If the wallet was internally imported (not UI pasted), like via vanity, display the encryption prompt + (((fRaw && newWif.length) || newWif) && + !hasEncryptedWallet()) || + // If the wallet was pasted and is an unencrypted key, then display the encryption prompt + !hasEncryptedWallet() + ) { + doms.domGenKeyWarning.style.display = 'block'; + } else if (hasEncryptedWallet()) { + // If the wallet was pasted and is an encrypted import, display the lock wallet UI + doms.domWipeWallet.hidden = false; + } + } // Fetch state from explorer if (getNetwork().enabled) refreshChainData(); @@ -757,9 +783,9 @@ export async function decryptWallet(strPassword = '') { await importWallet({ newWif: strDecWIF, skipConfirmation: true, + // Save the public key to disk for View Only mode + fSavePublicKey: true, }); - // Ensure publicKey is set - localStorage.setItem('publicKey', await masterKey.keyToExport); return true; } }