Bug 1897278 - Add methods to BackupService to enable / disable encryption. r=backup-reviewers,kpatenio
Differential Revision: https://phabricator.services.mozilla.com/D211438
This commit is contained in:
@@ -25,6 +25,8 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
|
||||
});
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ArchiveEncryptionState:
|
||||
"resource:///modules/backup/ArchiveEncryptionState.sys.mjs",
|
||||
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
||||
JsonSchemaValidator:
|
||||
"resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
|
||||
@@ -113,6 +115,7 @@ export class BackupService extends EventTarget {
|
||||
backupFilePath: "Documents", // TODO: make save location configurable (bug 1895943)
|
||||
backupInProgress: false,
|
||||
scheduledBackupsEnabled: lazy.scheduledBackupsPref,
|
||||
encryptionEnabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,6 +135,23 @@ export class BackupService extends EventTarget {
|
||||
*/
|
||||
#postRecoveryResolver;
|
||||
|
||||
/**
|
||||
* The currently used ArchiveEncryptionState. Callers should use
|
||||
* loadEncryptionState() instead, to ensure that any pre-serialized
|
||||
* encryption state has been read in and deserialized.
|
||||
*
|
||||
* This member can be in 3 states:
|
||||
*
|
||||
* 1. undefined - no attempt has been made to load encryption state from
|
||||
* disk yet.
|
||||
* 2. null - encryption is not enabled.
|
||||
* 3. ArchiveEncryptionState - encryption is enabled.
|
||||
*
|
||||
* @see BackupService.loadEncryptionState();
|
||||
* @type {ArchiveEncryptionState|null|undefined}
|
||||
*/
|
||||
#encState = undefined;
|
||||
|
||||
/**
|
||||
* The name of the folder within the profile folder where this service reads
|
||||
* and writes state to.
|
||||
@@ -196,6 +216,16 @@ export class BackupService extends EventTarget {
|
||||
return "post-recovery.json";
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the serialized ArchiveEncryptionState that is written to disk
|
||||
* if encryption is enabled.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
static get ARCHIVE_ENCRYPTION_STATE_FILE() {
|
||||
return "enc-state.json";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the schema for the backup manifest for a given version.
|
||||
*
|
||||
@@ -264,7 +294,8 @@ export class BackupService extends EventTarget {
|
||||
/**
|
||||
* Create a BackupService instance.
|
||||
*
|
||||
* @param {object} [backupResources=DefaultBackupResources] - Object containing BackupResource classes to associate with this service.
|
||||
* @param {object} [backupResources=DefaultBackupResources]
|
||||
* Object containing BackupResource classes to associate with this service.
|
||||
*/
|
||||
constructor(backupResources = DefaultBackupResources) {
|
||||
super();
|
||||
@@ -355,6 +386,10 @@ export class BackupService extends EventTarget {
|
||||
}
|
||||
);
|
||||
|
||||
let encState = await this.loadEncryptionState(profilePath);
|
||||
let encryptionEnabled = !!encState;
|
||||
lazy.logConsole.debug("Encryption enabled: ", encryptionEnabled);
|
||||
|
||||
// Perform the backup for each resource.
|
||||
for (let resourceClass of sortedResources) {
|
||||
try {
|
||||
@@ -362,6 +397,14 @@ export class BackupService extends EventTarget {
|
||||
`Backing up resource with key ${resourceClass.key}. ` +
|
||||
`Requires encryption: ${resourceClass.requiresEncryption}`
|
||||
);
|
||||
|
||||
if (resourceClass.requiresEncryption && !encryptionEnabled) {
|
||||
lazy.logConsole.debug(
|
||||
"Encryption is not currently enabled. Skipping."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let resourcePath = PathUtils.join(stagingPath, resourceClass.key);
|
||||
await IOUtils.makeDirectory(resourcePath);
|
||||
|
||||
@@ -584,10 +627,15 @@ export class BackupService extends EventTarget {
|
||||
|
||||
/**
|
||||
* Bug 1892532: for now, we only support a single backup file.
|
||||
* If there are other pre-existing backup folders, delete them.
|
||||
* If there are other pre-existing backup folders, delete them - but don't
|
||||
* delete anything that doesn't match the backup folder naming scheme.
|
||||
*/
|
||||
let expectedFormatRegex = /\d{4}(-\d{2}){2}T(\d{2}-){2}\d{2}Z/;
|
||||
for (let existingBackupPath of existingBackups) {
|
||||
if (existingBackupPath !== renamedBackupPath) {
|
||||
if (
|
||||
existingBackupPath !== renamedBackupPath &&
|
||||
existingBackupPath.match(expectedFormatRegex)
|
||||
) {
|
||||
await IOUtils.remove(existingBackupPath, {
|
||||
recursive: true,
|
||||
});
|
||||
@@ -944,4 +992,153 @@ export class BackupService extends EventTarget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal promise that is created on the first call to
|
||||
* loadEncryptionState.
|
||||
*
|
||||
* @type {Promise}
|
||||
*/
|
||||
#loadEncryptionStatePromise = null;
|
||||
|
||||
/**
|
||||
* Returns the current ArchiveEncryptionState. This method will only attempt
|
||||
* to read the state from the disk the first time it is called.
|
||||
*
|
||||
* @param {string} [profilePath=PathUtils.profileDir]
|
||||
* The profile path where the encryption state might exist. This is only
|
||||
* used for testing.
|
||||
* @returns {Promise<ArchiveEncryptionState>}
|
||||
*/
|
||||
loadEncryptionState(profilePath = PathUtils.profileDir) {
|
||||
if (this.#encState !== undefined) {
|
||||
return Promise.resolve(this.#encState);
|
||||
}
|
||||
|
||||
// This little dance makes it so that we only attempt to read the state off
|
||||
// of the disk the first time `loadEncryptionState` is called. Any
|
||||
// subsequent calls will await this same promise, OR, after the state has
|
||||
// been read in, they'll just get the #encState which is set after the
|
||||
// state has been read in.
|
||||
if (!this.#loadEncryptionStatePromise) {
|
||||
this.#loadEncryptionStatePromise = (async () => {
|
||||
// Default this to null here - that way, if we fail to read it in,
|
||||
// the null will indicate that we have at least _tried_ to load the
|
||||
// state.
|
||||
let encState = null;
|
||||
let encStateFile = PathUtils.join(
|
||||
profilePath,
|
||||
BackupService.PROFILE_FOLDER_NAME,
|
||||
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
|
||||
);
|
||||
|
||||
// Try to read in any pre-existing encryption state. If that fails,
|
||||
// we fallback to not encrypting, and only backing up non-sensitive data.
|
||||
try {
|
||||
if (await IOUtils.exists(encStateFile)) {
|
||||
let stateObject = await IOUtils.readJSON(encStateFile);
|
||||
({ instance: encState } =
|
||||
await lazy.ArchiveEncryptionState.initialize(stateObject));
|
||||
}
|
||||
} catch (e) {
|
||||
lazy.logConsole.error(
|
||||
"Failed to read / deserialize archive encryption state file: ",
|
||||
e
|
||||
);
|
||||
// TODO: This kind of error might be worth collecting telemetry on.
|
||||
}
|
||||
|
||||
this.#_state.encryptionEnabled = !!encState;
|
||||
this.stateUpdate();
|
||||
|
||||
this.#encState = encState;
|
||||
return encState;
|
||||
})();
|
||||
}
|
||||
|
||||
return this.#loadEncryptionStatePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables encryption for backups, allowing sensitive data to be backed up.
|
||||
* Throws if encryption is already enabled. After enabling encryption, that
|
||||
* state is written to disk.
|
||||
*
|
||||
* @throws Exception
|
||||
* @param {string} password
|
||||
* A non-blank password ("recovery code") that can be used to derive keys
|
||||
* for encrypting the backup.
|
||||
* @param {string} [profilePath=PathUtils.profileDir]
|
||||
* The profile path where the encryption state will be written. This is only
|
||||
* used for testing.
|
||||
*/
|
||||
async enableEncryption(password, profilePath = PathUtils.profileDir) {
|
||||
lazy.logConsole.debug("Enabling encryption.");
|
||||
let encState = await this.loadEncryptionState(profilePath);
|
||||
if (encState) {
|
||||
throw new Error("Encryption is already enabled.");
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new Error("Cannot supply a blank password.");
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw new Error("Password must be at least 8 characters.");
|
||||
}
|
||||
|
||||
// TODO: Enforce other password rules here, such as ensuring that the
|
||||
// password is not considered common.
|
||||
({ instance: encState } = await lazy.ArchiveEncryptionState.initialize(
|
||||
password
|
||||
));
|
||||
if (!encState) {
|
||||
throw new Error("Failed to construct ArchiveEncryptionState");
|
||||
}
|
||||
|
||||
this.#encState = encState;
|
||||
|
||||
let encStateFile = PathUtils.join(
|
||||
profilePath,
|
||||
BackupService.PROFILE_FOLDER_NAME,
|
||||
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
|
||||
);
|
||||
|
||||
let stateObj = await encState.serialize();
|
||||
await IOUtils.writeJSON(encStateFile, stateObj);
|
||||
|
||||
this.#_state.encryptionEnabled = true;
|
||||
this.stateUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables encryption of backups. Throws is encryption is already disabled.
|
||||
*
|
||||
* @throws Exception
|
||||
* @param {string} [profilePath=PathUtils.profileDir]
|
||||
* The profile path where the encryption state exists. This is only used for
|
||||
* testing.
|
||||
* @returns {Promise<undefined>}
|
||||
*/
|
||||
async disableEncryption(profilePath = PathUtils.profileDir) {
|
||||
lazy.logConsole.debug("Disabling encryption.");
|
||||
let encState = await this.loadEncryptionState(profilePath);
|
||||
if (!encState) {
|
||||
throw new Error("Encryption is already disabled.");
|
||||
}
|
||||
|
||||
let encStateFile = PathUtils.join(
|
||||
profilePath,
|
||||
BackupService.PROFILE_FOLDER_NAME,
|
||||
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
|
||||
);
|
||||
// It'd be pretty strange, but not impossible, for something else to have
|
||||
// gotten rid of the encryption state file at this point. We'll ignore it
|
||||
// if that's the case.
|
||||
await IOUtils.remove(encStateFile, { ignoreAbsent: true });
|
||||
|
||||
this.#encState = null;
|
||||
this.#_state.encryptionEnabled = false;
|
||||
this.stateUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user