mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-10 17:47:07 +08:00
141 lines
4.4 KiB
JavaScript
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 };
|