mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-08 14:15:40 +08:00
refactored encrypted fields
This commit is contained in:
parent
78b774865c
commit
6d3badf1db
7 changed files with 247 additions and 184 deletions
|
@ -29,8 +29,8 @@ maxForwards=2000
|
||||||
# If enabled then encrypt TOTP seed tokens with the secret password. By default TOTP seeds
|
# If enabled then encrypt TOTP seed tokens with the secret password. By default TOTP seeds
|
||||||
# are not encrypted and stored as cleartext. Once set up do not change these values,
|
# are not encrypted and stored as cleartext. Once set up do not change these values,
|
||||||
# otherwise decrypting totp seeds is going to fail
|
# otherwise decrypting totp seeds is going to fail
|
||||||
#cipher="aes192"
|
|
||||||
#secret="a secret cat"
|
#secret="a secret cat"
|
||||||
|
#cipher="aes192" # only for decrypting legacy values (if there are any)
|
||||||
|
|
||||||
[u2f]
|
[u2f]
|
||||||
# Fully qualified URL of your website (must use HTTPS!)
|
# Fully qualified URL of your website (must use HTTPS!)
|
||||||
|
@ -84,9 +84,9 @@ maxForwards=2000
|
||||||
# @include "acme.toml"
|
# @include "acme.toml"
|
||||||
|
|
||||||
[certs]
|
[certs]
|
||||||
# Encrypt stored TLS private keys
|
# Encrypt stored TLS private keys with the following password (disabled by default):
|
||||||
#cipher="aes192"
|
|
||||||
#secret="a secret cat"
|
#secret="a secret cat"
|
||||||
|
#cipher="aes192" # only for decrypting legacy values (if there are any)
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
# @include "plugins/*.toml"
|
# @include "plugins/*.toml"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# If enabled then encrypt DKIM keys with the secret password. By default DKIM keys
|
# If enabled then encrypt DKIM keys with the secret password. By default DKIM keys
|
||||||
# are not encrypted and stored as cleartext. Once set up do not change these values,
|
# are not encrypted and stored as cleartext. Once set up do not change these values,
|
||||||
# otherwise decrypting DKIM keys is going to fail
|
# otherwise decrypting DKIM keys is going to fail
|
||||||
#cipher="aes192"
|
|
||||||
#secret="a secret cat"
|
#secret="a secret cat"
|
||||||
|
#cipher="aes192" # only for decrypting legacy values (if there are any)
|
||||||
|
|
||||||
# If true then also adds a signature for the outbound domain
|
# If true then also adds a signature for the outbound domain
|
||||||
# Affects WildDuck ZoneMTA plugin only
|
# Affects WildDuck ZoneMTA plugin only
|
||||||
|
|
|
@ -9,6 +9,7 @@ const tools = require('./tools');
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
const tlsOptions = require('../imap-core/lib/tls-options');
|
const tlsOptions = require('../imap-core/lib/tls-options');
|
||||||
const { publish, CERT_CREATED, CERT_UPDATED, CERT_DELETED } = require('./events');
|
const { publish, CERT_CREATED, CERT_UPDATED, CERT_DELETED } = require('./events');
|
||||||
|
const { encrypt, decrypt } = require('./encrypt');
|
||||||
|
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const generateKeyPair = promisify(crypto.generateKeyPair);
|
const generateKeyPair = promisify(crypto.generateKeyPair);
|
||||||
|
@ -148,7 +149,7 @@ class CertHandler {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let encodedPrivateKey = await this.encodeKey(updates.privateKey);
|
let encodedPrivateKey = await encrypt(updates.privateKey, this.secret);
|
||||||
|
|
||||||
changes.$set = Object.assign({}, updates, { fp, privateKey: encodedPrivateKey });
|
changes.$set = Object.assign({}, updates, { fp, privateKey: encodedPrivateKey });
|
||||||
|
|
||||||
|
@ -210,7 +211,7 @@ class CertHandler {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
let encodedPrivateKey = await this.encodeKey(privateKey);
|
let encodedPrivateKey = await encrypt(privateKey, this.secret);
|
||||||
|
|
||||||
let r;
|
let r;
|
||||||
try {
|
try {
|
||||||
|
@ -273,7 +274,7 @@ class CertHandler {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
certData.privateKey = await this.encodeKey(privateKey);
|
certData.privateKey = await encrypt(privateKey, this.secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cert) {
|
if (cert) {
|
||||||
|
@ -434,7 +435,7 @@ class CertHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includePrivateKey) {
|
if (includePrivateKey) {
|
||||||
certData.privateKey = await this.decodeKey(certData.privateKey);
|
certData.privateKey = await decrypt(certData.privateKey, this.secret, this.cipher);
|
||||||
} else {
|
} else {
|
||||||
delete certData.privateKey;
|
delete certData.privateKey;
|
||||||
}
|
}
|
||||||
|
@ -442,55 +443,6 @@ class CertHandler {
|
||||||
return certData;
|
return certData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async encodeKey(privateKey) {
|
|
||||||
if (!privateKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.secret) {
|
|
||||||
try {
|
|
||||||
let cipher = crypto.createCipher(this.cipher || 'aes192', this.secret);
|
|
||||||
privateKey = '$' + cipher.update(privateKey, 'utf8', 'hex');
|
|
||||||
privateKey += cipher.final('hex');
|
|
||||||
} catch (E) {
|
|
||||||
let err = new Error('Failed to encrypt private key. ' + E.message);
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalConfigError';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async decodeKey(privateKey) {
|
|
||||||
if (!privateKey) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privateKey.charAt(0) === '$') {
|
|
||||||
if (this.secret) {
|
|
||||||
try {
|
|
||||||
let decipher = crypto.createDecipher(this.cipher || 'aes192', this.secret);
|
|
||||||
privateKey = decipher.update(privateKey.substr(1), 'hex', 'utf-8');
|
|
||||||
privateKey += decipher.final('utf8');
|
|
||||||
} catch (E) {
|
|
||||||
let err = new Error('Failed to decrypt private key. ' + E.message);
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalConfigError';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let err = new Error('Can not use decrypted key');
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalConfigError';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async del(options) {
|
async del(options) {
|
||||||
let query = this.prepareQuery(options);
|
let query = this.prepareQuery(options);
|
||||||
|
|
||||||
|
@ -567,7 +519,7 @@ class CertHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// key might be encrypted
|
// key might be encrypted
|
||||||
let privateKey = await this.decodeKey(certData.privateKey);
|
let privateKey = await decrypt(certData.privateKey, this.secret, this.cipher);
|
||||||
|
|
||||||
let serviceCtxOpts = { key: privateKey, cert: certData.cert, ca: certData.ca };
|
let serviceCtxOpts = { key: privateKey, cert: certData.cert, ca: certData.ca };
|
||||||
for (let key of ['dhparam']) {
|
for (let key of ['dhparam']) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ const forge = require('node-forge');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const tools = require('./tools');
|
const tools = require('./tools');
|
||||||
const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events');
|
const { publish, DKIM_CREATED, DKIM_UPDATED, DKIM_DELETED } = require('./events');
|
||||||
|
const { encrypt, decrypt } = require('./encrypt');
|
||||||
|
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const generateKeyPair = promisify(crypto.generateKeyPair);
|
const generateKeyPair = promisify(crypto.generateKeyPair);
|
||||||
|
@ -112,90 +113,81 @@ class DkimHandler {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.secret) {
|
encrypt(privateKeyPem, this.secret)
|
||||||
try {
|
.then(privateKeyPem => {
|
||||||
let cipher = crypto.createCipher(this.cipher || 'aes192', this.secret);
|
let dkimData = {
|
||||||
privateKeyPem = '$' + cipher.update(privateKeyPem, 'utf8', 'hex');
|
domain,
|
||||||
privateKeyPem += cipher.final('hex');
|
selector,
|
||||||
} catch (E) {
|
privateKey: privateKeyPem,
|
||||||
let err = new Error('Failed to encrypt private key. ' + E.message);
|
publicKey: publicKeyPem,
|
||||||
err.responseCode = 500;
|
fingerprint: fp,
|
||||||
err.code = 'InternalConfigError';
|
created: new Date(),
|
||||||
return callback(err);
|
latest: true
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let dkimData = {
|
if (description) {
|
||||||
domain,
|
dkimData.description = description;
|
||||||
selector,
|
|
||||||
privateKey: privateKeyPem,
|
|
||||||
publicKey: publicKeyPem,
|
|
||||||
fingerprint: fp,
|
|
||||||
created: new Date(),
|
|
||||||
latest: true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (description) {
|
|
||||||
dkimData.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.database.collection('dkim').findOneAndReplace(
|
|
||||||
{
|
|
||||||
domain
|
|
||||||
},
|
|
||||||
dkimData,
|
|
||||||
{
|
|
||||||
upsert: true,
|
|
||||||
returnDocument: 'after'
|
|
||||||
},
|
|
||||||
(err, r) => {
|
|
||||||
if (err) {
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalDatabaseError';
|
|
||||||
return callback(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.value) {
|
this.database.collection('dkim').findOneAndReplace(
|
||||||
let err = new Error('Failed to insert DKIM key');
|
{
|
||||||
err.responseCode = 500;
|
domain
|
||||||
err.code = 'InternalDatabaseError';
|
},
|
||||||
return callback(err);
|
dkimData,
|
||||||
}
|
{
|
||||||
|
upsert: true,
|
||||||
|
returnDocument: 'after'
|
||||||
|
},
|
||||||
|
(err, r) => {
|
||||||
|
if (err) {
|
||||||
|
err.responseCode = 500;
|
||||||
|
err.code = 'InternalDatabaseError';
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.redis) {
|
if (!r.value) {
|
||||||
if (r.lastErrorObject.upserted) {
|
let err = new Error('Failed to insert DKIM key');
|
||||||
publish(this.redis, {
|
err.responseCode = 500;
|
||||||
ev: DKIM_CREATED,
|
err.code = 'InternalDatabaseError';
|
||||||
dkim: r.value._id,
|
return callback(err);
|
||||||
domain,
|
}
|
||||||
selector,
|
|
||||||
fingerprint: fp
|
if (this.redis) {
|
||||||
}).catch(() => false);
|
if (r.lastErrorObject.upserted) {
|
||||||
} else if (r.lastErrorObject.updatedExisting) {
|
publish(this.redis, {
|
||||||
publish(this.redis, {
|
ev: DKIM_CREATED,
|
||||||
ev: DKIM_UPDATED,
|
dkim: r.value._id,
|
||||||
dkim: r.value._id,
|
domain,
|
||||||
domain,
|
selector,
|
||||||
selector,
|
fingerprint: fp
|
||||||
fingerprint: fp
|
}).catch(() => false);
|
||||||
}).catch(() => false);
|
} else if (r.lastErrorObject.updatedExisting) {
|
||||||
|
publish(this.redis, {
|
||||||
|
ev: DKIM_UPDATED,
|
||||||
|
dkim: r.value._id,
|
||||||
|
domain,
|
||||||
|
selector,
|
||||||
|
fingerprint: fp
|
||||||
|
}).catch(() => false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, {
|
||||||
|
id: r.value._id.toString(),
|
||||||
|
domain: dkimData.domain,
|
||||||
|
selector: dkimData.selector,
|
||||||
|
description: dkimData.description,
|
||||||
|
fingerprint: dkimData.fingerprint,
|
||||||
|
publicKey: dkimData.publicKey,
|
||||||
|
dnsTxt: {
|
||||||
|
name: dkimData.selector + '._domainkey.' + dkimData.domain,
|
||||||
|
value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
})
|
||||||
return callback(null, {
|
.catch(err => callback(err));
|
||||||
id: r.value._id.toString(),
|
|
||||||
domain: dkimData.domain,
|
|
||||||
selector: dkimData.selector,
|
|
||||||
description: dkimData.description,
|
|
||||||
fingerprint: dkimData.fingerprint,
|
|
||||||
publicKey: dkimData.publicKey,
|
|
||||||
dnsTxt: {
|
|
||||||
name: dkimData.selector + '._domainkey.' + dkimData.domain,
|
|
||||||
value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -228,43 +220,35 @@ class DkimHandler {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let privateKey;
|
let loadPrivateKey = next => {
|
||||||
if (includePrivateKey) {
|
if (!includePrivateKey) {
|
||||||
privateKey = dkimData.privateKey;
|
return false;
|
||||||
if (privateKey.charAt(0) === '$') {
|
|
||||||
if (this.secret) {
|
|
||||||
try {
|
|
||||||
let decipher = crypto.createDecipher(this.cipher || 'aes192', this.secret);
|
|
||||||
privateKey = decipher.update(privateKey.substr(1), 'hex', 'utf-8');
|
|
||||||
privateKey += decipher.final('utf8');
|
|
||||||
} catch (E) {
|
|
||||||
let err = new Error('Failed to decrypt private key. ' + E.message);
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalConfigError';
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let err = new Error('Can not use decrypted key');
|
|
||||||
err.responseCode = 500;
|
|
||||||
err.code = 'InternalConfigError';
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, {
|
decrypt(dkimData.privateKey, this.secret, this.cipher)
|
||||||
id: dkimData._id.toString(),
|
.then(privateKey => next(null, privateKey))
|
||||||
domain: dkimData.domain,
|
.catch(err => next(err));
|
||||||
selector: dkimData.selector,
|
};
|
||||||
description: dkimData.description,
|
|
||||||
fingerprint: dkimData.fingerprint,
|
loadPrivateKey((err, privateKey) => {
|
||||||
publicKey: dkimData.publicKey,
|
if (err) {
|
||||||
privateKey,
|
return callback(err);
|
||||||
dnsTxt: {
|
}
|
||||||
name: dkimData.selector + '._domainkey.' + dkimData.domain,
|
|
||||||
value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
|
callback(null, {
|
||||||
},
|
id: dkimData._id.toString(),
|
||||||
created: dkimData.created
|
domain: dkimData.domain,
|
||||||
|
selector: dkimData.selector,
|
||||||
|
description: dkimData.description,
|
||||||
|
fingerprint: dkimData.fingerprint,
|
||||||
|
publicKey: dkimData.publicKey,
|
||||||
|
privateKey,
|
||||||
|
dnsTxt: {
|
||||||
|
name: dkimData.selector + '._domainkey.' + dkimData.domain,
|
||||||
|
value: 'v=DKIM1;t=s;p=' + dkimData.publicKey.replace(/^-.*-$/gm, '').replace(/\s/g, '')
|
||||||
|
},
|
||||||
|
created: dkimData.created
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
133
lib/encrypt.js
Normal file
133
lib/encrypt.js
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
'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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
|
@ -20,6 +20,7 @@ const UserCache = require('./user-cache');
|
||||||
const isemail = require('isemail');
|
const isemail = require('isemail');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const TaskHandler = require('./task-handler');
|
const TaskHandler = require('./task-handler');
|
||||||
|
const { encrypt, decrypt } = require('./encrypt');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
publish,
|
publish,
|
||||||
|
@ -1789,9 +1790,7 @@ class UserHandler {
|
||||||
let seed = secret.base32;
|
let seed = secret.base32;
|
||||||
if (config.totp && config.totp.secret) {
|
if (config.totp && config.totp.secret) {
|
||||||
try {
|
try {
|
||||||
let cipher = crypto.createCipher(config.totp.cipher || 'aes192', config.totp.secret);
|
seed = await encrypt(seed, config.totp.secret);
|
||||||
seed = '$' + cipher.update(seed, 'utf8', 'hex');
|
|
||||||
seed += cipher.final('hex');
|
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
log.error('DB', 'TOTPFAIL cipher failed id=%s error=%s', user, E.message);
|
log.error('DB', 'TOTPFAIL cipher failed id=%s error=%s', user, E.message);
|
||||||
let err = new Error('Database Error, failed to update user');
|
let err = new Error('Database Error, failed to update user');
|
||||||
|
@ -1903,11 +1902,9 @@ class UserHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
let secret = userData.pendingSeed;
|
let secret = userData.pendingSeed;
|
||||||
if (secret.charAt(0) === '$' && config.totp && config.totp.secret) {
|
if (config.totp && config.totp.secret) {
|
||||||
try {
|
try {
|
||||||
let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret);
|
secret = await decrypt(secret, config.totp.secret, config.totp.cipher);
|
||||||
secret = decipher.update(secret.substr(1), 'hex', 'utf-8');
|
|
||||||
secret += decipher.final('utf8');
|
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
|
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
|
||||||
let err = new Error('Can not use decrypted secret');
|
let err = new Error('Can not use decrypted secret');
|
||||||
|
@ -2192,9 +2189,7 @@ class UserHandler {
|
||||||
let secret = userData.seed;
|
let secret = userData.seed;
|
||||||
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
|
if (userData.seed.charAt(0) === '$' && config.totp && config.totp.secret) {
|
||||||
try {
|
try {
|
||||||
let decipher = crypto.createDecipher(config.totp.cipher || 'aes192', config.totp.secret);
|
secret = await decrypt(userData.seed, config.totp.secret, config.totp.cipher);
|
||||||
secret = decipher.update(userData.seed.substr(1), 'hex', 'utf-8');
|
|
||||||
secret += decipher.final('utf8');
|
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
|
log.error('DB', 'TOTPFAIL decipher failed id=%s error=%s', user, E.message);
|
||||||
let err = new Error('Can not use decrypted secret');
|
let err = new Error('Can not use decrypted secret');
|
||||||
|
|
|
@ -45,8 +45,7 @@ port=24
|
||||||
disableSTARTTLS=true" > /etc/wildduck/lmtp.toml
|
disableSTARTTLS=true" > /etc/wildduck/lmtp.toml
|
||||||
|
|
||||||
# make sure that DKIM keys are not stored to database as cleartext
|
# make sure that DKIM keys are not stored to database as cleartext
|
||||||
#echo "secret=\"$DKIM_SECRET\"
|
echo "secret=\"$DKIM_SECRET\"" >> /etc/wildduck/dkim.toml
|
||||||
#cipher=\"aes192\"" >> /etc/wildduck/dkim.toml
|
|
||||||
|
|
||||||
echo "user=\"wildduck\"
|
echo "user=\"wildduck\"
|
||||||
group=\"wildduck\"
|
group=\"wildduck\"
|
||||||
|
|
Loading…
Add table
Reference in a new issue