AccessTokenEncryptor.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. const crypto = require('crypto')
  2. const logger = require('@overleaf/logger')
  3. const ALGORITHM = 'aes-256-ctr'
  4. const keyFn = (password, salt, callback) =>
  5. crypto.pbkdf2(password, salt, 10000, 64, 'sha1', callback)
  6. const keyFn32 = (password, salt, keyLength, callback) =>
  7. crypto.pbkdf2(password, salt, 10000, 32, 'sha1', callback)
  8. class AccessTokenEncryptor {
  9. constructor(settings) {
  10. this.settings = settings
  11. this.cipherLabel = this.settings.cipherLabel
  12. if (this.cipherLabel && this.cipherLabel.match(/:/)) {
  13. throw Error('cipherLabel must not contain a colon (:)')
  14. }
  15. this.cipherPassword = this.settings.cipherPasswords[this.cipherLabel]
  16. if (!this.cipherPassword) {
  17. throw Error('cipherPassword not set')
  18. }
  19. if (this.cipherPassword.length < 16) {
  20. throw Error('cipherPassword too short')
  21. }
  22. }
  23. encryptJson(json, callback) {
  24. const string = JSON.stringify(json)
  25. crypto.randomBytes(32, (err, bytes) => {
  26. if (err) {
  27. return callback(err)
  28. }
  29. const salt = bytes.slice(0, 16)
  30. const iv = bytes.slice(16, 32)
  31. keyFn32(this.cipherPassword, salt, 32, (err, key) => {
  32. if (err) {
  33. logger.err({ err }, 'error getting Fn key')
  34. return callback(err)
  35. }
  36. const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
  37. const crypted =
  38. cipher.update(string, 'utf8', 'base64') + cipher.final('base64')
  39. callback(
  40. null,
  41. `${this.cipherLabel}:${salt.toString('hex')}:${crypted}:${iv.toString(
  42. 'hex'
  43. )}`
  44. )
  45. })
  46. })
  47. }
  48. decryptToJson(encryptedJson, callback) {
  49. const [label, salt, cipherText, iv] = encryptedJson.split(':', 4)
  50. const password = this.settings.cipherPasswords[label]
  51. if (!password || password.length < 16) {
  52. return callback(new Error('invalid password'))
  53. }
  54. if (iv) {
  55. this.decryptToJsonV2(password, salt, cipherText, iv, callback)
  56. } else {
  57. this.decryptToJsonV1(password, salt, cipherText, callback)
  58. }
  59. }
  60. decryptToJsonV1(password, salt, cipherText, callback) {
  61. keyFn(password, Buffer.from(salt, 'hex'), (err, key) => {
  62. let json
  63. if (err) {
  64. logger.err({ err }, 'error getting Fn key')
  65. return callback(err)
  66. }
  67. // eslint-disable-next-line n/no-deprecated-api
  68. const decipher = crypto.createDecipher(ALGORITHM, key)
  69. const dec =
  70. decipher.update(cipherText, 'base64', 'utf8') + decipher.final('utf8')
  71. try {
  72. json = JSON.parse(dec)
  73. } catch (e) {
  74. return callback(new Error('error decrypting token'))
  75. }
  76. callback(null, json, true)
  77. })
  78. }
  79. decryptToJsonV2(password, salt, cipherText, iv, callback) {
  80. keyFn32(password, Buffer.from(salt, 'hex'), 32, (err, key) => {
  81. let json
  82. if (err) {
  83. logger.err({ err }, 'error getting Fn key')
  84. return callback(err)
  85. }
  86. const decipher = crypto.createDecipheriv(
  87. ALGORITHM,
  88. key,
  89. Buffer.from(iv, 'hex')
  90. )
  91. const dec =
  92. decipher.update(cipherText, 'base64', 'utf8') + decipher.final('utf8')
  93. try {
  94. json = JSON.parse(dec)
  95. } catch (e) {
  96. return callback(new Error('error decrypting token'))
  97. }
  98. callback(null, json)
  99. })
  100. }
  101. }
  102. module.exports = AccessTokenEncryptor