Skip to content

Commit

Permalink
Various import fixes (#108)
Browse files Browse the repository at this point in the history
* Import Fixes + JSDoc

* Fix modern encrypted imports

* Run Prettier
  • Loading branch information
JSKitty committed Apr 4, 2023
1 parent 60643e2 commit 23da164
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 14 deletions.
28 changes: 23 additions & 5 deletions scripts/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'
Expand All @@ -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();
Expand All @@ -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,
});
}
}
Expand Down
1 change: 0 additions & 1 deletion scripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,3 @@ export { Masternode };
export { getNetwork } from './network.js';
const toggleNetwork = () => getNetwork().toggle();
export { toggleNetwork };

29 changes: 29 additions & 0 deletions scripts/misc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
42 changes: 34 additions & 8 deletions scripts/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>} 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<void>}
*/
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.";
Expand Down Expand Up @@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand Down

0 comments on commit 23da164

Please sign in to comment.