From fcb6da55d81433abc1a4924135580a14326fbfa9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 21 Jan 2025 14:48:38 +0100 Subject: [PATCH] Check if we can decrypt backup on download (#23756) Co-authored-by: Kevin Cathcart Co-authored-by: Wendelin Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> --- src/data/backup.ts | 21 ++- .../config/backup/ha-config-backup-backups.ts | 13 +- .../config/backup/ha-config-backup-details.ts | 25 +-- .../config/backup/helper/download_backup.ts | 146 ++++++++++++++++++ src/translations/en.json | 9 ++ 5 files changed, 193 insertions(+), 21 deletions(-) create mode 100644 src/panels/config/backup/helper/download_backup.ts diff --git a/src/data/backup.ts b/src/data/backup.ts index ca1c6a836f85..efb54e0179c0 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -152,8 +152,12 @@ export const updateBackupConfig = ( config: BackupMutableConfig ) => hass.callWS({ type: "backup/config/update", ...config }); -export const getBackupDownloadUrl = (id: string, agentId: string) => - `/api/backup/download/${id}?agent_id=${agentId}`; +export const getBackupDownloadUrl = ( + id: string, + agentId: string, + password?: string | null +) => + `/api/backup/download/${id}?agent_id=${agentId}${password ? `&password=${password}` : ""}`; export const fetchBackupInfo = (hass: HomeAssistant): Promise => hass.callWS({ @@ -246,6 +250,19 @@ export const getPreferredAgentForDownload = (agents: string[]) => { return agents[0]; }; +export const canDecryptBackupOnDownload = ( + hass: HomeAssistant, + backup_id: string, + agent_id: string, + password: string +) => + hass.callWS({ + type: "backup/can_decrypt_on_download", + backup_id, + agent_id, + password, + }); + export const CORE_LOCAL_AGENT = "backup.local"; export const HASSIO_LOCAL_AGENT = "hassio.local"; export const CLOUD_AGENT = "cloud.cloud"; diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index f129900cf02f..5e1eddb86b9c 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -33,15 +33,12 @@ import "../../../components/ha-icon-next"; import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; -import { getSignedPath } from "../../../data/auth"; import type { BackupConfig, BackupContent } from "../../../data/backup"; import { computeBackupAgentName, deleteBackup, generateBackup, generateBackupWithAutomaticSettings, - getBackupDownloadUrl, - getPreferredAgentForDownload, isLocalAgent, isNetworkMountAgent, } from "../../../data/backup"; @@ -60,10 +57,10 @@ import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { bytesToString } from "../../../util/bytes-to-string"; -import { fileDownload } from "../../../util/file_download"; import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; +import { downloadBackup } from "./helper/download_backup"; interface BackupRow extends DataTableRowData, BackupContent { formatted_type: string; @@ -487,12 +484,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { } private async _downloadBackup(backup: BackupContent): Promise { - const preferedAgent = getPreferredAgentForDownload(backup!.agent_ids!); - const signedUrl = await getSignedPath( + downloadBackup( this.hass, - getBackupDownloadUrl(backup.backup_id, preferedAgent) + this, + backup, + this.config?.create_backup.password ); - fileDownload(signedUrl.path); } 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 66a3e1357aeb..5b91462e4623 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -20,15 +20,16 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; -import { getSignedPath } from "../../../data/auth"; -import type { BackupContentExtended, BackupData } from "../../../data/backup"; +import type { + BackupConfig, + BackupContentExtended, + BackupData, +} from "../../../data/backup"; import { compareAgents, computeBackupAgentName, deleteBackup, fetchBackupDetails, - getBackupDownloadUrl, - getPreferredAgentForDownload, isLocalAgent, isNetworkMountAgent, } from "../../../data/backup"; @@ -37,11 +38,11 @@ import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { bytesToString } from "../../../util/bytes-to-string"; -import { fileDownload } from "../../../util/file_download"; -import { showConfirmationDialog } from "../../lovelace/custom-card-helpers"; import "./components/ha-backup-data-picker"; import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup"; import { fireEvent } from "../../../common/dom/fire_event"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { downloadBackup } from "./helper/download_backup"; interface Agent { id: string; @@ -67,6 +68,8 @@ class HaConfigBackupDetails extends LitElement { @property({ attribute: "backup-id" }) public backupId!: string; + @property({ attribute: false }) public config?: BackupConfig; + @state() private _backup?: BackupContentExtended | null; @state() private _agents: Agent[] = []; @@ -377,13 +380,13 @@ class HaConfigBackupDetails extends LitElement { } private async _downloadBackup(agentId?: string): Promise { - const preferedAgent = - agentId ?? getPreferredAgentForDownload(this._backup!.agent_ids!); - const signedUrl = await getSignedPath( + await downloadBackup( this.hass, - getBackupDownloadUrl(this._backup!.backup_id, preferedAgent) + this, + this._backup!, + this.config?.create_backup.password, + agentId ); - fileDownload(signedUrl.path); } private async _deleteBackup(): Promise { diff --git a/src/panels/config/backup/helper/download_backup.ts b/src/panels/config/backup/helper/download_backup.ts new file mode 100644 index 000000000000..6f8b48258e37 --- /dev/null +++ b/src/panels/config/backup/helper/download_backup.ts @@ -0,0 +1,146 @@ +import type { LitElement } from "lit"; +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"; + +const triggerDownload = async ( + hass: HomeAssistant, + backupId: string, + preferedAgent: string, + encryptionKey?: string | null +) => { + const signedUrl = await getSignedPath( + hass, + getBackupDownloadUrl(backupId, preferedAgent, encryptionKey) + ); + 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", + }) + ) { + triggerDownload( + hass, + backup.backup_id, + agentId ?? getPreferredAgentForDownload(backup.agent_ids!) + ); + } +}; + +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 +): Promise => { + const preferedAgent = + agentId ?? getPreferredAgentForDownload(backup.agent_ids!); + + if (backup.protected) { + 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; + } + } + + await triggerDownload(hass, backup.backup_id, preferedAgent, encryptionKey); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 65af29ae6334..a09048adedcb 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2375,6 +2375,15 @@ "show_encryption_key": { "title": "Encryption key", "description": "Make sure you save the encryption key in a secure place so you always have access to your backups." + }, + "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}" } }, "agents": {