refactor!: signal that the user chose to not have a passphrase with 'NONE' as the...
[crowdnode.js/.git] / bin / crowdnode.js
index 2cf8cdfaf269a9dc74b4f50be7449d4ee633fa9d..6a13b9f57283e821254f052be936f7c6ed2c8d5a 100755 (executable)
@@ -23,6 +23,7 @@ let Ws = require("../lib/ws.js");
 
 let Dashcore = require("@dashevo/dashcore-lib");
 
+const NO_SHADOW = "NONE";
 const DUFFS = 100000000;
 let qrWidth = 2 + 67 + 2;
 // Sign Up Fees:
@@ -74,7 +75,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 +120,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 +160,7 @@ async function main() {
   }
 
   if ("generate" === subcommand) {
-    await generateKey({ defaultKey: defaultAddr }, args);
+    await generateKey({ defaultKey: defaultAddr, plainText }, args);
     return;
   }
 
@@ -196,7 +198,10 @@ async function main() {
   if ("decrypt" === subcommand) {
     let addr = args.shift() || "";
     if (!addr) {
-      decryptAll(null);
+      await decryptAll(null);
+      await Fs.writeFile(shadowPath, NO_SHADOW, "utf8").catch(
+        emptyStringOnErrEnoent,
+      );
       return;
     }
     let keypath = await findWif(addr);
@@ -330,12 +335,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 +565,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 +576,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 +614,20 @@ 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, []);
+    // TODO should contain the shadow?
+    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,
   );
@@ -653,7 +650,7 @@ async function setPassphrase({ _askPreviousPassphrase }, args) {
     let filepath = Path.join(HOME, `${configdir}/keys.${date}.bak`);
     console.info(``);
     console.info(`Backing up previous (encrypted) keys:`);
-    encAddrs.unshift(curShadow);
+    encAddrs.unshift(`SHADOW:${curShadow}`);
     await Fs.writeFile(filepath, encAddrs.join("\n") + "\n", "utf8");
     console.info(`  ~/${configdir}/keys.${date}.bak`);
     console.info(``);
@@ -662,6 +659,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 +730,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 +749,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,38 +823,54 @@ 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)
   //   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;
-  });
+  let shadow = await Fs.readFile(shadowPath, "utf8").catch(
+    emptyStringOnErrEnoent,
+  );
+  if (!shadow) {
+    needsInit = true;
+  } else if (NO_SHADOW === 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
@@ -830,9 +878,9 @@ cmds.getPassphrase = async function (psuedoState, args) {
         continue;
       }
 
-      // No passphrase, create empty shadow file
-      await Fs.writeFile(shadowPath, "", "utf8");
-      return "";
+      // No passphrase, create a NONE shadow file
+      await Fs.writeFile(shadowPath, NO_SHADOW, "utf8");
+      return result;
     }
   }
 
@@ -840,26 +888,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 +1037,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 +1048,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;
   }