+ await Fs.rename(filepath, bakpath);
+ }
+ await Fs.rename(tmpPath, filepath);
+ if (!err) {
+ await Fs.unlink(bakpath);
+ }
+}
+
+/**
+ * @param {Null} psuedoState
+ * @param {Array<String>} args
+ */
+cmds.getPassphrase = async function (psuedoState, args) {
+ // Three possible states:
+ // 1. no shadow file yet (ask to set one)
+ // 2. empty shadow file (initialized, but not set - don't ask to set one)
+ // 3. encrypted shadow file (initialized, requires passphrase)
+ let needsInit = false;
+ let shadow = await Fs.readFile(shadowPath, "utf8").catch(function (err) {
+ if ("ENOENT" === err.code) {
+ needsInit = true;
+ return;
+ }
+ throw err;
+ });
+
+ // State 1: not initialized, what does the user want?
+ if (needsInit) {
+ for (;;) {
+ let no = await Prompt.prompt(
+ "Would you like to set an encryption passphrase? [Y/n]: ",
+ );
+
+ // Set a passphrase and create shadow file
+ if (!no || ["yes", "y"].includes(no.toLowerCase())) {
+ let passphrase = await setPassphrase(
+ { _askPreviousPassphrase: false },
+ args,
+ );
+ cmds._setPassphrase(passphrase);
+ return passphrase;
+ }
+
+ // ask user again
+ if (!["no", "n"].includes(no.toLowerCase())) {
+ continue;
+ }
+
+ // No passphrase, create empty shadow file
+ await Fs.writeFile(shadowPath, "", "utf8");
+ return "";
+ }
+ }
+
+ // State 2: shadow already initialized to empty
+ // (user doesn't want a passphrase)
+ if (!shadow) {
+ cmds._setPassphrase("");
+ return "";
+ }
+
+ // State 3: passphrase & shadow already in use
+ for (;;) {
+ let passphrase = await Prompt.prompt("Enter (current) passphrase: ", {
+ mask: true,
+ });
+ passphrase = passphrase.trim();
+ if (!passphrase || "q" === passphrase) {
+ console.error("cancel: no passphrase");
+ process.exit(1);
+ return;
+ }
+
+ let match = await Cipher.checkPassphrase(passphrase, shadow);
+ if (match) {
+ cmds._setPassphrase(passphrase);
+ console.info(``);
+ return passphrase;
+ }
+
+ console.error("incorrect passphrase");
+ }
+
+ throw new Error("SANITY FAIL: unreachable return");
+};
+
+cmds._getPassphrase = function () {
+ return "";
+};
+
+/**
+ * @param {String} passphrase
+ */
+cmds._setPassphrase = function (passphrase) {
+ // Look Ma! A private variable!
+ cmds._getPassphrase = function () {
+ return passphrase;
+ };
+};
+
+/**
+ * @typedef {Object} RawKey
+ * @property {String} addr
+ * @property {Boolean} encrypted
+ * @property {String} wif
+ */
+
+/**
+ * @throws
+ */
+async function readAllKeys() {
+ let wifnames = await listManagedKeynames();
+
+ /** @type Array<RawKey> */
+ let keys = [];
+ await wifnames.reduce(async function (promise, wifname) {
+ await promise;
+
+ let keypath = Path.join(keysDir, wifname);
+ let key = await maybeReadKeyFileRaw(keypath);
+ if (!key?.wif) {
+ return;
+ }
+
+ if (`${key.addr}.wif` !== wifname) {
+ throw new Error(
+ `computed pubkey '${key.addr}' of WIF does not match filename '${keypath}'`,
+ );
+ }
+
+ keys.push(key);
+ }, Promise.resolve());
+
+ return keys;
+}
+
+/**
+ * @param {String} filepath
+ * @param {Object} [opts]
+ * @param {Boolean} opts.wif
+ * @returns {Promise<String>}
+ */
+async function maybeReadKeyFile(filepath, opts) {
+ let key = await maybeReadKeyFileRaw(filepath, opts);
+ if (false === opts?.wif) {
+ return key?.addr || "";
+ }
+ return key?.wif || "";
+}
+
+/**
+ * @param {String} filepath
+ * @param {Object} [opts]
+ * @param {Boolean} opts.wif
+ * @returns {Promise<RawKey?>}
+ */
+async function maybeReadKeyFileRaw(filepath, opts) {
+ let privKey = await Fs.readFile(filepath, "utf8").catch(
+ emptyStringOnErrEnoent,
+ );
+ privKey = privKey.trim();
+ if (!privKey) {
+ return null;
+ }
+
+ let encrypted = false;
+ if (privKey.includes(":")) {
+ encrypted = true;
+ try {
+ if (false !== opts?.wif) {
+ privKey = await decrypt(privKey);
+ }
+ } catch (err) {
+ //@ts-ignore
+ console.error(err.message);
+ console.error(`passphrase does not match for key ${filepath}`);
+ process.exit(1);
+ }
+ }
+ if (false === opts?.wif) {
+ return {
+ addr: Path.basename(filepath, ".wif"),
+ encrypted: encrypted,
+ wif: "",
+ };
+ }
+
+ let pk = new Dashcore.PrivateKey(privKey);
+ let pub = pk.toAddress().toString();
+
+ return {
+ addr: pub,
+ encrypted: encrypted,
+ wif: privKey,
+ };
+}
+
+/**
+ * @param {String} encWif
+ */
+async function decrypt(encWif) {
+ let passphrase = cmds._getPassphrase();
+ if (!passphrase) {
+ passphrase = await cmds.getPassphrase(null, []);
+ }
+ let key128 = await Cipher.deriveKey(passphrase);
+ let cipher = Cipher.create(key128);
+
+ return cipher.decrypt(encWif);
+}
+
+/**
+ * @param {String} plainWif
+ */
+async function maybeEncrypt(plainWif) {
+ let passphrase = cmds._getPassphrase();
+ if (!passphrase) {
+ passphrase = await cmds.getPassphrase(null, []);
+ }
+ if (!passphrase) {
+ return plainWif;
+ }
+
+ let key128 = await Cipher.deriveKey(passphrase);
+ let cipher = Cipher.create(key128);
+ return cipher.encrypt(plainWif);
+}
+
+/**
+ * @param {Null} _
+ * @param {Array<String>} args
+ */
+async function setDefault(_, args) {
+ let addr = args.shift() || "";
+
+ let keyname = await findWif(addr);
+ if (!keyname) {
+ console.error(`no key matches '${addr}'`);
+ process.exit(1);