json-keystore.js 12 KB

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