mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Improve encrypted backup dialog (#23991)
* Improve encrypted backup dialog * Remove unused code
This commit is contained in:
parent
15f33e1f19
commit
f0a56e75f5
@ -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`
|
||||||
|
<ha-md-dialog open @closed=${this._dialogClosed} disable-cancel-action>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.title"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
|
||||||
|
{
|
||||||
|
download_it_encrypted: html`<button
|
||||||
|
class="link"
|
||||||
|
@click=${this._downloadEncrypted}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
|
||||||
|
)}
|
||||||
|
</button>`,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-password-field
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.encryption_key"
|
||||||
|
)}
|
||||||
|
@input=${this._keyChanged}
|
||||||
|
></ha-password-field>
|
||||||
|
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
|
<ha-button @click=${this._cancel}>
|
||||||
|
${this.hass.localize("ui.dialogs.generic.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
|
||||||
|
<ha-button @click=${this._submit}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
@ -531,12 +531,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||||
downloadBackup(
|
downloadBackup(this.hass, this, backup, this.config);
|
||||||
this.hass,
|
|
||||||
this,
|
|
||||||
backup,
|
|
||||||
this.config?.create_backup.password
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
||||||
|
@ -401,13 +401,7 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _downloadBackup(agentId?: string): Promise<void> {
|
private async _downloadBackup(agentId?: string): Promise<void> {
|
||||||
await downloadBackup(
|
await downloadBackup(this.hass, this, this._backup!, this.config, agentId);
|
||||||
this.hass,
|
|
||||||
this,
|
|
||||||
this._backup!,
|
|
||||||
this.config?.create_backup.password,
|
|
||||||
agentId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteBackup(): Promise<void> {
|
private async _deleteBackup(): Promise<void> {
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
|
import { getSignedPath } from "../../../../data/auth";
|
||||||
|
import type { BackupConfig, BackupContent } from "../../../../data/backup";
|
||||||
import {
|
import {
|
||||||
canDecryptBackupOnDownload,
|
canDecryptBackupOnDownload,
|
||||||
getBackupDownloadUrl,
|
getBackupDownloadUrl,
|
||||||
getPreferredAgentForDownload,
|
getPreferredAgentForDownload,
|
||||||
type BackupContent,
|
|
||||||
} from "../../../../data/backup";
|
} from "../../../../data/backup";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
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 { 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,
|
hass: HomeAssistant,
|
||||||
backupId: string,
|
backupId: string,
|
||||||
preferedAgent: string,
|
preferedAgent: string,
|
||||||
@ -27,83 +24,50 @@ const triggerDownload = async (
|
|||||||
fileDownload(signedUrl.path);
|
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<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 (
|
export const downloadBackup = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
element: LitElement,
|
element: LitElement,
|
||||||
backup: BackupContent,
|
backup: BackupContent,
|
||||||
encryptionKey?: string | null,
|
backupConfig?: BackupConfig,
|
||||||
agentId?: string,
|
agentId?: string
|
||||||
userProvided = false
|
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const agentIds = Object.keys(backup.agents);
|
const agentIds = Object.keys(backup.agents);
|
||||||
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
||||||
const isProtected = backup.agents[preferedAgent]?.protected;
|
const isProtected = backup.agents[preferedAgent]?.protected;
|
||||||
|
|
||||||
if (isProtected) {
|
if (!isProtected) {
|
||||||
if (encryptionKey) {
|
downloadBackupFile(hass, backup.backup_id, preferedAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionKey = backupConfig?.create_backup?.password;
|
||||||
|
|
||||||
|
if (!encryptionKey) {
|
||||||
|
showDownloadDecryptedBackupDialog(element, {
|
||||||
|
backup,
|
||||||
|
agentId: preferedAgent,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if we can decrypt it
|
||||||
await canDecryptBackupOnDownload(
|
await canDecryptBackupOnDownload(
|
||||||
hass,
|
hass,
|
||||||
backup.backup_id,
|
backup.backup_id,
|
||||||
preferedAgent,
|
preferedAgent,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
);
|
);
|
||||||
|
downloadBackupFile(hass, backup.backup_id, preferedAgent, encryptionKey);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// If encryption key is incorrect, ask for encryption key
|
||||||
if (err?.code === "password_incorrect") {
|
if (err?.code === "password_incorrect") {
|
||||||
if (userProvided) {
|
showDownloadDecryptedBackupDialog(element, {
|
||||||
downloadEncryptedBackup(hass, element, backup, agentId);
|
backup,
|
||||||
} else {
|
agentId: preferedAgent,
|
||||||
requestEncryptionKey(hass, element, backup, agentId);
|
});
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If decryption is not supported, ask for confirmation and download it encrypted
|
||||||
if (err?.code === "decrypt_not_supported") {
|
if (err?.code === "decrypt_not_supported") {
|
||||||
showAlertDialog(element, {
|
showAlertDialog(element, {
|
||||||
title: hass.localize(
|
title: hass.localize(
|
||||||
@ -113,13 +77,13 @@ export const downloadBackup = async (
|
|||||||
"ui.panel.config.backup.dialogs.download.decryption_unsupported"
|
"ui.panel.config.backup.dialogs.download.decryption_unsupported"
|
||||||
),
|
),
|
||||||
confirm() {
|
confirm() {
|
||||||
triggerDownload(hass, backup.backup_id, preferedAgent);
|
downloadBackupFile(hass, backup.backup_id, preferedAgent);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
encryptionKey = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Else, show generic error
|
||||||
showAlertDialog(element, {
|
showAlertDialog(element, {
|
||||||
title: hass.localize(
|
title: hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.download.error_check_title",
|
"ui.panel.config.backup.dialogs.download.error_check_title",
|
||||||
@ -134,13 +98,5 @@ export const downloadBackup = async (
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
requestEncryptionKey(hass, element, backup, agentId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await triggerDownload(hass, backup.backup_id, preferedAgent, encryptionKey);
|
|
||||||
};
|
};
|
||||||
|
@ -2392,11 +2392,16 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"decryption_unsupported_title": "Decryption unsupported",
|
"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.",
|
"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_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": {
|
"agents": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user