fix: don't re-run encrypt, delete shadow on decrypt
[crowdnode.js/.git] / bin / crowdnode.js
index 2cf8cdfaf269a9dc74b4f50be7449d4ee633fa9d..57e49a330eff5e32b572b1b861ea58d80bc0c2de 100755 (executable)
@@ -74,7 +74,7 @@ function showHelp() {
   console.info("");
 
   console.info("Key Management & Encryption:");
-  console.info("    crowdnode generate [./privkey.wif]");
+  console.info("    crowdnode generate [--plain-text] [./privkey.wif]");
   console.info("    crowdnode encrypt"); // TODO allow encrypting one-by-one?
   console.info("    crowdnode list");
   console.info("    crowdnode use <addr>");
@@ -119,6 +119,7 @@ async function main() {
 
   // flags
   let forceConfirm = removeItem(args, "--unconfirmed");
+  let plainText = removeItem(args, "--plain-text");
   let noReserve = removeItem(args, "--no-reserve");
 
   let subcommand = args.shift();
@@ -158,7 +159,7 @@ async function main() {
   }
 
   if ("generate" === subcommand) {
-    await generateKey({ defaultKey: defaultAddr }, args);
+    await generateKey({ defaultKey: defaultAddr, plainText }, args);
     return;
   }
 
@@ -196,7 +197,8 @@ async function main() {
   if ("decrypt" === subcommand) {
     let addr = args.shift() || "";
     if (!addr) {
-      decryptAll(null);
+      await decryptAll(null);
+      await Fs.writeFile(shadowPath, "", "utf8").catch(emptyStringOnErrEnoent);
       return;
     }
     let keypath = await findWif(addr);
@@ -330,12 +332,13 @@ async function initCrowdNode(insightBaseUrl) {
  * @param {Number} duffs - 1/100000000 of a DASH
  */
 function showQr(addr, duffs = 0) {
+  let dashAmount = toDash(duffs);
   let dashUri = `dash://${addr}`;
   if (duffs) {
-    dashUri += `?amount=${duffs}`;
+    dashUri += `?amount=${dashAmount}`;
   }
 
-  let dashQr = Qr.ascii(dashUri, { indent: 4 });
+  let dashQr = Qr.ascii(dashUri, { indent: 4, size: "mini" });
   let addrPad = Math.ceil((qrWidth - dashUri.length) / 2);
 
   console.info(dashQr);
@@ -559,9 +562,10 @@ async function mustGetDefaultWif(defaultAddr, opts) {
 /**
  * @param {Object} psuedoState
  * @param {String} psuedoState.defaultKey - addr name of default key
+ * @param {Boolean} psuedoState.plainText - don't encrypt
  * @param {Array<String>} args
  */
-async function generateKey({ defaultKey }, args) {
+async function generateKey({ defaultKey, plainText }, args) {
   let name = args.shift();
   //@ts-ignore - TODO submit JSDoc PR for Dashcore
   let pk = new Dashcore.PrivateKey();
@@ -569,7 +573,10 @@ async function generateKey({ defaultKey }, args) {
   let addr = pk.toAddress().toString();
   let plainWif = pk.toWIF();
 
-  let wif = await maybeEncrypt(plainWif);
+  let wif = plainWif;
+  if (!plainText) {
+    wif = await maybeEncrypt(plainWif);
+  }
 
   let filename = `~/${configdir}/keys/${addr}.wif`;
   let filepath = Path.join(`${keysDir}/${addr}.wif`);
@@ -604,33 +611,19 @@ async function generateKey({ defaultKey }, args) {
  * @param {Array<String>} args
  */
 async function setPassphrase({ _askPreviousPassphrase }, args) {
+  let result = {
+    passphrase: "",
+    changed: false,
+  };
   let date = getFsDateString();
 
   // get the old passphrase
   if (false !== _askPreviousPassphrase) {
-    await cmds.getPassphrase(null, []);
+    await cmds.getPassphrase({ _rotatePassphrase: true }, []);
   }
 
   // get the new passphrase
-  let newPassphrase;
-  for (;;) {
-    newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
-      mask: true,
-    });
-    newPassphrase = newPassphrase.trim();
-
-    let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
-      mask: true,
-    });
-    _newPassphrase = _newPassphrase.trim();
-
-    let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
-    if (match) {
-      break;
-    }
-
-    console.error("passphrases do not match");
-  }
+  let newPassphrase = await promptPassphrase();
   let curShadow = await Fs.readFile(shadowPath, "utf8").catch(
     emptyStringOnErrEnoent,
   );
@@ -662,6 +655,31 @@ async function setPassphrase({ _askPreviousPassphrase }, args) {
 
   await encryptAll(rawKeys, { rotateKey: true });
 
+  result.passphrase = newPassphrase;
+  result.changed = true;
+  return result;
+}
+
+async function promptPassphrase() {
+  let newPassphrase;
+  for (;;) {
+    newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
+      mask: true,
+    });
+    newPassphrase = newPassphrase.trim();
+
+    let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
+      mask: true,
+    });
+    _newPassphrase = _newPassphrase.trim();
+
+    let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
+    if (match) {
+      break;
+    }
+
+    console.error("passphrases do not match");
+  }
   return newPassphrase;
 }
 
@@ -708,6 +726,16 @@ async function encryptAll(rawKeys, opts) {
   }
   let date = getFsDateString();
 
+  let passphrase = cmds._getPassphrase();
+  if (!passphrase) {
+    let result = await cmds.getPassphrase({ _force: true }, []);
+    if (result.changed) {
+      // encryptAll was already called on rotation
+      return;
+    }
+    passphrase = result.passphrase;
+  }
+
   console.info(`Encrypting...`);
   console.info(``);
   await rawKeys.reduce(async function (promise, key) {
@@ -717,7 +745,7 @@ async function encryptAll(rawKeys, opts) {
       console.info(`🙈 ${key.addr} [already encrypted]`);
       return;
     }
-    let encWif = await maybeEncrypt(key.wif);
+    let encWif = await maybeEncrypt(key.wif, { force: true });
     await safeSave(
       Path.join(keysDir, `${key.addr}.wif`),
       encWif,
@@ -791,10 +819,25 @@ async function safeSave(filepath, wif, bakpath) {
 }
 
 /**
- * @param {Null} psuedoState
+ * @param {Object} opts
+ * @param {Boolean} [opts._rotatePassphrase]
+ * @param {Boolean} [opts._force]
  * @param {Array<String>} args
  */
-cmds.getPassphrase = async function (psuedoState, args) {
+cmds.getPassphrase = async function ({ _rotatePassphrase, _force }, args) {
+  let result = {
+    passphrase: "",
+    changed: false,
+  };
+  /*
+  if (!_rotatePassphrase) {
+    let cachedphrase = cmds._getPassphrase();
+    if (cachedphrase) {
+      return cachedphrase;
+    }
+  }
+  */
+
   // 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)
@@ -807,22 +850,25 @@ cmds.getPassphrase = async function (psuedoState, args) {
     }
     throw err;
   });
+  if (!shadow && _force) {
+    needsInit = true;
+  }
 
   // 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]: ",
-      );
+      let no;
+      if (!_force) {
+        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;
+        result = await setPassphrase({ _askPreviousPassphrase: false }, args);
+        cmds._setPassphrase(result.passphrase);
+        return result;
       }
 
       // ask user again
@@ -832,7 +878,7 @@ cmds.getPassphrase = async function (psuedoState, args) {
 
       // No passphrase, create empty shadow file
       await Fs.writeFile(shadowPath, "", "utf8");
-      return "";
+      return result;
     }
   }
 
@@ -840,26 +886,30 @@ cmds.getPassphrase = async function (psuedoState, args) {
   // (user doesn't want a passphrase)
   if (!shadow) {
     cmds._setPassphrase("");
-    return "";
+    return result;
   }
 
   // State 3: passphrase & shadow already in use
   for (;;) {
-    let passphrase = await Prompt.prompt("Enter (current) passphrase: ", {
+    let prompt = `Enter passphrase: `;
+    if (_rotatePassphrase) {
+      prompt = `Enter (current) passphrase: `;
+    }
+    result.passphrase = await Prompt.prompt(prompt, {
       mask: true,
     });
-    passphrase = passphrase.trim();
-    if (!passphrase || "q" === passphrase) {
+    result.passphrase = result.passphrase.trim();
+    if (!result.passphrase || "q" === result.passphrase) {
       console.error("cancel: no passphrase");
       process.exit(1);
-      return;
+      return result;
     }
 
-    let match = await Cipher.checkPassphrase(passphrase, shadow);
+    let match = await Cipher.checkPassphrase(result.passphrase, shadow);
     if (match) {
-      cmds._setPassphrase(passphrase);
+      cmds._setPassphrase(result.passphrase);
       console.info(``);
-      return passphrase;
+      return result;
     }
 
     console.error("incorrect passphrase");
@@ -985,7 +1035,10 @@ async function maybeReadKeyFileRaw(filepath, opts) {
 async function decrypt(encWif) {
   let passphrase = cmds._getPassphrase();
   if (!passphrase) {
-    passphrase = await cmds.getPassphrase(null, []);
+    let result = await cmds.getPassphrase({}, []);
+    passphrase = result.passphrase;
+    // we don't return just in case they're setting a passphrase to
+    // decrypt a previously encrypted file (i.e. for recovery from elsewhere)
   }
   let key128 = await Cipher.deriveKey(passphrase);
   let cipher = Cipher.create(key128);
@@ -993,15 +1046,22 @@ async function decrypt(encWif) {
   return cipher.decrypt(encWif);
 }
 
+// tuple example {Promise<[String, Boolean]>}
 /**
+ * @param {Object} [opts]
+ * @param {Boolean} [opts.force]
  * @param {String} plainWif
  */
-async function maybeEncrypt(plainWif) {
+async function maybeEncrypt(plainWif, opts) {
   let passphrase = cmds._getPassphrase();
   if (!passphrase) {
-    passphrase = await cmds.getPassphrase(null, []);
+    let result = await cmds.getPassphrase({}, []);
+    passphrase = result.passphrase;
   }
   if (!passphrase) {
+    if (opts?.force) {
+      throw new Error(`no passphrase with which to encrypt file`);
+    }
     return plainWif;
   }