You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

149 lines
5.5 KiB

  1. // Copyright (c) 2019-2020, Zpalmtree
  2. //
  3. // Please see the included LICENSE file for more information.
  4. import * as crypto from 'crypto';
  5. import * as pbkdf2 from 'pbkdf2';
  6. import {IS_A_WALLET_IDENTIFIER, IS_CORRECT_PASSWORD_IDENTIFIER, PBKDF2_ITERATIONS} from './Constants';
  7. import {WalletError, WalletErrorCode} from './WalletError';
  8. export class WalletEncryption {
  9. /**
  10. * Encrypt the wallet using the given password. Note that an empty password does not mean an
  11. * unencrypted wallet - simply a wallet encrypted with the empty string.
  12. *
  13. * This will take some time (Roughly a second on a modern PC) - it runs 500,000 iterations of pbkdf2.
  14. *
  15. * Example:
  16. * ```javascript
  17. * const dataJson = wallet.encryptWalletToString('hunter2');
  18. *
  19. * ```
  20. *
  21. * @param walletJson
  22. * @param password The password to encrypt the wallet with
  23. *
  24. * @return Returns a string containing the encrypted fileData.
  25. */
  26. public static encryptWalletToString(walletJson: string, password: string): string {
  27. const buffer = WalletEncryption.encryptWalletToBuffer(walletJson, password);
  28. return JSON.stringify(buffer);
  29. }
  30. /**
  31. * Encrypt the wallet using the given password. Note that an empty password does not mean an
  32. * unencrypted wallet - simply a wallet encrypted with the empty string.
  33. *
  34. * This will take some time (Roughly a second on a modern PC) - it runs 500,000 iterations of pbkdf2.
  35. *
  36. * Example:
  37. * ```javascript
  38. * const dataJson = wallet.encryptWalletToBuffer('hunter2');
  39. *
  40. * ```
  41. *
  42. * @param walletJson
  43. * @param password The password to encrypt the wallet with
  44. *
  45. * @return Returns a Buffer containing the encrypted fileData.
  46. */
  47. public static encryptWalletToBuffer(walletJson: string, password: string): Buffer {
  48. /* Append the identifier so we can verify the password is correct */
  49. const data: Buffer = Buffer.concat([
  50. IS_CORRECT_PASSWORD_IDENTIFIER,
  51. Buffer.from(walletJson),
  52. ]);
  53. /* Random salt */
  54. const salt: Buffer = crypto.randomBytes(16);
  55. /* PBKDF2 key for our encryption */
  56. const key: Buffer = pbkdf2.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 16, 'sha256');
  57. /* Encrypt with AES */
  58. const cipher = crypto.createCipheriv('aes-128-cbc', key, salt);
  59. /* Perform the encryption */
  60. const encryptedData: Buffer = Buffer.concat([
  61. cipher.update(data),
  62. cipher.final(),
  63. ]);
  64. /* Write the wallet identifier to the file so we know it's a wallet file.
  65. Write the salt so it can be decrypted again */
  66. return Buffer.concat([
  67. IS_A_WALLET_IDENTIFIER,
  68. salt,
  69. encryptedData,
  70. ]);
  71. }
  72. /**
  73. * Decrypt the wallet from the given encrypted string with the given password and return
  74. * a JSON string. Uses pbkdf2 encryption, not the same as fedoragold-service
  75. *
  76. * Returns the JSON, and an error. If error is not undefined, the JSON will
  77. * be an empty string.
  78. */
  79. public static decryptWalletFromString(dataString: string, password: string): [string, WalletError | undefined] {
  80. const data = Buffer.from(JSON.parse(dataString).data);
  81. return WalletEncryption.decryptWalletFromBuffer(data, password);
  82. }
  83. /**
  84. * Decrypt the wallet from the given Buffer with the given password and return
  85. * a JSON string. Uses pbkdf2 encryption, not the same as fedoragold-service
  86. *
  87. * Returns the JSON, and an error. If error is not undefined, the JSON will
  88. * be an empty string.
  89. */
  90. public static decryptWalletFromBuffer(data: Buffer, password: string): [string, WalletError | undefined] {
  91. /* Take a slice containing the wallet identifier magic bytes */
  92. const magicBytes1: Buffer = data.slice(0, IS_A_WALLET_IDENTIFIER.length);
  93. if (magicBytes1.compare(IS_A_WALLET_IDENTIFIER) !== 0) {
  94. return ['', new WalletError(WalletErrorCode.NOT_A_WALLET_FILE)];
  95. }
  96. /* Remove the magic bytes */
  97. data = data.slice(IS_A_WALLET_IDENTIFIER.length, data.length);
  98. /* Grab the salt from the data */
  99. const salt: Buffer = data.slice(0, 16);
  100. /* Remove the salt from the data */
  101. data = data.slice(salt.length, data.length);
  102. /* Derive our key with pbkdf2, 16 bytes long */
  103. const key: Buffer = pbkdf2.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, 16, 'sha256');
  104. /* Setup the aes decryption */
  105. const decipher = crypto.createDecipheriv('aes-128-cbc', key, salt);
  106. let decrypted: Buffer;
  107. try {
  108. /* Perform the decryption */
  109. decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
  110. } catch (err) {
  111. return ['', new WalletError(WalletErrorCode.WRONG_PASSWORD)];
  112. }
  113. /* Grab the second set of magic bytes */
  114. const magicBytes2: Buffer = decrypted.slice(0, IS_CORRECT_PASSWORD_IDENTIFIER.length);
  115. /* Verify the magic bytes are present */
  116. if (magicBytes2.compare(IS_CORRECT_PASSWORD_IDENTIFIER) !== 0) {
  117. return ['', new WalletError(WalletErrorCode.WRONG_PASSWORD)];
  118. }
  119. console.log("wallet decrypted, removing magic bytes...");
  120. /* Remove the magic bytes */
  121. decrypted = decrypted.slice(IS_CORRECT_PASSWORD_IDENTIFIER.length, decrypted.length);
  122. return [decrypted.toString(), undefined];
  123. }
  124. }