refactor!: signal that the user chose to not have a passphrase with 'NONE' as the...
[crowdnode.js/.git] / bin / crowdnode.js
index f3b1d88a4f6fc7db31cfe9e9aab9ab19e687c0de..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:
@@ -197,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);
@@ -610,33 +614,20 @@ async function generateKey({ defaultKey, plainText }, 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) {
+    // 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,
   );
@@ -659,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(``);
@@ -668,38 +659,32 @@ async function setPassphrase({ _askPreviousPassphrase }, args) {
 
   await encryptAll(rawKeys, { rotateKey: true });
 
-  return newPassphrase;
+  result.passphrase = newPassphrase;
+  result.changed = true;
+  return result;
 }
 
-/**
- * Import and Encrypt
- * @param {Null} _
- * @param {Array<String>} args
- */
-async function importKey(_, args) {
-  let keypath = args.shift() || "";
-  let key = await maybeReadKeyFileRaw(keypath);
-  if (!key?.wif) {
-    console.error(`no key found for '${keypath}'`);
-    process.exit(1);
-    return;
-  }
+async function promptPassphrase() {
+  let newPassphrase;
+  for (;;) {
+    newPassphrase = await Prompt.prompt("Enter (new) passphrase: ", {
+      mask: true,
+    });
+    newPassphrase = newPassphrase.trim();
 
-  let encWif = await maybeEncrypt(key.wif);
-  let icon = "💾";
-  if (encWif.includes(":")) {
-    icon = "🔐";
-  }
-  let date = getFsDateString();
+    let _newPassphrase = await Prompt.prompt("Enter passphrase again: ", {
+      mask: true,
+    });
+    _newPassphrase = _newPassphrase.trim();
 
-  await safeSave(
-    Path.join(keysDir, `${key.addr}.wif`),
-    encWif,
-    Path.join(keysDir, `${key.addr}.${date}.bak`),
-  );
+    let match = Cipher.secureCompare(newPassphrase, _newPassphrase);
+    if (match) {
+      break;
+    }
 
-  console.info(`${icon} Imported ${keysDirRel}/${key.addr}.wif`);
-  console.info(``);
+    console.error("passphrases do not match");
+  }
+  return newPassphrase;
 }
 
 /**
@@ -745,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) {
@@ -754,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,
@@ -830,37 +825,52 @@ async function safeSave(filepath, wif, bakpath) {
 /**
  * @param {Object} opts
  * @param {Boolean} [opts._rotatePassphrase]
+ * @param {Boolean} [opts._force]
  * @param {Array<String>} args
  */
-cmds.getPassphrase = async function ({ _rotatePassphrase }, 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
@@ -868,9 +878,9 @@ cmds.getPassphrase = async function ({ _rotatePassphrase }, 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;
     }
   }
 
@@ -878,7 +888,7 @@ cmds.getPassphrase = async function ({ _rotatePassphrase }, args) {
   // (user doesn't want a passphrase)
   if (!shadow) {
     cmds._setPassphrase("");
-    return "";
+    return result;
   }
 
   // State 3: passphrase & shadow already in use
@@ -887,21 +897,21 @@ cmds.getPassphrase = async function ({ _rotatePassphrase }, args) {
     if (_rotatePassphrase) {
       prompt = `Enter (current) passphrase: `;
     }
-    let passphrase = await Prompt.prompt(prompt, {
+    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");
@@ -1027,7 +1037,10 @@ async function maybeReadKeyFileRaw(filepath, opts) {
 async function decrypt(encWif) {
   let passphrase = cmds._getPassphrase();
   if (!passphrase) {
-    passphrase = await cmds.getPassphrase({}, []);
+    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);
@@ -1035,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({}, []);
+    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;
   }