Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix error migrating key backup to 4S #12441

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/SecurityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { deriveKey } from "matrix-js-sdk/src/crypto/key_passphrase";
import { decodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";
import { logger } from "matrix-js-sdk/src/logger";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";

import type CreateSecretStorageDialog from "./async-components/views/dialogs/security/CreateSecretStorageDialog";
import Modal from "./Modal";
Expand Down Expand Up @@ -297,7 +298,7 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
RestoreKeyBackupDialog,
{
showSummary: false,
keyCallback: (k: Uint8Array) => (key = k),
keyCallback: (k: Uint8Array, _: GeneratedSecretStorageKey) => (key = k),
},
undefined,
/* priority = */ false,
Expand Down Expand Up @@ -430,7 +431,7 @@ async function doAccessSecretStorage(func: () => Promise<void>, forceReset: bool
// inner operation completes.
return await func();
} catch (e) {
ModuleRunner.instance.extensions.cryptoSetup.catchAccessSecretStorageError(e as Error);
SecurityCustomisations.catchAccessSecretStorageError?.(e);
logger.error(e);
// Re-throw so that higher level logic can abort as needed
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ limitations under the License.
import React, { createRef } from "react";
import FileSaver from "file-saver";
import { logger } from "matrix-js-sdk/src/logger";
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix";
import { AuthDict, CrossSigningKeys, MatrixError, UIAFlow, UIAResponse, encodeBase64 } from "matrix-js-sdk/src/matrix";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import classNames from "classnames";
import { BackupTrustInfo, GeneratedSecretStorageKey, KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { encodeRecoveryKey } from "matrix-js-sdk/src/crypto/recoverykey";

import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t, _td } from "../../../../languageHandler";
Expand Down Expand Up @@ -56,6 +57,7 @@ enum Phase {
LoadError = "load_error",
ChooseKeyPassphrase = "choose_key_passphrase",
Migrate = "migrate",
MigrationError = "migration_error",
Passphrase = "passphrase",
PassphraseConfirm = "passphrase_confirm",
ShowKey = "show_key",
Expand Down Expand Up @@ -126,6 +128,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private backupKey?: Uint8Array;
private recoveryKeyNode = createRef<HTMLElement>();
private passphraseField = createRef<Field>();
private usedBackupWithout4s?: boolean;

public constructor(props: IProps) {
super(props);
Expand Down Expand Up @@ -397,6 +400,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
return promptForBackupPassphrase();
},
});
await this.resignBackupIfRequired();
}
await initialiseDehydration(true);

