diff --git a/CHANGES b/CHANGES index 230052410..469ad8a3c 100644 --- a/CHANGES +++ b/CHANGES @@ -1931,4 +1931,5 @@ * graphs: fix issue where large number of apps would crash the box code (query param limit exceeded) * backups: fix various security issues in encypted backups (thanks @mehdi) * graphs: add app graphs +* older encrypted backups cannot be used in this version diff --git a/migrations/20200514045746-backups-add-encryptionVersion.js b/migrations/20200514045746-backups-add-encryptionVersion.js new file mode 100644 index 000000000..60b24af3c --- /dev/null +++ b/migrations/20200514045746-backups-add-encryptionVersion.js @@ -0,0 +1,24 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) { + if (error) return callback(error); + + db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) { + if (error || results.length === 0) return callback(error); + + var backupConfig = JSON.parse(results[0].value); + if (!backupConfig.encryption) return callback(null); + + // mark old encrypted backups as v1 + db.runSql('UPDATE backups SET encryptionVersion=1', callback); + }); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index f6e917d0a..cb98d9c97 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -121,7 +121,8 @@ CREATE TABLE IF NOT EXISTS appEnvVars( CREATE TABLE IF NOT EXISTS backups( id VARCHAR(128) NOT NULL, creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - version VARCHAR(128) NOT NULL, /* app version or box version */ + packageVersion VARCHAR(128) NOT NULL, /* app version or box version */ + encryptionVersion INTEGER, /* when null, unencrypted backup */ type VARCHAR(16) NOT NULL, /* 'box' or 'app' */ dependsOn TEXT, /* comma separate list of objects this backup depends on */ state VARCHAR(16) NOT NULL, diff --git a/src/apps.js b/src/apps.js index a214a1ec6..c323f1799 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1480,7 +1480,8 @@ function restore(app, backupId, auditSource, callback) { func(function (error, backupInfo) { if (error) return callback(error); - if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest')); + if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest')); + if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool')); // re-validate because this new box version may not accept old configs error = checkManifestConstraints(backupInfo.manifest); @@ -1603,7 +1604,8 @@ function clone(app, data, user, auditSource, callback) { backups.get(backupId, function (error, backupInfo) { if (error) return callback(error); - if (!backupInfo.manifest) callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); + if (!backupInfo.manifest) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore config')); + if (backupInfo.encryptionVersion === 1) return callback(new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned')); const manifest = backupInfo.manifest, appStoreId = app.appStoreId; diff --git a/src/backupdb.js b/src/backupdb.js index 201d7a3fb..0b739b77e 100644 --- a/src/backupdb.js +++ b/src/backupdb.js @@ -6,7 +6,7 @@ var assert = require('assert'), safe = require('safetydance'), util = require('util'); -var BACKUPS_FIELDS = [ 'id', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ]; +var BACKUPS_FIELDS = [ 'id', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ]; exports = module.exports = { add: add, @@ -106,6 +106,7 @@ function get(id, callback) { function add(id, data, callback) { assert(data && typeof data === 'object'); assert.strictEqual(typeof id, 'string'); + assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number'); assert.strictEqual(typeof data.packageVersion, 'string'); assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX); assert(util.isArray(data.dependsOn)); @@ -116,8 +117,8 @@ function add(id, data, callback) { var creationTime = data.creationTime || new Date(); // allow tests to set the time var manifestJson = JSON.stringify(data.manifest); - database.query('INSERT INTO backups (id, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [ id, data.packageVersion, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ], + database.query('INSERT INTO backups (id, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ id, data.encryptionVersion, data.packageVersion, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ], function (error) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS)); if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); diff --git a/src/backups.js b/src/backups.js index f4e351652..d4944a10e 100644 --- a/src/backups.js +++ b/src/backups.js @@ -941,7 +941,15 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call debug(`Rotating box backup to id ${backupId}`); - backupdb.add(backupId, { packageVersion: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) { + const data = { + encryptionVersion: backupConfig.encryption ? 2 : null, + packageVersion: constants.VERSION, + type: backupdb.BACKUP_TYPE_BOX, + dependsOn: appBackupIds, + manifest: null, + format: format + }; + backupdb.add(backupId, data, function (error) { if (error) return callback(error); var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format)); @@ -1023,7 +1031,16 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call debug(`Rotating app backup of ${app.id} to id ${backupId}`); - backupdb.add(backupId, { packageVersion: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) { + const data = { + encryptionVersion: backupConfig.encryption ? 2 : null, + packageVersion: manifest.version, + type: backupdb.BACKUP_TYPE_APP, + dependsOn: [ ], + manifest, + format: format + }; + + backupdb.add(backupId, data, function (error) { if (error) return callback(error); var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format)); diff --git a/src/test/backups-test.js b/src/test/backups-test.js index decf3f970..107d98e75 100644 --- a/src/test/backups-test.js +++ b/src/test/backups-test.js @@ -95,6 +95,7 @@ describe('backups', function () { describe('cleanup', function () { var BACKUP_0 = { id: 'backup-box-0', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'backup-app-00', 'backup-app-01' ], @@ -104,6 +105,7 @@ describe('backups', function () { var BACKUP_0_APP_0 = { id: 'backup-app-00', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], @@ -113,6 +115,7 @@ describe('backups', function () { var BACKUP_0_APP_1 = { id: 'backup-app-01', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], @@ -122,6 +125,7 @@ describe('backups', function () { var BACKUP_1 = { id: 'backup-box-1', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'backup-app-10', 'backup-app-11' ], @@ -131,6 +135,7 @@ describe('backups', function () { var BACKUP_1_APP_0 = { id: 'backup-app-10', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], @@ -140,6 +145,7 @@ describe('backups', function () { var BACKUP_1_APP_1 = { id: 'backup-app-11', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], diff --git a/src/test/database-test.js b/src/test/database-test.js index 01b4a101b..2a0dd5d09 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -1324,6 +1324,7 @@ describe('database', function () { it('add succeeds', function (done) { var backup = { id: 'backup-box', + encryptionVersion: 2, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'dep1' ], @@ -1340,6 +1341,7 @@ describe('database', function () { it('get succeeds', function (done) { backupdb.get('backup-box', function (error, result) { expect(error).to.be(null); + expect(result.encryptionVersion).to.be(2); expect(result.packageVersion).to.be('1.0.0'); expect(result.type).to.be(backupdb.BACKUP_TYPE_BOX); expect(result.creationTime).to.be.a(Date); @@ -1365,6 +1367,7 @@ describe('database', function () { expect(results.length).to.be(1); expect(results[0].id).to.be('backup-box'); + expect(results[0].encryptionVersion).to.be(2); expect(results[0].packageVersion).to.be('1.0.0'); expect(results[0].dependsOn).to.eql(['dep1']); expect(results[0].manifest).to.eql(null); @@ -1390,6 +1393,7 @@ describe('database', function () { it('add app succeeds', function (done) { var backup = { id: 'app_appid_123', + encryptionVersion: null, packageVersion: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], @@ -1406,6 +1410,7 @@ describe('database', function () { it('get succeeds', function (done) { backupdb.get('app_appid_123', function (error, result) { expect(error).to.be(null); + expect(result.encryptionVersion).to.be(null); expect(result.packageVersion).to.be('1.0.0'); expect(result.type).to.be(backupdb.BACKUP_TYPE_APP); expect(result.creationTime).to.be.a(Date); @@ -1422,6 +1427,7 @@ describe('database', function () { expect(results.length).to.be(1); expect(results[0].id).to.be('app_appid_123'); + expect(results[0].encryptionVersion).to.be(null); expect(results[0].packageVersion).to.be('1.0.0'); expect(results[0].dependsOn).to.eql([]); expect(results[0].manifest).to.eql({ foo: 'bar' });