| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- const { promisify } = require('util')
- const crypto = require('crypto')
- const ALGORITHM = 'aes-256-ctr'
- const cryptoHkdf = promisify(crypto.hkdf)
- const cryptoPbkdf2 = promisify(crypto.pbkdf2)
- const cryptoRandomBytes = promisify(crypto.randomBytes)
- class AbstractAccessTokenScheme {
- constructor(cipherLabel, cipherPassword) {
- this.cipherLabel = cipherLabel
- this.cipherPassword = cipherPassword
- }
- /**
- * @param {Object} json
- * @return {Promise<string>}
- */
- async encryptJson(json) {
- throw new Error('encryptJson is not implemented')
- }
- /**
- * @param {string} encryptedJson
- * @return {Promise<Object>}
- */
- async decryptToJson(encryptedJson) {
- throw new Error('decryptToJson is not implemented')
- }
- }
- class AccessTokenSchemeWithGenericKeyFn extends AbstractAccessTokenScheme {
- /**
- * @param {Buffer} salt
- * @return {Promise<Buffer>}
- */
- async keyFn(salt) {
- throw new Error('keyFn is not implemented')
- }
- async encryptJson(json) {
- const plainText = JSON.stringify(json)
- const bytes = await cryptoRandomBytes(32)
- const salt = bytes.slice(0, 16)
- const iv = bytes.slice(16, 32)
- const key = await this.keyFn(salt)
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
- const cipherText =
- cipher.update(plainText, 'utf8', 'base64') + cipher.final('base64')
- return [
- this.cipherLabel,
- salt.toString('hex'),
- cipherText,
- iv.toString('hex'),
- ].join(':')
- }
- async decryptToJson(encryptedJson) {
- const [, salt, cipherText, iv] = encryptedJson.split(':', 4)
- const key = await this.keyFn(Buffer.from(salt, 'hex'))
- const decipher = crypto.createDecipheriv(
- ALGORITHM,
- key,
- Buffer.from(iv, 'hex')
- )
- const plainText =
- decipher.update(cipherText, 'base64', 'utf8') + decipher.final('utf8')
- try {
- return JSON.parse(plainText)
- } catch (e) {
- throw new Error('error decrypting token')
- }
- }
- }
- class AccessTokenSchemeV2 extends AccessTokenSchemeWithGenericKeyFn {
- async keyFn(salt) {
- return cryptoPbkdf2(this.cipherPassword, salt, 10000, 32, 'sha1')
- }
- }
- class AccessTokenSchemeV3 extends AccessTokenSchemeWithGenericKeyFn {
- async keyFn(salt) {
- const optionalInfo = ''
- return cryptoHkdf('sha512', this.cipherPassword, salt, optionalInfo, 32)
- }
- }
- class AccessTokenEncryptor {
- constructor(settings) {
- this.schemeByCipherLabel = new Map()
- for (const cipherLabel of Object.keys(settings.cipherPasswords)) {
- if (!cipherLabel) {
- throw new Error('cipherLabel cannot be empty')
- }
- if (cipherLabel.match(/:/)) {
- throw new Error(
- `cipherLabel must not contain a colon (:), got ${cipherLabel}`
- )
- }
- const [cipherLabelNoVersion, version] = cipherLabel.split('-')
- if (!version) {
- throw new Error(
- `cipherLabel must contain version suffix (e.g. 2042.1-v42), got ${cipherLabel}`
- )
- }
- const cipherPassword = settings.cipherPasswords[cipherLabel]
- if (!cipherPassword) {
- throw new Error(`cipherPasswords['${cipherLabel}'] is missing`)
- }
- if (cipherPassword.length < 16) {
- throw new Error(`cipherPasswords['${cipherLabel}'] is too short`)
- }
- let scheme, schemeNoVersion
- switch (version) {
- case 'v2':
- scheme = new AccessTokenSchemeV2(cipherLabel, cipherPassword)
- schemeNoVersion = new AccessTokenSchemeV2(
- cipherLabelNoVersion,
- cipherPassword
- )
- break
- case 'v3':
- scheme = new AccessTokenSchemeV3(cipherLabel, cipherPassword)
- schemeNoVersion = new AccessTokenSchemeV3(
- cipherLabelNoVersion,
- cipherPassword
- )
- break
- default:
- throw new Error(`unknown version '${version}' for ${cipherLabel}`)
- }
- this.schemeByCipherLabel.set(cipherLabel, scheme)
- this.schemeByCipherLabel.set(cipherLabelNoVersion, schemeNoVersion)
- }
- this.defaultScheme = this.schemeByCipherLabel.get(settings.cipherLabel)
- if (!this.defaultScheme) {
- throw new Error(`unknown default cipherLabel ${settings.cipherLabel}`)
- }
- }
- encryptJson(json, callback) {
- this.defaultScheme.encryptJson(json).then(s => callback(null, s), callback)
- }
- decryptToJson(encryptedJson, callback) {
- const [label] = encryptedJson.split(':', 1)
- const scheme = this.schemeByCipherLabel.get(label)
- if (!scheme) {
- return callback(
- new Error('unknown access-token-encryptor label ' + label)
- )
- }
- scheme.decryptToJson(encryptedJson).then(o => callback(null, o), callback)
- }
- }
- module.exports = AccessTokenEncryptor
|