From 945914a50f4ad9df3ca49cde940030188b60ce50 Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Fri, 9 Feb 2024 10:40:26 +0200 Subject: [PATCH] storage: Remember passphrases and export them to Anaconda This also changes everything from localStorage to sessionStorage, for good measure. --- doc/anaconda.md | 32 +++++++++++----------------- pkg/storaged/anaconda.jsx | 28 ++++++++++++++++++------ pkg/storaged/block/format-dialog.jsx | 30 ++++++++++++++++---------- pkg/storaged/client.js | 2 +- pkg/storaged/crypto/actions.jsx | 8 ++++--- pkg/storaged/crypto/keyslots.jsx | 20 +++++++++-------- test/verify/check-storage-anaconda | 12 ++++++++--- 7 files changed, 80 insertions(+), 52 deletions(-) diff --git a/doc/anaconda.md b/doc/anaconda.md index a81306a86db..517907f2c20 100644 --- a/doc/anaconda.md +++ b/doc/anaconda.md @@ -15,22 +15,10 @@ Entering Anaconda mode ---------------------- The "storaged" page is put into Anaconda mode by storing a -"cockpit_anaconda" item in its `window.localStorage`. The value +"cockpit_anaconda" item in its `window.sessionStorage`. The value should be a JSON encoded object, the details of which are explained below. -Since both Anaconda and the storaged page are served from the same -origin, Anaconda can just execute something like this: - -``` - window.localStorage.setItem("cockpit_anaconda", - JSON.stringify({ - "mount_point_prefix": "/sysroot", - "available_devices": [ "/dev/sda" ] - })); - window.open("/cockpit/@localhost/storage/index.html", "storage-tab"); -``` - Ignoring storage devices ------------------------ @@ -71,8 +59,6 @@ configuration into the real /etc/fstab (_not_ /sysroot/etc/fstab). This is done for the convenience of Cockpit, and Anaconda is not expected to read it. -If and how Cockpit communicates back to Anaconda is still open. - BIOS or EFI ----------- @@ -106,9 +92,9 @@ case, Cockpit will use the type from "default_fsys_type". Exported information -------------------- -Cockpit maintains some information in local browser storage that can -be used by Anaconda to learn things that it doesn't get from -blivet. This is mostly information from fstab and crypttab. +Cockpit maintains some information in the session storage that can be +used by Anaconda to learn things that it doesn't get from blivet. This +is mostly information from fstab. The "cockpit_mount_points" entry in local storage will have a JSON encoded object, for example: @@ -160,7 +146,8 @@ types might appear: An encrypted device. It has a "content" field with a value that is structured like a value for "cockpit_mount_points", i.e., a object with a "type" field and maybe a "dir" field if "type" is - "filesystem". This is also present when the crypto device is closed. + "filesystem". This might also be present when the crypto device is + closed. It might also have a "cleartext_device" field if the encrpyted device is currently open. @@ -168,3 +155,10 @@ types might appear: Cockpit does some magic (via the "x-parent" options in fstab and crypttab) to produce information also for locked LUKS devices, and inactive logical volumes. + +Cockpit also remembers and exports encryption passphrases in session +storage, in the "cockpit_passphrases" entry. This is a map from device +names to cleartext passphrases. This is only done when Cockpit runs in +a "secure context", see + + https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts diff --git a/pkg/storaged/anaconda.jsx b/pkg/storaged/anaconda.jsx index 302ee0ccb41..ffc54b1c404 100644 --- a/pkg/storaged/anaconda.jsx +++ b/pkg/storaged/anaconda.jsx @@ -34,15 +34,31 @@ function uuid_equal(a, b) { return a.replace("-", "").toUpperCase() == b.replace("-", "").toUpperCase(); } -export function export_mount_point_mapping() { +function device_name(block) { + // Prefer symlinks in /dev/stratis/. + return (block.Symlinks.map(decode_filename).find(n => n.indexOf("/dev/stratis/") == 0) || + decode_filename(block.PreferredDevice)); +} + +export function remember_passphrase(block, passphrase) { if (!client.in_anaconda_mode()) return; - function device_name(block) { - // Prefer symlinks in /dev/stratis/. - return (block.Symlinks.map(decode_filename).find(n => n.indexOf("/dev/stratis/") == 0) || - decode_filename(block.PreferredDevice)); + if (!window.isSecureContext) + return; + + try { + const passphrases = JSON.parse(window.sessionStorage.getItem("cockpit_passphrases")) || { }; + passphrases[device_name(block)] = passphrase; + window.sessionStorage.setItem("cockpit_passphrases", JSON.stringify(passphrases)); + } catch { + console.warn("Can't record passphrases"); } +} + +export function export_mount_point_mapping() { + if (!client.in_anaconda_mode()) + return; function tab_info(config, for_parent) { let dir; @@ -146,5 +162,5 @@ export function export_mount_point_mapping() { } } - window.localStorage.setItem("cockpit_mount_points", JSON.stringify(mpm)); + window.sessionStorage.setItem("cockpit_mount_points", JSON.stringify(mpm)); } diff --git a/pkg/storaged/block/format-dialog.jsx b/pkg/storaged/block/format-dialog.jsx index bacaf1fc456..64ea8567a4f 100644 --- a/pkg/storaged/block/format-dialog.jsx +++ b/pkg/storaged/block/format-dialog.jsx @@ -44,6 +44,7 @@ import { get_fstab_config, is_valid_mount_point } from "../filesystem/utils.jsx" import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx"; import { job_progress_wrapper } from "../jobs-panel.jsx"; import { at_boot_input, mount_options } from "../filesystem/mounting-dialog.jsx"; +import { remember_passphrase } from "../anaconda.jsx"; const _ = cockpit.gettext; @@ -629,18 +630,25 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, return client.blocks_crypto[path]; } - function maybe_mount(new_path) { + async function maybe_mount(new_path) { const path = new_path || block.path; - if (is_filesystem(vals) && mount_now) - return (client.wait_for(() => block_fsys_for_block(path)) - .then(block_fsys => client.mount_at(client.blocks[block_fsys.path], - mount_point))); - if (type == "swap" && mount_now) - return (client.wait_for(() => block_swap_for_block(path)) - .then(block_swap => block_swap.Start({}))); - if (is_encrypted(vals) && !mount_now) - return (client.wait_for(() => block_crypto_for_block(path)) - .then(block_crypto => block_crypto.Lock({ }))); + const new_block = await client.wait_for(() => client.blocks[path]); + + if (is_encrypted(vals)) + remember_passphrase(new_block, vals.passphrase); + + if (is_filesystem(vals) && mount_now) { + const block_fsys = await client.wait_for(() => block_fsys_for_block(path)); + await client.mount_at(client.blocks[block_fsys.path], mount_point); + } + if (type == "swap" && mount_now) { + const block_swap = await client.wait_for(() => block_swap_for_block(path)); + await block_swap.Start({}); + } + if (is_encrypted(vals) && !mount_now) { + const block_crypto = await client.wait_for(() => block_crypto_for_block(path)); + await block_crypto.Lock({ }); + } } return teardown_active_usage(client, usage) diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 04e47399306..67693161cce 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -977,7 +977,7 @@ function init_model(callback) { } try { - client.anaconda = JSON.parse(window.localStorage.getItem("cockpit_anaconda")); + client.anaconda = JSON.parse(window.sessionStorage.getItem("cockpit_anaconda")); if (client.anaconda) console.log("ANACONDA", client.anaconda); } catch { diff --git a/pkg/storaged/crypto/actions.jsx b/pkg/storaged/crypto/actions.jsx index f149baf160a..d6139c8283f 100644 --- a/pkg/storaged/crypto/actions.jsx +++ b/pkg/storaged/crypto/actions.jsx @@ -23,6 +23,7 @@ import client from "../client"; import { get_existing_passphrase, unlock_with_type } from "./keyslots.jsx"; import { set_crypto_auto_option } from "../utils.js"; import { dialog_open, PassInput } from "../dialog.jsx"; +import { remember_passphrase } from "../anaconda.jsx"; const _ = cockpit.gettext; @@ -43,9 +44,10 @@ export function unlock(block) { ], Action: { Title: _("Unlock"), - action: function (vals) { - return (crypto.Unlock(vals.passphrase, {}) - .then(() => set_crypto_auto_option(block, true))); + action: async function (vals) { + await crypto.Unlock(vals.passphrase, {}); + remember_passphrase(block, vals.passphrase); + await set_crypto_auto_option(block, true); } } }); diff --git a/pkg/storaged/crypto/keyslots.jsx b/pkg/storaged/crypto/keyslots.jsx index 497f07ec27a..9a6962d6fbe 100644 --- a/pkg/storaged/crypto/keyslots.jsx +++ b/pkg/storaged/crypto/keyslots.jsx @@ -32,6 +32,7 @@ import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/comp import { check_missing_packages, install_missing_packages, Enum as PkEnum } from "packagekit"; import { fmt_to_fragments } from "utils.jsx"; +import { remember_passphrase } from "../anaconda.jsx"; import { dialog_open, @@ -78,17 +79,18 @@ function clevis_unlock(block) { { superuser: true }); } -export function unlock_with_type(client, block, passphrase, passphrase_type) { +export async function unlock_with_type(client, block, passphrase, passphrase_type) { const crypto = client.blocks_crypto[block.path]; - if (passphrase) - return crypto.Unlock(passphrase, {}); - else if (passphrase_type == "stored") - return crypto.Unlock("", {}); - else if (passphrase_type == "clevis") - return clevis_unlock(block); - else { + if (passphrase) { + await crypto.Unlock(passphrase, {}); + remember_passphrase(block, passphrase); + } else if (passphrase_type == "stored") { + await crypto.Unlock("", {}); + } else if (passphrase_type == "clevis") { + await clevis_unlock(block); + } else { // This should always be caught and should never show up in the UI - return Promise.reject(new Error("No passphrase")); + throw new Error("No passphrase"); } } diff --git a/test/verify/check-storage-anaconda b/test/verify/check-storage-anaconda index e711b175593..fa78b81523c 100755 --- a/test/verify/check-storage-anaconda +++ b/test/verify/check-storage-anaconda @@ -28,15 +28,20 @@ class TestStorageAnaconda(storagelib.StorageCase): def enterAnacondaMode(self, config): b = self.browser - b.call_js_func("window.localStorage.setItem", "cockpit_anaconda", json.dumps(config)) + b.call_js_func("window.sessionStorage.setItem", "cockpit_anaconda", json.dumps(config)) b.reload() b.enter_page("/storage") def expectExportedDevice(self, device, value): - mpm = json.loads(self.browser.call_js_func("window.localStorage.getItem", "cockpit_mount_points")) + mpm = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_mount_points")) self.assertIn(device, mpm) self.assertEqual(mpm[device], value) + def expectExportedDevicePassphrase(self, device, value): + pp = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_passphrases")) + self.assertIn(device, pp) + self.assertEqual(pp[device], value) + def testBasic(self): m = self.machine b = self.browser @@ -122,7 +127,8 @@ class TestStorageAnaconda(storagelib.StorageCase): self.confirm() testlib.wait(lambda: m.execute("if ! test -e /dev/vgroup0/lvol0; then echo gone; fi").strip() == "gone") - # Check exported mount point information. + # Check exported information. + self.expectExportedDevicePassphrase("/dev/vgroup0/lvol0", "vainu-reku-toma-rolle-kaja") self.expectExportedDevice("/dev/vgroup0/lvol0", { "type": "crypto",