backups: generate keys from password

this also removes storage of password from db

part of #579
This commit is contained in:
Girish Ramakrishnan
2020-05-12 14:00:05 -07:00
parent 5af957dc9c
commit 1df94fd84d
4 changed files with 92 additions and 66 deletions

View File

@@ -32,6 +32,8 @@ exports = module.exports = {
configureCollectd: configureCollectd,
generateEncryptionKeysSync: generateEncryptionKeysSync,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
// for testing
@@ -102,13 +104,19 @@ function api(provider) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
if (newConfig.password === exports.SECRET_PLACEHOLDER) {
delete newConfig.password;
newConfig.encryption = currentConfig.encryption;
}
if (newConfig.provider === currentConfig.provider) api(newConfig.provider).injectPrivateFields(newConfig, currentConfig);
}
function removePrivateFields(backupConfig) {
assert.strictEqual(typeof backupConfig, 'object');
if (backupConfig.password) backupConfig.password = exports.SECRET_PLACEHOLDER;
if (backupConfig.encryption) {
delete backupConfig.encryption;
backupConfig.password = exports.SECRET_PLACEHOLDER;
}
return api(backupConfig.provider).removePrivateFields(backupConfig);
}
@@ -132,6 +140,17 @@ function testConfig(backupConfig, callback) {
api(backupConfig.provider).testConfig(backupConfig, callback);
}
function generateEncryptionKeysSync(password) {
assert.strictEqual(typeof password, 'string');
const aesKeys = crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 128);
return {
dataKey: aesKeys.slice(0, 32).toString('hex'),
dataHmacKey: aesKeys.slice(32, 64).toString('hex'),
filenameKey: aesKeys.slice(64, 96).toString('hex'),
filenameHmacKey: aesKeys.slice(96).toString('hex')
};
}
function testProviderConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
@@ -186,20 +205,21 @@ function getBackupFilePath(backupConfig, backupId, format) {
assert.strictEqual(typeof format, 'string');
if (format === 'tgz') {
const fileType = backupConfig.password ? '.tar.gz.enc' : '.tar.gz';
const fileType = backupConfig.encryption ? '.tar.gz.enc' : '.tar.gz';
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId+fileType);
} else {
return path.join(backupConfig.prefix || backupConfig.backupFolder || '', backupId);
}
}
function encryptFilePath(filePath, key) {
function encryptFilePath(filePath, backupConfig) {
assert.strictEqual(typeof filePath, 'string');
assert(Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
var encryptedParts = filePath.split('/').map(function (part) {
let iv = crypto.createHmac('sha256', key).update(part).digest().slice(0, 16); // iv has to be deterministic, for our sync (copy) logic to work
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let hmac = crypto.createHmac('sha256', Buffer.from(backupConfig.encryption.filenameHmacKey, 'hex'));
const iv = hmac.update(part).digest().slice(0, 16); // iv has to be deterministic, for our sync (copy) logic to work
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(backupConfig.encryption.filenameKey, 'hex'), iv);
let crypt = cipher.update(part);
crypt = Buffer.concat([ iv, crypt, cipher.final() ]);
@@ -211,9 +231,9 @@ function encryptFilePath(filePath, key) {
return encryptedParts.join('/');
}
function decryptFilePath(filePath, key) {
function decryptFilePath(filePath, backupConfig) {
assert.strictEqual(typeof filePath, 'string');
assert(Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
let decryptedParts = [];
for (let part of filePath.split('/')) {
@@ -223,7 +243,7 @@ function decryptFilePath(filePath, key) {
try {
const buffer = Buffer.from(part, 'base64');
const iv = buffer.slice(0, 16);
let decrypt = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypt = crypto.createDecipheriv('aes-256-cbc', Buffer.from(backupConfig.encryption.filenameKey, 'hex'), iv);
let text = decrypt.update(buffer.slice(16));
text = Buffer.concat([ text, decrypt.final() ]);
decryptedParts.push(text.toString('utf8'));
@@ -303,9 +323,9 @@ class DecryptStream extends TransformStream {
}
}
function createReadStream(sourceFile, key) {
function createReadStream(sourceFile, backupConfig) {
assert.strictEqual(typeof sourceFile, 'string');
assert(key === null || Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
var stream = fs.createReadStream(sourceFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
@@ -315,8 +335,8 @@ function createReadStream(sourceFile, key) {
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
if (key !== null) {
let encryptStream = new EncryptStream(key);
if (backupConfig.encryption) {
let encryptStream = new EncryptStream(Buffer.from(backupConfig.encryption.dataKey, 'hex'));
encryptStream.on('error', function (error) {
debug('createReadStream: encrypt stream error.', error);
@@ -329,9 +349,9 @@ function createReadStream(sourceFile, key) {
}
}
function createWriteStream(destFile, key) {
function createWriteStream(destFile, backupConfig) {
assert.strictEqual(typeof destFile, 'string');
assert(key === null || Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
var stream = fs.createWriteStream(destFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
@@ -341,8 +361,8 @@ function createWriteStream(destFile, key) {
ps.emit('error', new BoxError(BoxError.FS_ERROR, error.message));
});
if (key !== null) {
let decrypt = new DecryptStream(key);
if (backupConfig.encryption) {
let decrypt = new DecryptStream(Buffer.from(backupConfig.encryption.dataKey, 'hex'));
decrypt.on('error', function (error) {
debug('createWriteStream: decrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.CRYPTO_ERROR, error.message));
@@ -356,9 +376,9 @@ function createWriteStream(destFile, key) {
return ps;
}
function tarPack(dataLayout, key, callback) {
function tarPack(dataLayout, backupConfig, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var pack = tar.pack('/', {
@@ -391,8 +411,8 @@ function tarPack(dataLayout, key, callback) {
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
if (key !== null) {
const encryptStream = new EncryptStream(key);
if (backupConfig.encryption) {
const encryptStream = new EncryptStream(Buffer.from(backupConfig.encryption.dataKey, 'hex'));
encryptStream.on('error', function (error) {
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
@@ -406,14 +426,6 @@ function tarPack(dataLayout, key, callback) {
return callback(null, ps);
}
function aesKeyFromPassword(password) {
assert(typeof password === 'undefined' || typeof password === 'string');
if (!password) return null;
return crypto.scryptSync(password, Buffer.from('CLOUDRONSCRYPTSALT', 'utf8'), 32);
}
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
@@ -423,12 +435,11 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
const concurrency = backupConfig.syncConcurrency || (backupConfig.provider === 's3' ? 20 : 10);
const aesKey = aesKeyFromPassword(backupConfig.password);
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.password ? encryptFilePath(task.path, aesKey) : task.path;
const destPath = task.path && backupConfig.encryption ? encryptFilePath(task.path, backupConfig) : task.path;
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'removedir') {
@@ -449,7 +460,7 @@ function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
if (task.operation === 'add') {
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), aesKey);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig);
stream.on('error', function (error) {
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
@@ -547,11 +558,10 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
if (error) return callback(error);
if (format === 'tgz') {
const aesKey = aesKeyFromPassword(backupConfig.password);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
tarPack(dataLayout, aesKey, function (error, tarStream) {
tarPack(dataLayout, backupConfig, function (error, tarStream) {
if (error) return retryCallback(error);
tarStream.on('progress', function (progress) {
@@ -574,10 +584,10 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
});
}
function tarExtract(inStream, dataLayout, key, callback) {
function tarExtract(inStream, dataLayout, backupConfig, callback) {
assert.strictEqual(typeof inStream, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || Buffer.isBuffer(key));
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var gunzip = zlib.createGunzip({});
@@ -612,8 +622,8 @@ function tarExtract(inStream, dataLayout, key, callback) {
ps.emit('done');
});
if (key !== null) {
let decrypt = new DecryptStream(key);
if (backupConfig.encryption) {
let decrypt = new DecryptStream(Buffer.from(backupConfig.encryption.dataKey, 'hex'));
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
@@ -662,12 +672,10 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
const aesKey = aesKeyFromPassword(backupConfig.password);
function downloadFile(entry, done) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.password) {
relativePath = decryptFilePath(relativePath, aesKey);
if (backupConfig.encryption) {
relativePath = decryptFilePath(relativePath, backupConfig);
if (!relativePath) return done(new BoxError(BoxError.CRYPTO_ERROR, 'Unable to decrypt file'));
}
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
@@ -676,7 +684,7 @@ function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback,
if (error) return done(new BoxError(BoxError.FS_ERROR, error.message));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, aesKey);
let destStream = createWriteStream(destFilePath, backupConfig);
destStream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
@@ -727,12 +735,11 @@ function download(backupConfig, backupId, format, dataLayout, progressCallback,
const backupFilePath = getBackupFilePath(backupConfig, backupId, format);
if (format === 'tgz') {
const aesKey = aesKeyFromPassword(backupConfig.password);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
if (error) return retryCallback(error);
tarExtract(sourceStream, dataLayout, aesKey, function (error, ps) {
tarExtract(sourceStream, dataLayout, backupConfig, function (error, ps) {
if (error) return retryCallback(error);
ps.on('progress', function (progress) {