From f0a56e75f536e3bb542061162a9cbdd5bca0a399 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 31 Jan 2025 17:10:43 +0100 Subject: [PATCH] Improve encrypted backup dialog (#23991) * Improve encrypted backup dialog * Remove unused code --- .../dialog-download-decrypted-backup.ts | 225 ++++++++++++++++++ .../show-dialog-download-decrypted-backup.ts | 21 ++ .../config/backup/ha-config-backup-backups.ts | 7 +- .../config/backup/ha-config-backup-details.ts | 8 +- .../config/backup/helper/download_backup.ts | 182 ++++++-------- src/translations/en.json | 13 +- 6 files changed, 326 insertions(+), 130 deletions(-) create mode 100644 src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts create mode 100644 src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts diff --git a/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts b/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts new file mode 100644 index 0000000000..a9ac25a9d1 --- /dev/null +++ b/src/panels/config/backup/dialogs/dialog-download-decrypted-backup.ts @@ -0,0 +1,225 @@ +import { mdiClose } from "@mdi/js"; +import type { CSSResultGroup } from "lit"; +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-icon-next"; +import "../../../../components/ha-md-dialog"; +import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-md-list"; +import "../../../../components/ha-md-list-item"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-password-field"; +import "../../../../components/ha-alert"; +import { + canDecryptBackupOnDownload, + getPreferredAgentForDownload, +} from "../../../../data/backup"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { downloadBackupFile } from "../helper/download_backup"; +import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download-decrypted-backup"; + +@customElement("ha-dialog-download-decrypted-backup") +class DialogDownloadDecryptedBackup extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + @state() private _params?: DownloadDecryptedBackupDialogParams; + + @query("ha-md-dialog") private _dialog?: HaMdDialog; + + @state() private _encryptionKey = ""; + + @state() private _error = ""; + + public showDialog(params: DownloadDecryptedBackupDialogParams): void { + this._opened = true; + this._params = params; + } + + public closeDialog() { + this._dialog?.close(); + return true; + } + + private _dialogClosed() { + if (this._opened) { + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + this._opened = false; + this._params = undefined; + this._encryptionKey = ""; + this._error = ""; + } + + protected render() { + if (!this._opened || !this._params) { + return nothing; + } + + return html` + + + + + ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.title" + )} + + + +
+

+ ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.description" + )} +

+

+ ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.download_backup_encrypted", + { + download_it_encrypted: html``, + } + )} +

+ + + + ${this._error + ? html`${this._error}` + : nothing} +
+
+ + ${this.hass.localize("ui.dialogs.generic.cancel")} + + + + ${this.hass.localize( + "ui.panel.config.backup.dialogs.download.download" + )} + +
+
+ `; + } + + private _cancel() { + this.closeDialog(); + } + + private async _submit() { + if (this._encryptionKey === "") { + return; + } + try { + await canDecryptBackupOnDownload( + this.hass, + this._params!.backup.backup_id, + this._agentId, + this._encryptionKey + ); + downloadBackupFile( + this.hass, + this._params!.backup.backup_id, + this._agentId, + this._encryptionKey + ); + this.closeDialog(); + } catch (err: any) { + if (err?.code === "password_incorrect") { + this._error = this.hass.localize( + "ui.panel.config.backup.dialogs.download.incorrect_encryption_key" + ); + } else if (err?.code === "decrypt_not_supported") { + this._error = this.hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_not_supported" + ); + } else { + alert(err.message); + } + } + } + + private _keyChanged(ev) { + this._encryptionKey = ev.currentTarget.value; + this._error = ""; + } + + private get _agentId() { + if (this._params?.agentId) { + return this._params.agentId; + } + return getPreferredAgentForDownload( + Object.keys(this._params!.backup.agents) + ); + } + + private async _downloadEncrypted() { + downloadBackupFile( + this.hass, + this._params!.backup.backup_id, + this._agentId + ); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-md-dialog { + --dialog-content-padding: 8px 24px; + max-width: 500px; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-md-dialog { + max-width: none; + } + div[slot="content"] { + margin-top: 0; + } + } + + button.link { + background: none; + border: none; + padding: 0; + font-size: 14px; + color: var(--primary-color); + text-decoration: underline; + cursor: pointer; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-download-decrypted-backup": DialogDownloadDecryptedBackup; + } +} diff --git a/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts b/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts new file mode 100644 index 0000000000..9f6984171d --- /dev/null +++ b/src/panels/config/backup/dialogs/show-dialog-download-decrypted-backup.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { BackupContent } from "../../../../data/backup"; + +export interface DownloadDecryptedBackupDialogParams { + backup: BackupContent; + agentId?: string; +} + +export const loadDownloadDecryptedBackupDialog = () => + import("./dialog-download-decrypted-backup"); + +export const showDownloadDecryptedBackupDialog = ( + element: HTMLElement, + params: DownloadDecryptedBackupDialogParams +) => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-download-decrypted-backup", + dialogImport: loadDownloadDecryptedBackupDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 548cf9190d..4bab6ee118 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -531,12 +531,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } private async _downloadBackup(backup: BackupContent): Promise { - downloadBackup( - this.hass, - this, - backup, - this.config?.create_backup.password - ); + downloadBackup(this.hass, this, backup, this.config); } private async _deleteBackup(backup: BackupContent): Promise { diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index 6a9d8706c3..ee3a701f47 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -401,13 +401,7 @@ class HaConfigBackupDetails extends LitElement { } private async _downloadBackup(agentId?: string): Promise { - await downloadBackup( - this.hass, - this, - this._backup!, - this.config?.create_backup.password, - agentId - ); + await downloadBackup(this.hass, this, this._backup!, this.config, agentId); } private async _deleteBackup(): Promise { diff --git a/src/panels/config/backup/helper/download_backup.ts b/src/panels/config/backup/helper/download_backup.ts index e6b2f8d2c7..53a0d96823 100644 --- a/src/panels/config/backup/helper/download_backup.ts +++ b/src/panels/config/backup/helper/download_backup.ts @@ -1,20 +1,17 @@ import type { LitElement } from "lit"; +import { getSignedPath } from "../../../../data/auth"; +import type { BackupConfig, BackupContent } from "../../../../data/backup"; import { canDecryptBackupOnDownload, getBackupDownloadUrl, getPreferredAgentForDownload, - type BackupContent, } from "../../../../data/backup"; import type { HomeAssistant } from "../../../../types"; -import { - showAlertDialog, - showConfirmationDialog, - showPromptDialog, -} from "../../../lovelace/custom-card-helpers"; -import { getSignedPath } from "../../../../data/auth"; import { fileDownload } from "../../../../util/file_download"; +import { showAlertDialog } from "../../../lovelace/custom-card-helpers"; +import { showDownloadDecryptedBackupDialog } from "../dialogs/show-dialog-download-decrypted-backup"; -const triggerDownload = async ( +export const downloadBackupFile = async ( hass: HomeAssistant, backupId: string, preferedAgent: string, @@ -27,120 +24,79 @@ const triggerDownload = async ( fileDownload(signedUrl.path); }; -const downloadEncryptedBackup = async ( - hass: HomeAssistant, - element: LitElement, - backup: BackupContent, - agentId?: string -) => { - if ( - await showConfirmationDialog(element, { - title: "Encryption key incorrect", - text: hass.localize( - "ui.panel.config.backup.dialogs.download.incorrect_entered_encryption_key" - ), - confirmText: "Download encrypted", - }) - ) { - const agentIds = Object.keys(backup.agents); - const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds); - - triggerDownload(hass, backup.backup_id, preferedAgent); - } -}; - -const requestEncryptionKey = async ( - hass: HomeAssistant, - element: LitElement, - backup: BackupContent, - agentId?: string -): Promise => { - const encryptionKey = await showPromptDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.show_encryption_key.title" - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.incorrect_current_encryption_key" - ), - inputLabel: hass.localize( - "ui.panel.config.backup.dialogs.show_encryption_key.title" - ), - inputType: "password", - confirmText: hass.localize("ui.common.download"), - }); - if (encryptionKey === null) { - return; - } - downloadBackup(hass, element, backup, encryptionKey, agentId, true); -}; - export const downloadBackup = async ( hass: HomeAssistant, element: LitElement, backup: BackupContent, - encryptionKey?: string | null, - agentId?: string, - userProvided = false + backupConfig?: BackupConfig, + agentId?: string ): Promise => { const agentIds = Object.keys(backup.agents); const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds); const isProtected = backup.agents[preferedAgent]?.protected; - if (isProtected) { - if (encryptionKey) { - try { - await canDecryptBackupOnDownload( - hass, - backup.backup_id, - preferedAgent, - encryptionKey - ); - } catch (err: any) { - if (err?.code === "password_incorrect") { - if (userProvided) { - downloadEncryptedBackup(hass, element, backup, agentId); - } else { - requestEncryptionKey(hass, element, backup, agentId); - } - return; - } - if (err?.code === "decrypt_not_supported") { - showAlertDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.download.decryption_unsupported_title" - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.decryption_unsupported" - ), - confirm() { - triggerDownload(hass, backup.backup_id, preferedAgent); - }, - }); - encryptionKey = undefined; - return; - } - - showAlertDialog(element, { - title: hass.localize( - "ui.panel.config.backup.dialogs.download.error_check_title", - { - error: err.message, - } - ), - text: hass.localize( - "ui.panel.config.backup.dialogs.download.error_check_description", - { - error: err.message, - } - ), - }); - return; - } - } else { - requestEncryptionKey(hass, element, backup, agentId); - return; - } + if (!isProtected) { + downloadBackupFile(hass, backup.backup_id, preferedAgent); } - await triggerDownload(hass, backup.backup_id, preferedAgent, encryptionKey); + const encryptionKey = backupConfig?.create_backup?.password; + + if (!encryptionKey) { + showDownloadDecryptedBackupDialog(element, { + backup, + agentId: preferedAgent, + }); + return; + } + + try { + // Check if we can decrypt it + await canDecryptBackupOnDownload( + hass, + backup.backup_id, + preferedAgent, + encryptionKey + ); + downloadBackupFile(hass, backup.backup_id, preferedAgent, encryptionKey); + } catch (err: any) { + // If encryption key is incorrect, ask for encryption key + if (err?.code === "password_incorrect") { + showDownloadDecryptedBackupDialog(element, { + backup, + agentId: preferedAgent, + }); + return; + } + // If decryption is not supported, ask for confirmation and download it encrypted + if (err?.code === "decrypt_not_supported") { + showAlertDialog(element, { + title: hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_unsupported_title" + ), + text: hass.localize( + "ui.panel.config.backup.dialogs.download.decryption_unsupported" + ), + confirm() { + downloadBackupFile(hass, backup.backup_id, preferedAgent); + }, + }); + return; + } + + // Else, show generic error + showAlertDialog(element, { + title: hass.localize( + "ui.panel.config.backup.dialogs.download.error_check_title", + { + error: err.message, + } + ), + text: hass.localize( + "ui.panel.config.backup.dialogs.download.error_check_description", + { + error: err.message, + } + ), + }); + } }; diff --git a/src/translations/en.json b/src/translations/en.json index 47076e459e..e78f1b5e44 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2392,11 +2392,16 @@ "download": { "decryption_unsupported_title": "Decryption unsupported", "decryption_unsupported": "Decryption is not supported for this backup. The downloaded backup will remain encrypted and can't be opened. To restore it, you will need the encryption key.", - "incorrect_entered_encryption_key": "The entered encryption key was incorrect, try again or download the encrypted backup. The encrypted backup can't be opened. To restore it, you will need the encryption key.", - "download_encrypted": "Download encrypted", - "incorrect_current_encryption_key": "This backup is encrypted with a different encryption key than the current one, please enter the encryption key of this backup.", "error_check_title": "Error checking backup", - "error_check_description": "An error occurred while checking the backup, please try again. Error message: {error}" + "error_check_description": "An error occurred while checking the backup, please try again. Error message: {error}", + "title": "Download backup", + "description": "This backup is encrypted with a different encryption key than the current one, please enter the encryption key of this backup.", + "download_backup_encrypted": "You can still {download_it_encrypted}. To restore it, you will need the encryption key.", + "download_it_encrypted": "download the backup encrypted", + "encryption_key": "Encryption key", + "incorrect_encryption_key": "Incorrect encryption key", + "decryption_not_supported": "Decryption not supported", + "download": "Download" } }, "agents": {