diff --git a/src/backuptargets.js b/src/backuptargets.js index ca13528a8..2b501c1f2 100644 --- a/src/backuptargets.js +++ b/src/backuptargets.js @@ -13,6 +13,7 @@ exports = module.exports = { setSchedule, setRetention, setPrimary, + setEncryption, setName, removePrivateFields, @@ -123,6 +124,7 @@ function removePrivateFields(target) { target.encrypted = target.encryption !== null; target.encryptedFilenames = target.encryption?.encryptedFilenames || false; + target.encryptionPasswordHint = target.encryption?.encryptionPasswordHint || null; delete target.encryption; delete target.config.rootPath; @@ -267,6 +269,32 @@ async function setPrimary(backupTarget, auditSource) { await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, primary: true }); } +async function setEncryption(backupTarget, data, auditSource) { + assert.strictEqual(typeof backupTarget, 'object'); + assert.strictEqual(typeof data, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + let encryption = null; + if (data.encryptionPassword) { + const encryptionPasswordError = validateEncryptionPassword(data.encryptionPassword); + if (encryptionPasswordError) throw encryptionPasswordError; + encryption = hush.generateEncryptionKeysSync(data.encryptionPassword); + encryption.encryptedFilenames = !!data.encryptedFilenames; + encryption.encryptionPasswordHint = data.encryptionPasswordHint || ''; + } + + const queries = [ + { query: 'DELETE FROM FROM backups WHERE targetId=?', args: [ backupTarget.id ] }, + { query: 'UPDATE backupTargets SET encryptionJson=?', args: [ encryption ? JSON.stringify(encryption) : null ] }, + ]; + + const [error, result] = await safe(database.transaction(queries)); + if (error) throw error; + if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found'); + + await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupTarget, encryption: !!encryption }); +} + async function setName(backupTarget, name, auditSource) { assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof name, 'string'); @@ -454,7 +482,8 @@ async function add(data, auditSource) { const { provider, name, config, format, retention, schedule } = data; // required const limits = data.limits || null, encryptionPassword = data.encryptionPassword || null, - encryptedFilenames = data.encryptedFilenames || false; + encryptedFilenames = data.encryptedFilenames || false, + encryptionPasswordHint = data.encryptionPasswordHint || null; const formatError = backupFormat.validateFormat(format); if (formatError) throw formatError; @@ -468,6 +497,7 @@ async function add(data, auditSource) { if (encryptionPasswordError) throw encryptionPasswordError; encryption = hush.generateEncryptionKeysSync(encryptionPassword); encryption.encryptedFilenames = !!encryptedFilenames; + encryption.encryptionPasswordHint = encryptionPasswordHint; } const id = `bc-${crypto.randomUUID()}`; @@ -520,6 +550,7 @@ async function createPseudo(data) { if (encryptionPasswordError) throw encryptionPasswordError; encryption = hush.generateEncryptionKeysSync(encryptionPassword); encryption.encryptedFilenames = !!encryptedFilenames; + encryption.encryptionPasswordHint = ''; } debug('add: validating new storage configuration'); diff --git a/src/routes/backuptargets.js b/src/routes/backuptargets.js index e68cdeec8..0b85cc116 100644 --- a/src/routes/backuptargets.js +++ b/src/routes/backuptargets.js @@ -15,6 +15,7 @@ exports = module.exports = { setRetention, setPrimary, setName, + setEncryption, createBackup, cleanup, @@ -79,6 +80,7 @@ async function add(req, res, next) { if ('limits' in req.body && typeof req.body.limits !== 'object') return next(new HttpError(400, 'limits must be an object')); if ('encryptionPassword' in req.body && typeof req.body.encryptionPassword !== 'string') return next(new HttpError(400, 'encryptionPassword must be a string')); + if ('encryptionPasswordHint' in req.body && typeof req.body.encryptionPasswordHint !== 'string') return next(new HttpError(400, 'encryptionPasswordHint must be a string')); if ('encryptedFilenames' in req.body && typeof req.body.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames must be a boolean')); // testing the backup using put/del takes a bit of time at times @@ -181,6 +183,21 @@ async function setPrimary(req, res, next) { next(new HttpSuccess(200, {})); } +async function setEncryption(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if ('encryptionPassword' in req.body) { + if (req.body.encryptionPassword === null || typeof req.body.encryptionPassword !== 'string') return next(new HttpError(400, 'encryptionPassword must be a string or null')); + } + if ('encryptionPasswordHint' in req.body && typeof req.body.encryptionPasswordHint !== 'string') return next(new HttpError(400, 'encryptionPasswordHint must be a string')); + if ('encryptedFilenames' in req.body && typeof req.body.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames must be a boolean')); + + const [error] = await safe(backupTargets.setEncryption(req.resources.backupTarget, req.body, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, {})); +} + async function setName(req, res, next) { assert.strictEqual(typeof req.body, 'object'); diff --git a/src/server.js b/src/server.js index aead9079e..068c741c0 100644 --- a/src/server.js +++ b/src/server.js @@ -171,6 +171,7 @@ async function initializeExpressSync() { router.post('/api/v1/backup_targets/:id/configure/schedule', json, token, authorizeOwner, routes.backupTargets.load, routes.backupTargets.setSchedule); router.post('/api/v1/backup_targets/:id/configure/retention', json, token, authorizeOwner, routes.backupTargets.load, routes.backupTargets.setRetention); router.post('/api/v1/backup_targets/:id/configure/primary', json, token, authorizeOwner, routes.backupTargets.load, routes.backupTargets.setPrimary); + router.post('/api/v1/backup_targets/:id/configure/encryption', json, token, authorizeOwner, routes.backupTargets.load, routes.backupTargets.setEncryption); // app archive routes router.get ('/api/v1/archives', token, authorizeAdmin, routes.archives.list);