keygrip.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. 'use strict';
  2. const debug = require('debug')('egg-cookies:keygrip');
  3. const crypto = require('crypto');
  4. const assert = require('assert');
  5. const constantTimeCompare = require('scmp');
  6. const KEY_LEN = 32;
  7. const IV_SIZE = 16;
  8. const passwordCache = new Map();
  9. const replacer = {
  10. '/': '_',
  11. '+': '-',
  12. '=': '',
  13. };
  14. // patch from https://github.com/crypto-utils/keygrip
  15. class Keygrip {
  16. constructor(keys) {
  17. assert(Array.isArray(keys) && keys.length, 'keys must be provided and should be an array');
  18. this.keys = keys;
  19. this.hash = 'sha256';
  20. this.cipher = 'aes-256-cbc';
  21. }
  22. // encrypt a message
  23. encrypt(data, key) {
  24. key = key || this.keys[0];
  25. const password = keyToPassword(key);
  26. const cipher = crypto.createCipheriv(this.cipher, password.key, password.iv);
  27. return crypt(cipher, data);
  28. }
  29. // decrypt a single message
  30. // returns false on bad decrypts
  31. decrypt(data, key) {
  32. if (!key) {
  33. // decrypt every key
  34. const keys = this.keys;
  35. for (let i = 0; i < keys.length; i++) {
  36. const value = this.decrypt(data, keys[i]);
  37. if (value !== false) return { value, index: i };
  38. }
  39. return false;
  40. }
  41. try {
  42. const password = keyToPassword(key);
  43. const cipher = crypto.createDecipheriv(this.cipher, password.key, password.iv);
  44. return crypt(cipher, data);
  45. } catch (err) {
  46. debug('crypt error', err.stack);
  47. return false;
  48. }
  49. }
  50. sign(data, key) {
  51. // default to the first key
  52. key = key || this.keys[0];
  53. return crypto
  54. .createHmac(this.hash, key)
  55. .update(data)
  56. .digest('base64')
  57. .replace(/\/|\+|=/g, x => replacer[x]);
  58. }
  59. verify(data, digest) {
  60. const keys = this.keys;
  61. for (let i = 0; i < keys.length; i++) {
  62. if (constantTimeCompare(Buffer.from(digest), Buffer.from(this.sign(data, keys[i])))) {
  63. debug('data %s match key %s', data, keys[i]);
  64. return i;
  65. }
  66. }
  67. return -1;
  68. }
  69. }
  70. function crypt(cipher, data) {
  71. const text = cipher.update(data, 'utf8');
  72. const pad = cipher.final();
  73. return Buffer.concat([ text, pad ]);
  74. }
  75. function keyToPassword(key) {
  76. if (passwordCache.has(key)) {
  77. return passwordCache.get(key);
  78. }
  79. // Simulate EVP_BytesToKey.
  80. // see https://github.com/nodejs/help/issues/1673#issuecomment-503222925
  81. const bytes = Buffer.alloc(KEY_LEN + IV_SIZE);
  82. let lastHash = null,
  83. nBytes = 0;
  84. while (nBytes < bytes.length) {
  85. const hash = crypto.createHash('md5');
  86. if (lastHash) hash.update(lastHash);
  87. hash.update(key);
  88. lastHash = hash.digest();
  89. lastHash.copy(bytes, nBytes);
  90. nBytes += lastHash.length;
  91. }
  92. // Use these for decryption.
  93. const password = {
  94. key: bytes.slice(0, KEY_LEN),
  95. iv: bytes.slice(KEY_LEN, bytes.length),
  96. };
  97. passwordCache.set(key, password);
  98. return password;
  99. }
  100. module.exports = Keygrip;