wildduck/lib/encrypt.js

141 lines
4.4 KiB
JavaScript

'use strict';
const crypto = require('crypto');
const assert = require('assert');
function parseEncryptedData(encryptedData, defaultCipher) {
encryptedData = (encryptedData || '').toString();
if (!encryptedData) {
return false;
}
if (encryptedData.charAt(0) !== '$') {
// cleartext
return {
format: 'cleartext',
data: encryptedData
};
}
if (encryptedData.lastIndexOf('$') === 0) {
// legacy
return {
format: 'legacy',
cipher: defaultCipher || 'aes192',
data: Buffer.from(encryptedData.substr(1), 'hex')
};
}
let [, format, cipher, authTag, iv, salt, encryptedText] = encryptedData.split('$');
if (!format || !cipher || !authTag || !iv || !encryptedText) {
return false;
}
authTag = Buffer.from(authTag, 'hex');
iv = Buffer.from(iv, 'hex');
salt = Buffer.from(salt, 'hex');
encryptedText = Buffer.from(encryptedText, 'hex');
return {
format,
cipher,
authTag,
iv,
salt,
data: encryptedText
};
}
function getKeyFromPassword(password, salt, keyLen) {
return new Promise((resolve, reject) => {
crypto.scrypt(password, salt, keyLen, (err, result) => {
if (err) {
return reject(err);
}
if (!result) {
return reject(new Error('Failed to hash key'));
}
return resolve(result);
});
});
}
async function decrypt(encryptedData, secret, defaultCipher) {
const decryptData = parseEncryptedData(encryptedData, defaultCipher);
if (!decryptData) {
return encryptedData;
}
if (!secret && decryptData.format !== 'cleartext') {
// data is encrypted but we do not have a secret
let err = new Error('Failed to decrypt data. No secret provided.');
err.responseCode = 500;
err.code = 'InternalConfigError';
throw err;
}
switch (decryptData.format) {
case 'cleartext':
return encryptedData;
case 'legacy':
try {
let decipher = crypto.createDecipher(decryptData.cipher, secret);
return Buffer.concat([decipher.update(decryptData.data), decipher.final()]).toString('utf-8');
} catch (E) {
let err = new Error('Failed to decrypt data. ' + E.message);
err.responseCode = 500;
err.code = 'InternalConfigError';
throw err;
}
case 'wd01':
try {
assert.strictEqual(decryptData.authTag.length, 16, 'Invalid auth tag length');
assert.strictEqual(decryptData.iv.length, 12, 'Invalid iv length');
assert.strictEqual(decryptData.salt.length, 16, 'Invalid salt length');
// convert password to 32B key
const key = await getKeyFromPassword(secret, decryptData.salt, 32);
const decipher = crypto.createDecipheriv(decryptData.cipher, key, decryptData.iv, { authTagLength: decryptData.authTag.length });
decipher.setAuthTag(decryptData.authTag);
return Buffer.concat([decipher.update(decryptData.data), decipher.final()]).toString('utf-8');
} catch (E) {
let err = new Error('Failed to decrypt data. ' + E.message);
err.responseCode = 500;
err.code = 'InternalConfigError';
throw err;
}
default: {
let err = new Error('Unknown encryption format: ' + decryptData.format);
err.responseCode = 500;
err.code = 'InternalConfigError';
throw err;
}
}
}
async function encrypt(cleartext, secret) {
if (!secret) {
return cleartext;
}
const iv = crypto.randomBytes(12);
const salt = crypto.randomBytes(16);
const key = await getKeyFromPassword(secret, salt, 32);
const format = 'wd01';
const algo = 'aes-256-gcm';
const cipher = crypto.createCipheriv(algo, key, iv, { authTagLength: 16 });
const encryptedText = Buffer.concat([cipher.update(cleartext), cipher.final()]);
const authTag = cipher.getAuthTag();
return ['', format, algo].concat([authTag, iv, salt, encryptedText].map(buf => buf.toString('hex'))).join('$');
}
module.exports = { encrypt, decrypt };