Expand Down Expand Up @@ -429,8 +433,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
private restoreBackup = async (): Promise<void> => {
// It's possible we'll need the backup key later on for bootstrapping,
// so let's stash it here, rather than prompting for it twice.
const keyCallback = (k: Uint8Array): void => {
const keyCallback = (k: Uint8Array, recoveryKey: GeneratedSecretStorageKey): void => {
this.backupKey = k;
this.recoveryKey = recoveryKey;
};

const { finished } = Modal.createDialog(
Expand All @@ -448,9 +453,90 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
const backupTrustInfo = await this.fetchBackupInfo();
if (backupTrustInfo?.trusted && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
this.bootstrapSecretStorage();
} else {
this.checkForBackupMigrationAndApply(backupTrustInfo);
}
};

/**
* Retrieve the secret storage key (with which other keys are encrypted). Since this method can indirectly use
* {@link SecurityManager#getSecretStorageKey} a AccessSecretStorageDialog can be shown if the secret storage was never accessed before.
*
* @returns The secret storage key, if any can be retrieved and null otherwise.
*/
private async getSecretStorageKeyAsString(): Promise<string | null> {
try {
const cli = MatrixClientPeg.safeGet();
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();

if (!defaultKeyId) return null;

const secretStorageKeyTuple = await cli.secretStorage.getKey(defaultKeyId);

if (!secretStorageKeyTuple) return null;

const [, keyInfo] = secretStorageKeyTuple;
const possibleSsKPair = await cli!.cryptoCallbacks!.getSecretStorageKey?.({ keys: { [defaultKeyId!]: keyInfo } }, "");

if (!possibleSsKPair) return null;

const [, secretStorageKeyAsUint8Array] = possibleSsKPair;

return encodeRecoveryKey(secretStorageKeyAsUint8Array)!;
} catch(e) {
logger.error(`Coudln't run getSecretStorageKeyAsString!`);
}

return null;
}

/**
* If the keys of the "current" backup can be decrypted and no 4s was setup, then usedBackupWithout4s is set to true and bootstrapSecretStorage is invoked.
* Before this it is ensured that this.recoveryKey is set correctly. If this is not possible an error dialog is shown which ask to re-login.
*
* @param backupTrustInfo Required to check whether the keys of the current backup can be decrypted.
* @returns
*/
private async checkForBackupMigrationAndApply(backupTrustInfo: BackupTrustInfo | undefined): Promise<void> {
const cli = MatrixClientPeg.safeGet();

// The user has not entered the correct passphrase / recovery key or 4s is already set up.
if (!(backupTrustInfo?.matchesDecryptionKey)
|| await cli.secretStorage.hasKey()) return;

if (!this.recoveryKey) {
const secretStorageKeyAsString = await this.getSecretStorageKeyAsString();

if (!secretStorageKeyAsString) {
this.setState({ phase: Phase.MigrationError });
return;
} else {
const privatKey = new TextEncoder().encode(secretStorageKeyAsString);
this.recoveryKey = { privateKey: privatKey, encodedPrivateKey: secretStorageKeyAsString }
}
}

this.usedBackupWithout4s = true;
this.bootstrapSecretStorage();
}

private async resignBackupIfRequired(): Promise<void> {
if (this.usedBackupWithout4s) {
const cli = MatrixClientPeg.safeGet();
const crypto = cli.getCrypto()!;

const privKey = await crypto.getSessionBackupPrivateKey();
const defaultKeyId = await cli.secretStorage.getDefaultKeyId();
await cli.secretStorage.store("m.megolm_backup.v1", encodeBase64(privKey!), [defaultKeyId!]);

if (this.state.backupInfo?.version) {
await crypto.resignKeyBackup(privKey!, this.state.backupInfo.version);
} else {
logger.error(`It was not possible to run resignBackupIfRequired because no backupInfo existed or its version property was not set.`)
}
}
}

private onLoadRetryClick = (): void => {
this.setState({ phase: Phase.Loading });
this.fetchBackupInfo();
Expand Down Expand Up @@ -855,6 +941,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
);
}

private renderMigrationError(): JSX.Element {
return (
<div>
<p>{_t("settings|key_backup|setup_secure_backup|secret_storage_migration_failure")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons
primaryButton={_t("action|retry")}
onPrimaryButtonClick={this.onLoadRetryClick}
hasCancel={this.state.canSkip}
onCancel={this.onCancel}
/>
</div>
</div>
);
}

private titleForPhase(phase: Phase): string {
switch (phase) {
case Phase.ChooseKeyPassphrase:
Expand Down Expand Up @@ -940,6 +1042,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
case Phase.ConfirmSkip:
content = this.renderPhaseSkipConfirm();
break;
case Phase.MigrationError:
content = this.renderMigrationError();
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React, { ChangeEvent } from "react";
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
import { logger } from "matrix-js-sdk/src/logger";
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";

import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { _t } from "../../../../languageHandler";
Expand Down Expand Up @@ -46,7 +47,7 @@ interface IProps {
showSummary?: boolean;
// If specified, gather the key from the user but then call the function with the backup
// key rather than actually (necessarily) restoring the backup.
keyCallback?: (key: Uint8Array) => void;
keyCallback?: (key: Uint8Array, recoveryKey: GeneratedSecretStorageKey) => void;
onFinished(done?: boolean): void;
}

Expand Down Expand Up @@ -154,7 +155,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
this.state.passPhrase,
this.state.backupInfo,
);
this.props.keyCallback(key);
const recoveryKey = await MatrixClientPeg.safeGet().getCrypto()!.createRecoveryKeyFromPassphrase(this.state.passPhrase!);
this.props.keyCallback(key, recoveryKey);
}

if (!this.props.showSummary) {
Expand Down Expand Up @@ -192,7 +194,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
);
if (this.props.keyCallback) {
const key = MatrixClientPeg.safeGet().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
this.props.keyCallback(key);
const secretStorageKey = new TextEncoder().encode(this.state.recoveryKey);
this.props.keyCallback(key, { privateKey: secretStorageKey });
}
if (!this.props.showSummary) {
this.props.onFinished(true);
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -2514,6 +2514,7 @@
"requires_password_confirmation": "Gib dein Kontopasswort ein, um die Aktualisierung zu bestätigen:",
"requires_server_authentication": "Du musst dich authentifizieren, um die Aktualisierung zu bestätigen.",
"secret_storage_query_failure": "Status des sicheren Speichers kann nicht gelesen werden",
"secret_storage_migration_failure": "Während des Backup-Migrationsprozesses ist ein Fehler aufgetreten. Wir können nicht auf deinen Wiederherstellungsschlüssel zugreifen. Bitte versuche es erneut.",
"security_key_safety_reminder": "Bewahre deinen Sicherheitsschlüssel sicher auf, etwa in einem Passwortmanager oder einem Safe, da er verwendet wird, um deine Daten zu sichern.",
"session_upgrade_description": "Aktualisiere diese Sitzung, um mit ihr andere Sitzungen verifizieren zu können, damit sie Zugang zu verschlüsselten Nachrichten erhalten und für andere als vertrauenswürdig markiert werden.",
"set_phrase_again": "Gehe zurück und setze es erneut.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2547,6 +2547,7 @@
"requires_password_confirmation": "Enter your account password to confirm the upgrade:",
"requires_server_authentication": "You'll need to authenticate with the server to confirm the upgrade.",
"secret_storage_query_failure": "Unable to query secret storage status",
"secret_storage_migration_failure": "An error occurred during the backup migration process. We cannot access your recovery key. Please try it again.",
"security_key_safety_reminder": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.",
"session_upgrade_description": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.",
"set_phrase_again": "Go back to set it again.",
Expand Down
Loading