2021-08-16 05:54:39 -04:00
|
|
|
const crypto = require('crypto')
|
2021-10-28 05:23:06 -04:00
|
|
|
const logger = require('@overleaf/logger')
|
2020-02-17 10:04:53 -05:00
|
|
|
|
2021-08-16 05:54:39 -04:00
|
|
|
const ALGORITHM = 'aes-256-ctr'
|
2020-02-17 10:17:53 -05:00
|
|
|
|
2021-08-16 05:54:39 -04:00
|
|
|
const keyFn = (password, salt, callback) =>
|
|
|
|
crypto.pbkdf2(password, salt, 10000, 64, 'sha1', callback)
|
2020-02-17 10:04:53 -05:00
|
|
|
|
2021-08-16 05:54:39 -04:00
|
|
|
const keyFn32 = (password, salt, keyLength, callback) =>
|
|
|
|
crypto.pbkdf2(password, salt, 10000, 32, 'sha1', callback)
|
2020-02-17 10:04:53 -05:00
|
|
|
|
|
|
|
class AccessTokenEncryptor {
|
2021-08-16 05:54:39 -04:00
|
|
|
constructor(settings) {
|
|
|
|
this.settings = settings
|
|
|
|
this.cipherLabel = this.settings.cipherLabel
|
|
|
|
if (this.cipherLabel && this.cipherLabel.match(/:/)) {
|
|
|
|
throw Error('cipherLabel must not contain a colon (:)')
|
|
|
|
}
|
|
|
|
|
|
|
|
this.cipherPassword = this.settings.cipherPasswords[this.cipherLabel]
|
|
|
|
if (!this.cipherPassword) {
|
|
|
|
throw Error('cipherPassword not set')
|
|
|
|
}
|
|
|
|
if (this.cipherPassword.length < 16) {
|
|
|
|
throw Error('cipherPassword too short')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
encryptJson(json, callback) {
|
|
|
|
const string = JSON.stringify(json)
|
|
|
|
crypto.randomBytes(32, (err, bytes) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
const salt = bytes.slice(0, 16)
|
|
|
|
const iv = bytes.slice(16, 32)
|
|
|
|
|
|
|
|
keyFn32(this.cipherPassword, salt, 32, (err, key) => {
|
|
|
|
if (err) {
|
|
|
|
logger.err({ err }, 'error getting Fn key')
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
|
|
|
const crypted =
|
|
|
|
cipher.update(string, 'utf8', 'base64') + cipher.final('base64')
|
|
|
|
|
|
|
|
callback(
|
|
|
|
null,
|
|
|
|
`${this.cipherLabel}:${salt.toString('hex')}:${crypted}:${iv.toString(
|
|
|
|
'hex'
|
|
|
|
)}`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptToJson(encryptedJson, callback) {
|
|
|
|
const [label, salt, cipherText, iv] = encryptedJson.split(':', 4)
|
|
|
|
const password = this.settings.cipherPasswords[label]
|
|
|
|
if (!password || password.length < 16) {
|
|
|
|
return callback(new Error('invalid password'))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (iv) {
|
|
|
|
this.decryptToJsonV2(password, salt, cipherText, iv, callback)
|
|
|
|
} else {
|
|
|
|
this.decryptToJsonV1(password, salt, cipherText, callback)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptToJsonV1(password, salt, cipherText, callback) {
|
|
|
|
keyFn(password, Buffer.from(salt, 'hex'), (err, key) => {
|
|
|
|
let json
|
|
|
|
if (err) {
|
|
|
|
logger.err({ err }, 'error getting Fn key')
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line node/no-deprecated-api
|
|
|
|
const decipher = crypto.createDecipher(ALGORITHM, key)
|
|
|
|
const dec =
|
|
|
|
decipher.update(cipherText, 'base64', 'utf8') + decipher.final('utf8')
|
|
|
|
try {
|
|
|
|
json = JSON.parse(dec)
|
|
|
|
} catch (e) {
|
|
|
|
return callback(new Error('error decrypting token'))
|
|
|
|
}
|
|
|
|
callback(null, json, true)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptToJsonV2(password, salt, cipherText, iv, callback) {
|
|
|
|
keyFn32(password, Buffer.from(salt, 'hex'), 32, (err, key) => {
|
|
|
|
let json
|
|
|
|
if (err) {
|
|
|
|
logger.err({ err }, 'error getting Fn key')
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const decipher = crypto.createDecipheriv(
|
|
|
|
ALGORITHM,
|
|
|
|
key,
|
|
|
|
Buffer.from(iv, 'hex')
|
|
|
|
)
|
|
|
|
const dec =
|
|
|
|
decipher.update(cipherText, 'base64', 'utf8') + decipher.final('utf8')
|
|
|
|
try {
|
|
|
|
json = JSON.parse(dec)
|
|
|
|
} catch (e) {
|
|
|
|
return callback(new Error('error decrypting token'))
|
|
|
|
}
|
|
|
|
callback(null, json)
|
|
|
|
})
|
|
|
|
}
|
2020-02-17 10:04:53 -05:00
|
|
|
}
|
|
|
|
|
2021-08-16 05:54:39 -04:00
|
|
|
module.exports = AccessTokenEncryptor
|