Skip to content

Commit

Permalink
storage: Remember passphrases and export them to Anaconda
Browse files Browse the repository at this point in the history
This also changes everything from localStorage to sessionStorage, for
good measure.
  • Loading branch information
mvollmer committed Feb 13, 2024
1 parent aca409c commit 945914a
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 52 deletions.
32 changes: 13 additions & 19 deletions doc/anaconda.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------

Expand Down Expand Up @@ -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
-----------

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -160,11 +146,19 @@ 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.

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
28 changes: 22 additions & 6 deletions pkg/storaged/anaconda.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
30 changes: 19 additions & 11 deletions pkg/storaged/block/format-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/storaged/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions pkg/storaged/crypto/actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}
});
Expand Down
20 changes: 11 additions & 9 deletions pkg/storaged/crypto/keyslots.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
}

Expand Down
12 changes: 9 additions & 3 deletions test/verify/check-storage-anaconda
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 945914a

Please sign in to comment.