feat: fully managed, encrypted staking keys
[crowdnode.js/.git] / bin / _cipher.js
1 "use strict";
2
3 let Crypto = require("crypto");
4
5 let Cipher = module.exports;
6
7 const ALG = "aes-128-cbc";
8 const IV_SIZE = 16;
9
10 /**
11  * @param {String} passphrase - what the human entered
12  * @param {String} shadow - encrypted, hashed, key-expanded passphrase
13  */
14 Cipher.checkPassphrase = async function (passphrase, shadow) {
15   let key128 = await Cipher.deriveKey(passphrase);
16   let cipher = Cipher.create(key128);
17
18   let plainShadow;
19   try {
20     plainShadow = cipher.decrypt(shadow);
21   } catch (e) {
22     //@ts-ignore
23     let msg = e.message;
24     if (!msg.includes("decrypt")) {
25       throw e;
26     }
27     return false;
28   }
29
30   let untrustedShadow = Crypto.createHash("sha512")
31     .update(key128)
32     .digest("base64");
33   return Cipher.secureCompare(plainShadow, untrustedShadow);
34 };
35
36 /**
37  * @param {String} passphrase - what the human entered
38  */
39 Cipher.shadowPassphrase = async function (passphrase) {
40   let key128 = await Cipher.deriveKey(passphrase);
41   let plainShadow = Crypto.createHash("sha512").update(key128).digest("base64");
42   let cipher = Cipher.create(key128);
43   let shadow = cipher.encrypt(plainShadow);
44
45   return shadow;
46 };
47
48 /**
49  * @param {String} passphrase
50  */
51 Cipher.deriveKey = async function (passphrase) {
52   // See https://crypto.stackexchange.com/a/6557
53   // and https://nodejs.org/api/crypto.html#cryptohkdfdigest-ikm-salt-info-keylen-callback
54   const DIGEST = "sha512";
55   const SALT = Buffer.from("crowdnode-cli", "utf8");
56   // 'info' is a string describing a sub-context
57   const INFO = Buffer.from("staking-keys", "utf8");
58   const SIZE = 16;
59
60   let ikm = Buffer.from(passphrase, "utf8");
61   let key128 = await new Promise(function (resolve, reject) {
62     //@ts-ignore
63     Crypto.hkdf(DIGEST, ikm, SALT, INFO, SIZE, function (err, key128) {
64       if (err) {
65         reject(err);
66         return;
67       }
68       resolve(Buffer.from(key128));
69     });
70   });
71
72   return key128;
73 };
74
75 /**
76  * @param {String} shadow
77  * @param {Buffer} key128
78  */
79 Cipher.checkShadow = function (shadow, key128) {
80   let untrustedShadow = Crypto.createHash("sha512")
81     .update(key128)
82     .digest("base64");
83   return Cipher.secureCompare(shadow, untrustedShadow);
84 };
85
86 /**
87  * @param {String} a
88  * @param {String} b
89  */
90 Cipher.secureCompare = function (a, b) {
91   if (!a && !b) {
92     throw new Error("[secure compare] reference string should not be empty");
93   }
94
95   if (a.length !== b.length) {
96     return false;
97   }
98
99   return Crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
100 };
101
102 /**
103  * @param {Buffer} key128
104  */
105 Cipher.create = function (key128) {
106   //let sharedSecret = Buffer.from(key128, "base64");
107
108   let cipher = {};
109
110   /**
111    * @param {String} plaintext
112    */
113   cipher.encrypt = function (plaintext) {
114     let initializationVector = Crypto.randomBytes(IV_SIZE); // IV is always 16-bytes
115     let encrypted = "";
116
117     let _cipher = Crypto.createCipheriv(ALG, key128, initializationVector);
118     encrypted += _cipher.update(plaintext, "utf8", "base64");
119     encrypted += _cipher.final("base64");
120
121     return (
122       toWeb64(initializationVector.toString("base64")) +
123       ":" +
124       toWeb64(encrypted) +
125       ":" +
126       // as a backup
127       toWeb64(initializationVector.toString("base64"))
128     );
129   };
130
131   /**
132    * @param {String} parts
133    */
134   cipher.decrypt = function (parts) {
135     let [initializationVector, encrypted, initializationVectorBak] =
136       parts.split(":");
137     let plaintext = "";
138     if (initializationVector !== initializationVectorBak) {
139       console.error("corrupt (but possibly recoverable) initialization vector");
140     }
141
142     let iv = Buffer.from(initializationVector, "base64");
143     let _cipher = Crypto.createDecipheriv(ALG, key128, iv);
144     plaintext += _cipher.update(encrypted, "base64", "utf8");
145     plaintext += _cipher.final("utf8");
146
147     return plaintext;
148   };
149
150   return cipher;
151 };
152
153 /**
154  * @param {String} x
155  */
156 function toWeb64(x) {
157   return x.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
158 }