mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-26 18:38:30 +00:00
update 1.2.2.3 beta
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
||||
6da865786f93c94359fd4428876a953f2706d752bd28120510ef5330f1d420aa ProxMenux-1.2.2.3-beta.AppImage
|
||||
8db4067d1bcd3d7cd440d4de72d449e8b47480787f3650b35213c83a4bfade40 ProxMenux-1.2.2.3-beta.AppImage
|
||||
|
||||
@@ -594,48 +594,21 @@ function KeyfileActionsBar({
|
||||
mutateStatus: () => Promise<unknown>
|
||||
escrowMode?: "none" | "local" | "full"
|
||||
}) {
|
||||
const [action, setAction] = useState<null | "passphrase" | "escrow">(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [err, setErr] = useState<string | null>(null)
|
||||
const [pass1, setPass1] = useState("")
|
||||
const [pass2, setPass2] = useState("")
|
||||
const [newEscrowMode, setNewEscrowMode] = useState<"none" | "local" | "full">(escrowMode ?? "full")
|
||||
// Pending radio state — reflects the operator's intended mode after
|
||||
// Apply. Seeded from the current mode so a No-op click on Apply is
|
||||
// rejected. Binary UX ('none' vs 'full') matches the setup wizard.
|
||||
const currentIsFull = escrowMode === "full"
|
||||
const [pendingMode, setPendingMode] = useState<"none" | "full">(currentIsFull ? "full" : "none")
|
||||
|
||||
const close = () => {
|
||||
setAction(null)
|
||||
setBusy(false)
|
||||
setErr(null)
|
||||
setPass1("")
|
||||
setPass2("")
|
||||
setNewEscrowMode(escrowMode ?? "full")
|
||||
}
|
||||
|
||||
const runPassphrase = async () => {
|
||||
if (pass1 !== pass2) {
|
||||
setErr("Passphrases do not match.")
|
||||
return
|
||||
}
|
||||
setBusy(true)
|
||||
setErr(null)
|
||||
try {
|
||||
await fetchApi("/api/host-backups/pbs-encryption/passphrase", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ new_passphrase: pass1 }),
|
||||
})
|
||||
await mutateStatus()
|
||||
close()
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runEscrowMode = async () => {
|
||||
// Local/full → passphrase required + match. None → passphrase ignored.
|
||||
if (newEscrowMode !== "none") {
|
||||
const runApply = async () => {
|
||||
// Yes → passphrase required + match. No → passphrase ignored.
|
||||
if (pendingMode === "full") {
|
||||
if (!pass1) {
|
||||
setErr("Passphrase is required for local or full escrow.")
|
||||
setErr("Recovery passphrase is required.")
|
||||
return
|
||||
}
|
||||
if (pass1 !== pass2) {
|
||||
@@ -646,179 +619,168 @@ function KeyfileActionsBar({
|
||||
setBusy(true)
|
||||
setErr(null)
|
||||
try {
|
||||
const body: Record<string, unknown> = { escrow_mode: newEscrowMode }
|
||||
if (newEscrowMode !== "none") body.escrow_passphrase = pass1
|
||||
const body: Record<string, unknown> = { escrow_mode: pendingMode }
|
||||
if (pendingMode === "full") body.escrow_passphrase = pass1
|
||||
await fetchApi("/api/host-backups/pbs-encryption/set-escrow-mode", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
await mutateStatus()
|
||||
close()
|
||||
setPass1("")
|
||||
setPass2("")
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
// The UX we expose is binary: "is the key uploaded to PBS?" — yes
|
||||
// (mode='full') or no (mode='none'). The 'local' backend mode still
|
||||
// exists but is not surfaced here; if a host somehow reports it,
|
||||
// show it verbatim so the state isn't lost on the operator.
|
||||
const uploadOn = escrowMode === "full"
|
||||
const escrowLabel = escrowMode === undefined
|
||||
? undefined
|
||||
: escrowMode === "full"
|
||||
? "Uploaded to PBS on every backup"
|
||||
: escrowMode === "local"
|
||||
? "Local envelope only (advanced mode)"
|
||||
: "Kept only on this host — you handle the offsite copy"
|
||||
// Contextual Apply-button label. Three transitions:
|
||||
// No → Yes → "Start uploading"
|
||||
// Yes → No → "Stop uploading"
|
||||
// Yes → Yes (new pw) → "Update passphrase" (rewraps envelope)
|
||||
const applyLabel =
|
||||
pendingMode === "full" && !currentIsFull
|
||||
? "Start uploading"
|
||||
: pendingMode === "none" && currentIsFull
|
||||
? "Stop uploading"
|
||||
: pendingMode === "full" && currentIsFull
|
||||
? "Update passphrase"
|
||||
: "Apply"
|
||||
|
||||
// Apply is enabled when there is a real change to commit.
|
||||
const canApply = (
|
||||
(pendingMode !== (currentIsFull ? "full" : "none")) || // mode change
|
||||
(pendingMode === "full" && !!pass1 && pass1 === pass2) // passphrase update
|
||||
)
|
||||
|
||||
const download = () => {
|
||||
const a = document.createElement("a")
|
||||
a.href = "/api/host-backups/pbs-encryption/download-keyfile"
|
||||
a.download = "pbs-key.conf"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="text-[11px] font-medium text-foreground">Manage installed keyfile</div>
|
||||
{escrowLabel !== undefined && (
|
||||
<div className="flex items-center gap-2 text-[10.5px] text-muted-foreground bg-background/40 border border-white/10 rounded px-2 py-1">
|
||||
|
||||
{/* Current status — icon + colour by state, no truncated fp. */}
|
||||
{escrowMode !== undefined && (
|
||||
<div className="flex items-center gap-2 text-xs bg-background/40 border border-white/10 rounded px-2.5 py-1.5">
|
||||
<span className="font-medium text-foreground">Upload to PBS:</span>
|
||||
<span>{uploadOn ? "Yes" : "No"} — {escrowLabel}</span>
|
||||
{currentIsFull ? (
|
||||
<span className="inline-flex items-center gap-1 text-emerald-400 font-medium">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
Yes — envelope uploaded on every backup
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-blue-400 font-medium">
|
||||
No — kept only on this host
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Change mode + passphrase — one integrated form, no popover.
|
||||
Radio picks the intent; the passphrase pair unfolds when the
|
||||
intent is Yes (both for a first-time upload and for a
|
||||
passphrase rotation while already in Yes). */}
|
||||
<div className="space-y-2 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
|
||||
<div className="text-[11px] font-medium text-foreground">Upload key to PBS?</div>
|
||||
<div className="grid gap-1.5">
|
||||
<label className="flex items-start gap-2 cursor-pointer text-[11px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="pendingModeChange"
|
||||
checked={pendingMode === "none"}
|
||||
onChange={() => { setPendingMode("none"); setPass1(""); setPass2("") }}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">No, keep local only</div>
|
||||
<div className="text-muted-foreground">Keyfile stays at <code className="font-mono text-[10.5px]">/usr/local/share/proxmenux/pbs-key.conf</code>. You handle the offsite copy — use <em>Download keyfile</em> below.</div>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-start gap-2 cursor-pointer text-[11px]">
|
||||
<input
|
||||
type="radio"
|
||||
name="pendingModeChange"
|
||||
checked={pendingMode === "full"}
|
||||
onChange={() => setPendingMode("full")}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Yes, upload</div>
|
||||
<div className="text-muted-foreground">A passphrase-wrapped copy of the keyfile is uploaded to PBS with every backup. Fill the passphrase pair below to enable — or to rotate the current one.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{pendingMode === "full" && (
|
||||
<div className="space-y-2 pt-2 border-t border-blue-500/20">
|
||||
<div>
|
||||
<Label htmlFor="mgmtPass1" className="text-[11px]">
|
||||
{currentIsFull ? "New recovery passphrase" : "Recovery passphrase"}
|
||||
</Label>
|
||||
<Input
|
||||
id="mgmtPass1"
|
||||
type="password"
|
||||
value={pass1}
|
||||
onChange={(e) => setPass1(e.target.value)}
|
||||
placeholder={currentIsFull ? "Type a new passphrase to rotate" : "Long random string — write it down somewhere safe"}
|
||||
className="font-mono mt-1 h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="mgmtPass2" className="text-[11px]">Confirm passphrase</Label>
|
||||
<Input
|
||||
id="mgmtPass2"
|
||||
type="password"
|
||||
value={pass2}
|
||||
onChange={(e) => setPass2(e.target.value)}
|
||||
placeholder="Type it again"
|
||||
className="font-mono mt-1 h-8 text-xs"
|
||||
/>
|
||||
{pass1 && pass2 && pass1 !== pass2 && (
|
||||
<p className="text-[11px] text-red-400 mt-1">Passphrases don't match.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{err && (
|
||||
<div className="text-[11px] text-red-500 px-2 py-1.5 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={runApply}
|
||||
disabled={busy || !canApply}
|
||||
className="!bg-blue-500 hover:!bg-blue-600 !text-white disabled:opacity-50"
|
||||
>
|
||||
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : applyLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px] !text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10"
|
||||
onClick={() => {
|
||||
// Trigger a plain browser download of the canonical keyfile.
|
||||
// No fetch/save-file dance — the browser handles it, so an
|
||||
// ad-blocker or a stale SWR cache can't get in the way.
|
||||
const a = document.createElement("a")
|
||||
a.href = "/api/host-backups/pbs-encryption/download-keyfile"
|
||||
a.download = "pbs-key.conf"
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}}
|
||||
onClick={download}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5 mr-1" />
|
||||
Download keyfile
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px]"
|
||||
onClick={() => setAction("passphrase")}
|
||||
>
|
||||
Update stored passphrase
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 text-[11px]"
|
||||
onClick={() => { setNewEscrowMode(uploadOn ? "none" : "full"); setAction("escrow") }}
|
||||
>
|
||||
{uploadOn ? "Stop uploading to PBS" : "Start uploading to PBS"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={action === "passphrase"} onOpenChange={(v) => !v && close()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update stored keyfile passphrase</DialogTitle>
|
||||
<DialogDescription>
|
||||
This only rotates the passphrase ProxMenux uses to unlock the keyfile at backup time (stored in <code className="font-mono">/usr/local/share/proxmenux/pbs-key.pass</code>). It does <strong>not</strong> re-encrypt the keyfile itself.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="mpass1" className="text-xs">New passphrase</Label>
|
||||
<Input id="mpass1" type="password" value={pass1} onChange={(e) => setPass1(e.target.value)} placeholder="Leave blank to remove the stored passphrase" className="font-mono h-9" />
|
||||
</div>
|
||||
{pass1 && (
|
||||
<div>
|
||||
<Label htmlFor="mpass2" className="text-xs">Confirm new passphrase</Label>
|
||||
<Input id="mpass2" type="password" value={pass2} onChange={(e) => setPass2(e.target.value)} className="font-mono h-9" />
|
||||
</div>
|
||||
)}
|
||||
{err && (
|
||||
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={close}>Cancel</Button>
|
||||
<Button onClick={runPassphrase} disabled={busy || (!!pass1 && pass1 !== pass2)}>
|
||||
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : "Save"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={action === "escrow"} onOpenChange={(v) => !v && close()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{newEscrowMode === "full" ? "Start uploading key to PBS" : "Stop uploading key to PBS"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{newEscrowMode === "full"
|
||||
? "ProxMenux will wrap the keyfile with a passphrase and upload the envelope to PBS after every backup. A reinstalled host can recover the key with just the passphrase."
|
||||
: "The passphrase-wrapped copy on this host will be dropped and no envelope will be uploaded on future backups. The keyfile itself stays at its canonical path — but you become responsible for keeping an offsite copy."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
{newEscrowMode === "full" ? (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="escrowPass1" className="text-xs">Recovery passphrase</Label>
|
||||
<Input
|
||||
id="escrowPass1"
|
||||
type="password"
|
||||
value={pass1}
|
||||
onChange={(e) => setPass1(e.target.value)}
|
||||
placeholder="Long random string — write it down somewhere safe"
|
||||
className="font-mono h-9 mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="escrowPass2" className="text-xs">Confirm passphrase</Label>
|
||||
<Input
|
||||
id="escrowPass2"
|
||||
type="password"
|
||||
value={pass2}
|
||||
onChange={(e) => setPass2(e.target.value)}
|
||||
placeholder="Type it again"
|
||||
className="font-mono h-9 mt-1"
|
||||
/>
|
||||
{pass1 && pass2 && pass1 !== pass2 && (
|
||||
<p className="text-xs text-red-400 mt-1">Passphrases don't match.</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-[11px] text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2 leading-relaxed space-y-1">
|
||||
<div className="font-semibold">Losing the keyfile makes every encrypted backup on PBS unreadable.</div>
|
||||
<div>Keyfile path on this host: <code className="font-mono text-[10.5px] bg-amber-500/20 rounded px-1">/usr/local/share/proxmenux/pbs-key.conf</code></div>
|
||||
<div>Copy that file to an offsite location (USB, password manager, another host) before continuing.</div>
|
||||
</div>
|
||||
)}
|
||||
{err && (
|
||||
<div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={close} disabled={busy}>Cancel</Button>
|
||||
<Button onClick={runEscrowMode} disabled={busy}>
|
||||
{busy ? <Loader2 className="h-4 w-4 animate-spin" /> : (newEscrowMode === "full" ? "Enable upload" : "Stop upload")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* NOTE: no Remove keyfile dialog here — deletion lives inside
|
||||
each PBS destination row (PbsKeyfileActions), so the setup
|
||||
menus of Create Job / Manual Backup only cover key-related
|
||||
toggles (download / passphrase / escrow) and never surface
|
||||
a destructive delete. */}
|
||||
{/* Deletion lives inside the PBS destination row (PbsKeyfileActions);
|
||||
setup menus of Create Job / Manual Backup never surface it. */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -687,38 +687,6 @@ _bk_manage_destinations() {
|
||||
done
|
||||
}
|
||||
|
||||
# Backup the current keyfile + its paired files to /root under a
|
||||
# timestamped prefix. Returns the timestamp on success (empty on
|
||||
# failure). Never overwrites existing files. Used before any
|
||||
# destructive change (replace / remove) so the operator can still
|
||||
# decrypt pre-existing backups if they need to.
|
||||
_bk_pbs_backup_keyfile_to_root() {
|
||||
local ts key_file="$HB_STATE_DIR/pbs-key.conf"
|
||||
local pass_file="$HB_STATE_DIR/pbs-key.pass"
|
||||
local recovery_enc="$HB_STATE_DIR/pbs-key.recovery.enc"
|
||||
[[ -f "$key_file" ]] || { echo ""; return 1; }
|
||||
ts=$(date +%Y%m%d_%H%M%S)
|
||||
umask 077
|
||||
cp -f "$key_file" "/root/pbs-key.old-${ts}.conf" 2>/dev/null || { echo ""; return 1; }
|
||||
chmod 600 "/root/pbs-key.old-${ts}.conf" 2>/dev/null || true
|
||||
if [[ -f "$pass_file" ]]; then
|
||||
cp -f "$pass_file" "/root/pbs-key.old-${ts}.pass" 2>/dev/null || true
|
||||
chmod 600 "/root/pbs-key.old-${ts}.pass" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -f "$recovery_enc" ]]; then
|
||||
cp -f "$recovery_enc" "/root/pbs-key.old-${ts}.recovery.enc" 2>/dev/null || true
|
||||
chmod 600 "/root/pbs-key.old-${ts}.recovery.enc" 2>/dev/null || true
|
||||
fi
|
||||
echo "$ts"
|
||||
}
|
||||
|
||||
# Wipe local keyfile + pass + recovery. Never touches PBS.
|
||||
_bk_pbs_wipe_local_keyfile() {
|
||||
rm -f "$HB_STATE_DIR/pbs-key.conf" \
|
||||
"$HB_STATE_DIR/pbs-key.pass" \
|
||||
"$HB_STATE_DIR/pbs-key.recovery.enc" 2>/dev/null
|
||||
}
|
||||
|
||||
# Dedicated management submenu for the PBS encryption keyfile.
|
||||
# Available as option 4 of "Configure backup destinations". All
|
||||
# destructive actions offer a backup-to-/root step first and require
|
||||
@@ -770,11 +738,12 @@ _bk_manage_pbs_encryption_keyfile() {
|
||||
--title "$(translate "Manage PBS encryption keyfile")" \
|
||||
--menu "\n${prompt}\n" \
|
||||
"$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
|
||||
"info" "$(hb_translate "Show detailed keyfile info")" \
|
||||
"pass" "$(hb_translate "Update stored keyfile passphrase")" \
|
||||
"replace" "$(hb_translate "Replace keyfile (generate new or import)")" \
|
||||
"remove" "$(hb_translate "Remove keyfile from this host")" \
|
||||
"back" "$(hb_translate "← Return")" \
|
||||
"info" "$(hb_translate "Show detailed keyfile info")" \
|
||||
"download" "$(hb_translate "Copy the keyfile to a path for offsite backup")" \
|
||||
"import" "$(hb_translate "Import a keyfile from an absolute path (replaces the current one)")" \
|
||||
"escrow" "$(hb_translate "Upload to PBS: enable, disable or rotate the recovery passphrase")" \
|
||||
"remove" "$(hb_translate "Remove keyfile from this host")" \
|
||||
"back" "$(hb_translate "← Return")" \
|
||||
3>&1 1>&2 2>&3) || break
|
||||
else
|
||||
choice=$(dialog --backtitle "ProxMenux" --colors \
|
||||
@@ -819,86 +788,108 @@ _bk_manage_pbs_encryption_keyfile() {
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
read -r
|
||||
;;
|
||||
pass)
|
||||
local new_pass new_pass2
|
||||
while true; do
|
||||
new_pass=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Update keyfile passphrase")" \
|
||||
--insecure --passwordbox "$(hb_translate "New passphrase used to unlock the keyfile (leave blank to remove the stored passphrase — for kdf=none keyfiles):")" \
|
||||
"$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || { new_pass=""; break; }
|
||||
if [[ -z "$new_pass" ]]; then
|
||||
new_pass2=""
|
||||
break
|
||||
fi
|
||||
new_pass2=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Update keyfile passphrase")" \
|
||||
--insecure --passwordbox "$(hb_translate "Confirm new passphrase:")" \
|
||||
"$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || { new_pass=""; break; }
|
||||
[[ "$new_pass" == "$new_pass2" ]] && break
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Passphrases do not match. Try again.")" 8 50
|
||||
done
|
||||
if [[ -n "$new_pass" ]]; then
|
||||
umask 077
|
||||
printf '%s' "$new_pass" > "$pass_file"
|
||||
chmod 600 "$pass_file" 2>/dev/null || true
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Stored keyfile passphrase updated.")" 8 60
|
||||
download)
|
||||
# Copy the keyfile to an operator-supplied path so
|
||||
# they can scp/USB it offsite. Same trust boundary as
|
||||
# the Monitor's Download button.
|
||||
local dst
|
||||
dst=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Download keyfile")" \
|
||||
--inputbox "$(hb_translate "Destination path for the copy (typically outside ProxMenux state):")"$'\n\n'"$(hb_translate "Default:") /tmp/pbs-key.conf" \
|
||||
12 78 "/tmp/pbs-key.conf" 3>&1 1>&2 2>&3) || continue
|
||||
dst="$(echo "$dst" | xargs)"
|
||||
[[ -z "$dst" ]] && continue
|
||||
if cp -f "$key_file" "$dst" 2>/dev/null; then
|
||||
chmod 600 "$dst" 2>/dev/null || true
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Keyfile copied")" \
|
||||
--msgbox "$(hb_translate "Keyfile copied to:")"$'\n\n'"$dst"$'\n\n'"$(hb_translate "Move that copy offsite (USB, password manager, another host). Delete it from this path when done.")" \
|
||||
13 78
|
||||
else
|
||||
rm -f "$pass_file" 2>/dev/null
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Stored keyfile passphrase removed.")" 8 60
|
||||
dialog --backtitle "ProxMenux" --title "$(hb_translate "Copy failed")" \
|
||||
--msgbox "$(hb_translate "Could not write to:") $dst" 9 70
|
||||
fi
|
||||
;;
|
||||
replace|remove)
|
||||
local action_title="$(hb_translate "Replace keyfile")"
|
||||
[[ "$choice" == "remove" ]] && action_title="$(hb_translate "Remove keyfile")"
|
||||
# Destructive warning + confirmation
|
||||
import)
|
||||
# Import a new keyfile from an absolute path — replaces
|
||||
# the currently installed one only if the operator
|
||||
# completes the import dialog. Cancel / empty path /
|
||||
# missing file all leave the current keyfile in place.
|
||||
local warn_body
|
||||
warn_body="\Zb\Z1$(hb_translate "This replaces the currently installed keyfile.")\Zn"$'\n\n'
|
||||
warn_body+="$(hb_translate "Backups already on PBS were encrypted with the current key — downloading them will fail unless you first Download the current keyfile to keep a copy.")"$'\n\n'
|
||||
warn_body+="$(hb_translate "Continue with the import?")"
|
||||
if ! dialog --backtitle "ProxMenux" --colors --yesno "$warn_body" 14 78; then
|
||||
continue
|
||||
fi
|
||||
# No upfront wipe. `_hb_pbs_import_dialog` only touches
|
||||
# the canonical path after the operator has typed a
|
||||
# valid absolute path AND finished the whole wizard;
|
||||
# on cancel or missing file it returns non-zero and
|
||||
# the current keyfile stays untouched.
|
||||
_hb_pbs_import_dialog || true
|
||||
;;
|
||||
escrow)
|
||||
# Toggle Upload to PBS Yes/No + set or rotate the
|
||||
# recovery passphrase — same three transitions the
|
||||
# Monitor exposes on the KeyfileActionsBar.
|
||||
local cur_mode="none"
|
||||
[[ -s "$HB_STATE_DIR/pbs-key.mode" ]] && cur_mode="$(cat "$HB_STATE_DIR/pbs-key.mode" 2>/dev/null | tr -d '[:space:]')"
|
||||
local esc_action
|
||||
if [[ "$cur_mode" == "full" ]]; then
|
||||
esc_action=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "PBS upload")" \
|
||||
--menu "$(hb_translate "Upload to PBS is currently: yes. Pick an action:")" \
|
||||
12 78 3 \
|
||||
"rotate" "$(hb_translate "Rotate the recovery passphrase")" \
|
||||
"stop" "$(hb_translate "Stop uploading to PBS")" \
|
||||
"cancel" "$(hb_translate "Cancel")" \
|
||||
3>&1 1>&2 2>&3) || continue
|
||||
else
|
||||
esc_action=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "PBS upload")" \
|
||||
--menu "$(hb_translate "Upload to PBS is currently: no. Pick an action:")" \
|
||||
10 78 2 \
|
||||
"start" "$(hb_translate "Start uploading to PBS — sets a recovery passphrase")" \
|
||||
"cancel" "$(hb_translate "Cancel")" \
|
||||
3>&1 1>&2 2>&3) || continue
|
||||
fi
|
||||
case "$esc_action" in
|
||||
start|rotate)
|
||||
_hb_pbs_prompt_recovery_pass || continue
|
||||
local pass="$HB_PBS_PASS_RESULT"
|
||||
if _hb_pbs_finalize_recovery "$pass" full; then
|
||||
_hb_pbs_write_escrow_mode full
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Upload to PBS enabled. The envelope is uploaded on every encrypted backup.")" 10 70
|
||||
fi
|
||||
;;
|
||||
stop)
|
||||
if dialog --backtitle "ProxMenux" --colors --defaultno --yesno \
|
||||
"\Zb\Z1$(hb_translate "Stop uploading?")\Zn"$'\n\n'"$(hb_translate "The local envelope is dropped and future backups do not upload anything. Uploaded envelopes already on PBS stay intact and remain recoverable with their original passphrase.")" \
|
||||
13 78; then
|
||||
rm -f "$recovery_enc" 2>/dev/null
|
||||
_hb_pbs_write_escrow_mode none
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Upload to PBS disabled.")" 8 60
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
remove)
|
||||
# Destructive removal of the local keyfile. No copies
|
||||
# kept anywhere — the operator is expected to hit
|
||||
# Download first if they want to save the key.
|
||||
local warn_body
|
||||
warn_body="\Zb\Z1$(hb_translate "This is a destructive action")\Zn"$'\n\n'
|
||||
warn_body+="$(hb_translate "Backups already stored on PBS were encrypted with the current keyfile. After this action:")"$'\n'
|
||||
warn_body+=" • $(hb_translate "New backups will use the new (or no) keyfile.")"$'\n'
|
||||
warn_body+=" • $(hb_translate "Downloading pre-existing encrypted backups from this host will fail unless you keep a copy of the current key.")"$'\n'
|
||||
warn_body+=" • $(hb_translate "Existing recovery blobs on PBS stay intact — they still recover the old key with its original passphrase.")"$'\n\n'
|
||||
warn_body+="$(hb_translate "Continue?")"
|
||||
if ! dialog --backtitle "ProxMenux" --colors --yesno "$warn_body" 16 80; then
|
||||
warn_body+=" • $(hb_translate "New backups on this host will be unencrypted until a new keyfile is set up.")"$'\n'
|
||||
warn_body+=" • $(hb_translate "Downloading pre-existing encrypted backups from this host will fail unless you kept a copy of the current key.")"$'\n'
|
||||
warn_body+=" • $(hb_translate "Existing recovery envelopes already on PBS stay intact — they still recover the old key with its original passphrase.")"$'\n\n'
|
||||
warn_body+="$(hb_translate "Use Download first if you want to save a copy of the current key. Continue?")"
|
||||
if ! dialog --backtitle "ProxMenux" --colors --defaultno --yesno "$warn_body" 18 80; then
|
||||
continue
|
||||
fi
|
||||
local do_backup=1
|
||||
if ! dialog --backtitle "ProxMenux" \
|
||||
--title "$(hb_translate "Backup current key")" \
|
||||
--yesno "$(hb_translate "Save a copy of the current keyfile + passphrase + recovery blob under /root/pbs-key.old-<timestamp>.* before ${action_title,,}?")" \
|
||||
10 78; then
|
||||
do_backup=0
|
||||
fi
|
||||
local ts=""
|
||||
if (( do_backup )); then
|
||||
ts=$(_bk_pbs_backup_keyfile_to_root)
|
||||
if [[ -z "$ts" ]]; then
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Backup to /root failed. Aborting.")" 8 60
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
_bk_pbs_wipe_local_keyfile
|
||||
if [[ "$choice" == "replace" ]]; then
|
||||
# Delegate to the yes/no + generate/import flow.
|
||||
# HB_ASK_ENCRYPT_INSTRUCTIONS is unused here, we
|
||||
# directly call the two helpers.
|
||||
local rmenu
|
||||
rmenu=$(dialog --backtitle "ProxMenux" \
|
||||
--title "$(hb_translate "Replace keyfile")" \
|
||||
--menu "$(hb_translate "How should the new keyfile be set up?")" \
|
||||
10 78 2 \
|
||||
"new" "$(hb_translate "Generate a new keyfile (per host — safest isolation)")" \
|
||||
"import" "$(hb_translate "Import an existing keyfile from a path (shared across hosts)")" \
|
||||
3>&1 1>&2 2>&3) || rmenu=""
|
||||
case "$rmenu" in
|
||||
new) _hb_pbs_create_new_keyfile || true ;;
|
||||
import) _hb_pbs_import_dialog || true ;;
|
||||
esac
|
||||
fi
|
||||
local done_msg="$action_title $(hb_translate "completed.")"
|
||||
[[ -n "$ts" ]] && done_msg+=$'\n\n'"$(hb_translate "Backup saved with prefix:") /root/pbs-key.old-${ts}"
|
||||
dialog --backtitle "ProxMenux" --msgbox "$done_msg" 10 78
|
||||
rm -f "$key_file" "$recovery_enc" "$pass_file" \
|
||||
"$HB_STATE_DIR/pbs-key.mode" 2>/dev/null
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Keyfile removed.")" 8 60
|
||||
;;
|
||||
back|"") break ;;
|
||||
esac
|
||||
|
||||
@@ -1485,13 +1485,17 @@ _hb_pbs_create_new_keyfile() {
|
||||
# 4. Finalize per mode. On mode='none' we skip finalize entirely —
|
||||
# no envelope, no msgbox. The operator was already told in the
|
||||
# upload prompt where the keyfile lives; the backup summary
|
||||
# reprints "Encryption: Enabled" when the run starts.
|
||||
# reprints "Encryption: Enabled" when the run starts. Any
|
||||
# envelope leftover from a previous keyfile is dropped so the
|
||||
# runner does not upload a stale, undecipherable blob.
|
||||
if [[ "$mode" != "none" ]]; then
|
||||
if ! _hb_pbs_finalize_recovery "$pass" "$mode"; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
rm -f "$recovery_enc" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_hb_pbs_write_escrow_mode "$mode"
|
||||
@@ -1593,13 +1597,17 @@ _hb_pbs_import_dialog() {
|
||||
|
||||
# 8. Finalize per mode. mode='none' skips finalize (no envelope,
|
||||
# no msgbox) — the operator was already told in the upload
|
||||
# prompt where the keyfile lives.
|
||||
# prompt where the keyfile lives. Any leftover envelope from a
|
||||
# previous keyfile (wrapped with the OLD key) is dropped here so
|
||||
# the runner does not upload a stale, undecipherable blob.
|
||||
if [[ "$mode" != "none" ]]; then
|
||||
if ! _hb_pbs_finalize_recovery "$pass" "$mode"; then
|
||||
rm -f "$key_file" "$recovery_enc" "$keyfile_pass_file" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
rm -f "$recovery_enc" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_hb_pbs_write_escrow_mode "$mode"
|
||||
@@ -1647,13 +1655,17 @@ _hb_pbs_reuse_pve_keyfile() {
|
||||
HB_PBS_ENC_PASS=""
|
||||
|
||||
# 5. Finalize per mode. mode='none' skips finalize (no envelope,
|
||||
# no msgbox).
|
||||
# no msgbox). Drop any envelope left over from a previous
|
||||
# keyfile — it is wrapped with an OLD key the runner cannot
|
||||
# decrypt any more.
|
||||
if [[ "$mode" != "none" ]]; then
|
||||
if ! _hb_pbs_finalize_recovery "$pass" "$mode"; then
|
||||
rm -f "$key_file" "$recovery_enc" 2>/dev/null
|
||||
HB_PBS_KEYFILE_OPT=""
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
rm -f "$recovery_enc" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
_hb_pbs_write_escrow_mode "$mode"
|
||||
|
||||
Reference in New Issue
Block a user