Skip to content

Commit

Permalink
Check if we can decrypt backup on download (#23756)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Cathcart <[email protected]>
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
  • Loading branch information
4 people authored Jan 21, 2025
1 parent 87907b9 commit fcb6da5
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 21 deletions.
21 changes: 19 additions & 2 deletions src/data/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BackupInfo> =>
hass.callWS({
Expand Down Expand Up @@ -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";
Expand Down
13 changes: 5 additions & 8 deletions src/panels/config/backup/ha-config-backup-backups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -487,12 +484,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}

private async _downloadBackup(backup: BackupContent): Promise<void> {
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<void> {
Expand Down
25 changes: 14 additions & 11 deletions src/panels/config/backup/ha-config-backup-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -377,13 +380,13 @@ class HaConfigBackupDetails extends LitElement {
}

private async _downloadBackup(agentId?: string): Promise<void> {
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<void> {
Expand Down
146 changes: 146 additions & 0 deletions src/panels/config/backup/helper/download_backup.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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<void> => {
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);
};
9 changes: 9 additions & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit fcb6da5

Please sign in to comment.