json-keystore.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. "use strict";
  2. /**
  3. * The JSON Wallet formats allow a simple way to store the private
  4. * keys needed in Ethereum along with related information and allows
  5. * for extensible forms of encryption.
  6. *
  7. * These utilities facilitate decrypting and encrypting the most common
  8. * JSON Wallet formats.
  9. *
  10. * @_subsection: api/wallet:JSON Wallets [json-wallets]
  11. */
  12. Object.defineProperty(exports, "__esModule", { value: true });
  13. exports.encryptKeystoreJson = exports.encryptKeystoreJsonSync = exports.decryptKeystoreJson = exports.decryptKeystoreJsonSync = exports.isKeystoreJson = void 0;
  14. const aes_js_1 = require("aes-js");
  15. const index_js_1 = require("../address/index.js");
  16. const index_js_2 = require("../crypto/index.js");
  17. const index_js_3 = require("../transaction/index.js");
  18. const index_js_4 = require("../utils/index.js");
  19. const utils_js_1 = require("./utils.js");
  20. const _version_js_1 = require("../_version.js");
  21. const defaultPath = "m/44'/60'/0'/0/0";
  22. /**
  23. * Returns true if %%json%% is a valid JSON Keystore Wallet.
  24. */
  25. function isKeystoreJson(json) {
  26. try {
  27. const data = JSON.parse(json);
  28. const version = ((data.version != null) ? parseInt(data.version) : 0);
  29. if (version === 3) {
  30. return true;
  31. }
  32. }
  33. catch (error) { }
  34. return false;
  35. }
  36. exports.isKeystoreJson = isKeystoreJson;
  37. function decrypt(data, key, ciphertext) {
  38. const cipher = (0, utils_js_1.spelunk)(data, "crypto.cipher:string");
  39. if (cipher === "aes-128-ctr") {
  40. const iv = (0, utils_js_1.spelunk)(data, "crypto.cipherparams.iv:data!");
  41. const aesCtr = new aes_js_1.CTR(key, iv);
  42. return (0, index_js_4.hexlify)(aesCtr.decrypt(ciphertext));
  43. }
  44. (0, index_js_4.assert)(false, "unsupported cipher", "UNSUPPORTED_OPERATION", {
  45. operation: "decrypt"
  46. });
  47. }
  48. function getAccount(data, _key) {
  49. const key = (0, index_js_4.getBytes)(_key);
  50. const ciphertext = (0, utils_js_1.spelunk)(data, "crypto.ciphertext:data!");
  51. const computedMAC = (0, index_js_4.hexlify)((0, index_js_2.keccak256)((0, index_js_4.concat)([key.slice(16, 32), ciphertext]))).substring(2);
  52. (0, index_js_4.assertArgument)(computedMAC === (0, utils_js_1.spelunk)(data, "crypto.mac:string!").toLowerCase(), "incorrect password", "password", "[ REDACTED ]");
  53. const privateKey = decrypt(data, key.slice(0, 16), ciphertext);
  54. const address = (0, index_js_3.computeAddress)(privateKey);
  55. if (data.address) {
  56. let check = data.address.toLowerCase();
  57. if (!check.startsWith("0x")) {
  58. check = "0x" + check;
  59. }
  60. (0, index_js_4.assertArgument)((0, index_js_1.getAddress)(check) === address, "keystore address/privateKey mismatch", "address", data.address);
  61. }
  62. const account = { address, privateKey };
  63. // Version 0.1 x-ethers metadata must contain an encrypted mnemonic phrase
  64. const version = (0, utils_js_1.spelunk)(data, "x-ethers.version:string");
  65. if (version === "0.1") {
  66. const mnemonicKey = key.slice(32, 64);
  67. const mnemonicCiphertext = (0, utils_js_1.spelunk)(data, "x-ethers.mnemonicCiphertext:data!");
  68. const mnemonicIv = (0, utils_js_1.spelunk)(data, "x-ethers.mnemonicCounter:data!");
  69. const mnemonicAesCtr = new aes_js_1.CTR(mnemonicKey, mnemonicIv);
  70. account.mnemonic = {
  71. path: ((0, utils_js_1.spelunk)(data, "x-ethers.path:string") || defaultPath),
  72. locale: ((0, utils_js_1.spelunk)(data, "x-ethers.locale:string") || "en"),
  73. entropy: (0, index_js_4.hexlify)((0, index_js_4.getBytes)(mnemonicAesCtr.decrypt(mnemonicCiphertext)))
  74. };
  75. }
  76. return account;
  77. }
  78. function getDecryptKdfParams(data) {
  79. const kdf = (0, utils_js_1.spelunk)(data, "crypto.kdf:string");
  80. if (kdf && typeof (kdf) === "string") {
  81. if (kdf.toLowerCase() === "scrypt") {
  82. const salt = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.salt:data!");
  83. const N = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.n:int!");
  84. const r = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.r:int!");
  85. const p = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.p:int!");
  86. // Make sure N is a power of 2
  87. (0, index_js_4.assertArgument)(N > 0 && (N & (N - 1)) === 0, "invalid kdf.N", "kdf.N", N);
  88. (0, index_js_4.assertArgument)(r > 0 && p > 0, "invalid kdf", "kdf", kdf);
  89. const dkLen = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.dklen:int!");
  90. (0, index_js_4.assertArgument)(dkLen === 32, "invalid kdf.dklen", "kdf.dflen", dkLen);
  91. return { name: "scrypt", salt, N, r, p, dkLen: 64 };
  92. }
  93. else if (kdf.toLowerCase() === "pbkdf2") {
  94. const salt = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.salt:data!");
  95. const prf = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.prf:string!");
  96. const algorithm = prf.split("-").pop();
  97. (0, index_js_4.assertArgument)(algorithm === "sha256" || algorithm === "sha512", "invalid kdf.pdf", "kdf.pdf", prf);
  98. const count = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.c:int!");
  99. const dkLen = (0, utils_js_1.spelunk)(data, "crypto.kdfparams.dklen:int!");
  100. (0, index_js_4.assertArgument)(dkLen === 32, "invalid kdf.dklen", "kdf.dklen", dkLen);
  101. return { name: "pbkdf2", salt, count, dkLen, algorithm };
  102. }
  103. }
  104. (0, index_js_4.assertArgument)(false, "unsupported key-derivation function", "kdf", kdf);
  105. }
  106. /**
  107. * Returns the account details for the JSON Keystore Wallet %%json%%
  108. * using %%password%%.
  109. *
  110. * It is preferred to use the [async version](decryptKeystoreJson)
  111. * instead, which allows a [[ProgressCallback]] to keep the user informed
  112. * as to the decryption status.
  113. *
  114. * This method will block the event loop (freezing all UI) until decryption
  115. * is complete, which can take quite some time, depending on the wallet
  116. * paramters and platform.
  117. */
  118. function decryptKeystoreJsonSync(json, _password) {
  119. const data = JSON.parse(json);
  120. const password = (0, utils_js_1.getPassword)(_password);
  121. const params = getDecryptKdfParams(data);
  122. if (params.name === "pbkdf2") {
  123. const { salt, count, dkLen, algorithm } = params;
  124. const key = (0, index_js_2.pbkdf2)(password, salt, count, dkLen, algorithm);
  125. return getAccount(data, key);
  126. }
  127. (0, index_js_4.assert)(params.name === "scrypt", "cannot be reached", "UNKNOWN_ERROR", { params });
  128. const { salt, N, r, p, dkLen } = params;
  129. const key = (0, index_js_2.scryptSync)(password, salt, N, r, p, dkLen);
  130. return getAccount(data, key);
  131. }
  132. exports.decryptKeystoreJsonSync = decryptKeystoreJsonSync;
  133. function stall(duration) {
  134. return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); });
  135. }
  136. /**
  137. * Resolves to the decrypted JSON Keystore Wallet %%json%% using the
  138. * %%password%%.
  139. *
  140. * If provided, %%progress%% will be called periodically during the
  141. * decrpytion to provide feedback, and if the function returns
  142. * ``false`` will halt decryption.
  143. *
  144. * The %%progressCallback%% will **always** receive ``0`` before
  145. * decryption begins and ``1`` when complete.
  146. */
  147. async function decryptKeystoreJson(json, _password, progress) {
  148. const data = JSON.parse(json);
  149. const password = (0, utils_js_1.getPassword)(_password);
  150. const params = getDecryptKdfParams(data);
  151. if (params.name === "pbkdf2") {
  152. if (progress) {
  153. progress(0);
  154. await stall(0);
  155. }
  156. const { salt, count, dkLen, algorithm } = params;
  157. const key = (0, index_js_2.pbkdf2)(password, salt, count, dkLen, algorithm);
  158. if (progress) {
  159. progress(1);
  160. await stall(0);
  161. }
  162. return getAccount(data, key);
  163. }
  164. (0, index_js_4.assert)(params.name === "scrypt", "cannot be reached", "UNKNOWN_ERROR", { params });
  165. const { salt, N, r, p, dkLen } = params;
  166. const key = await (0, index_js_2.scrypt)(password, salt, N, r, p, dkLen, progress);
  167. return getAccount(data, key);
  168. }
  169. exports.decryptKeystoreJson = decryptKeystoreJson;
  170. function getEncryptKdfParams(options) {
  171. // Check/generate the salt
  172. const salt = (options.salt != null) ? (0, index_js_4.getBytes)(options.salt, "options.salt") : (0, index_js_2.randomBytes)(32);
  173. // Override the scrypt password-based key derivation function parameters
  174. let N = (1 << 17), r = 8, p = 1;
  175. if (options.scrypt) {
  176. if (options.scrypt.N) {
  177. N = options.scrypt.N;
  178. }
  179. if (options.scrypt.r) {
  180. r = options.scrypt.r;
  181. }
  182. if (options.scrypt.p) {
  183. p = options.scrypt.p;
  184. }
  185. }
  186. (0, index_js_4.assertArgument)(typeof (N) === "number" && N > 0 && Number.isSafeInteger(N) && (BigInt(N) & BigInt(N - 1)) === BigInt(0), "invalid scrypt N parameter", "options.N", N);
  187. (0, index_js_4.assertArgument)(typeof (r) === "number" && r > 0 && Number.isSafeInteger(r), "invalid scrypt r parameter", "options.r", r);
  188. (0, index_js_4.assertArgument)(typeof (p) === "number" && p > 0 && Number.isSafeInteger(p), "invalid scrypt p parameter", "options.p", p);
  189. return { name: "scrypt", dkLen: 32, salt, N, r, p };
  190. }
  191. function _encryptKeystore(key, kdf, account, options) {
  192. const privateKey = (0, index_js_4.getBytes)(account.privateKey, "privateKey");
  193. // Override initialization vector
  194. const iv = (options.iv != null) ? (0, index_js_4.getBytes)(options.iv, "options.iv") : (0, index_js_2.randomBytes)(16);
  195. (0, index_js_4.assertArgument)(iv.length === 16, "invalid options.iv length", "options.iv", options.iv);
  196. // Override the uuid
  197. const uuidRandom = (options.uuid != null) ? (0, index_js_4.getBytes)(options.uuid, "options.uuid") : (0, index_js_2.randomBytes)(16);
  198. (0, index_js_4.assertArgument)(uuidRandom.length === 16, "invalid options.uuid length", "options.uuid", options.iv);
  199. // This will be used to encrypt the wallet (as per Web3 secret storage)
  200. // - 32 bytes As normal for the Web3 secret storage (derivedKey, macPrefix)
  201. // - 32 bytes AES key to encrypt mnemonic with (required here to be Ethers Wallet)
  202. const derivedKey = key.slice(0, 16);
  203. const macPrefix = key.slice(16, 32);
  204. // Encrypt the private key
  205. const aesCtr = new aes_js_1.CTR(derivedKey, iv);
  206. const ciphertext = (0, index_js_4.getBytes)(aesCtr.encrypt(privateKey));
  207. // Compute the message authentication code, used to check the password
  208. const mac = (0, index_js_2.keccak256)((0, index_js_4.concat)([macPrefix, ciphertext]));
  209. // See: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
  210. const data = {
  211. address: account.address.substring(2).toLowerCase(),
  212. id: (0, index_js_4.uuidV4)(uuidRandom),
  213. version: 3,
  214. Crypto: {
  215. cipher: "aes-128-ctr",
  216. cipherparams: {
  217. iv: (0, index_js_4.hexlify)(iv).substring(2),
  218. },
  219. ciphertext: (0, index_js_4.hexlify)(ciphertext).substring(2),
  220. kdf: "scrypt",
  221. kdfparams: {
  222. salt: (0, index_js_4.hexlify)(kdf.salt).substring(2),
  223. n: kdf.N,
  224. dklen: 32,
  225. p: kdf.p,
  226. r: kdf.r
  227. },
  228. mac: mac.substring(2)
  229. }
  230. };
  231. // If we have a mnemonic, encrypt it into the JSON wallet
  232. if (account.mnemonic) {
  233. const client = (options.client != null) ? options.client : `ethers/${_version_js_1.version}`;
  234. const path = account.mnemonic.path || defaultPath;
  235. const locale = account.mnemonic.locale || "en";
  236. const mnemonicKey = key.slice(32, 64);
  237. const entropy = (0, index_js_4.getBytes)(account.mnemonic.entropy, "account.mnemonic.entropy");
  238. const mnemonicIv = (0, index_js_2.randomBytes)(16);
  239. const mnemonicAesCtr = new aes_js_1.CTR(mnemonicKey, mnemonicIv);
  240. const mnemonicCiphertext = (0, index_js_4.getBytes)(mnemonicAesCtr.encrypt(entropy));
  241. const now = new Date();
  242. const timestamp = (now.getUTCFullYear() + "-" +
  243. (0, utils_js_1.zpad)(now.getUTCMonth() + 1, 2) + "-" +
  244. (0, utils_js_1.zpad)(now.getUTCDate(), 2) + "T" +
  245. (0, utils_js_1.zpad)(now.getUTCHours(), 2) + "-" +
  246. (0, utils_js_1.zpad)(now.getUTCMinutes(), 2) + "-" +
  247. (0, utils_js_1.zpad)(now.getUTCSeconds(), 2) + ".0Z");
  248. const gethFilename = ("UTC--" + timestamp + "--" + data.address);
  249. data["x-ethers"] = {
  250. client, gethFilename, path, locale,
  251. mnemonicCounter: (0, index_js_4.hexlify)(mnemonicIv).substring(2),
  252. mnemonicCiphertext: (0, index_js_4.hexlify)(mnemonicCiphertext).substring(2),
  253. version: "0.1"
  254. };
  255. }
  256. return JSON.stringify(data);
  257. }
  258. /**
  259. * Return the JSON Keystore Wallet for %%account%% encrypted with
  260. * %%password%%.
  261. *
  262. * The %%options%% can be used to tune the password-based key
  263. * derivation function parameters, explicitly set the random values
  264. * used. Any provided [[ProgressCallback]] is ignord.
  265. */
  266. function encryptKeystoreJsonSync(account, password, options) {
  267. if (options == null) {
  268. options = {};
  269. }
  270. const passwordBytes = (0, utils_js_1.getPassword)(password);
  271. const kdf = getEncryptKdfParams(options);
  272. const key = (0, index_js_2.scryptSync)(passwordBytes, kdf.salt, kdf.N, kdf.r, kdf.p, 64);
  273. return _encryptKeystore((0, index_js_4.getBytes)(key), kdf, account, options);
  274. }
  275. exports.encryptKeystoreJsonSync = encryptKeystoreJsonSync;
  276. /**
  277. * Resolved to the JSON Keystore Wallet for %%account%% encrypted
  278. * with %%password%%.
  279. *
  280. * The %%options%% can be used to tune the password-based key
  281. * derivation function parameters, explicitly set the random values
  282. * used and provide a [[ProgressCallback]] to receive periodic updates
  283. * on the completion status..
  284. */
  285. async function encryptKeystoreJson(account, password, options) {
  286. if (options == null) {
  287. options = {};
  288. }
  289. const passwordBytes = (0, utils_js_1.getPassword)(password);
  290. const kdf = getEncryptKdfParams(options);
  291. const key = await (0, index_js_2.scrypt)(passwordBytes, kdf.salt, kdf.N, kdf.r, kdf.p, 64, options.progressCallback);
  292. return _encryptKeystore((0, index_js_4.getBytes)(key), kdf, account, options);
  293. }
  294. exports.encryptKeystoreJson = encryptKeystoreJson;
  295. //# sourceMappingURL=json-keystore.js.map