-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
storage: Readonly crypto improvements #20148
Changes from all commits
9a6f8ea
c3439be
88e5a75
f1d0e23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* | ||
* This file is part of Cockpit. | ||
* | ||
* Copyright (C) 2024 Red Hat, Inc. | ||
* | ||
* Cockpit is free software; you can redistribute it and/or modify it | ||
* under the terms of the GNU Lesser General Public License as published by | ||
* the Free Software Foundation; either version 2.1 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* Cockpit is distributed in the hope that it will be useful, but | ||
* WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
* Lesser General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Lesser General Public License | ||
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
import cockpit from "cockpit"; | ||
import client from "../client"; | ||
|
||
import { format_dialog } from "./format-dialog.jsx"; | ||
|
||
const _ = cockpit.gettext; | ||
|
||
export function std_format_action(backing_block, content_block) { | ||
const excuse = backing_block.ReadOnly ? _("Device is read-only") : null; | ||
|
||
return { | ||
title: _("Format"), | ||
action: () => format_dialog(client, backing_block.path), | ||
excuse, | ||
danger: true | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,7 +121,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, | |
const content_block = block.IdUsage == "crypto" ? client.blocks_cleartext[path] : block; | ||
|
||
const offer_keep_keys = block.IdUsage == "crypto"; | ||
const unlock_before_format = offer_keep_keys && !content_block; | ||
const unlock_before_format = offer_keep_keys && (!content_block || content_block.ReadOnly); | ||
|
||
const create_partition = (start !== undefined); | ||
|
||
|
@@ -230,6 +230,8 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, | |
extract_option(crypto_split_options, "noauto"); | ||
extract_option(crypto_split_options, "nofail"); | ||
extract_option(crypto_split_options, "_netdev"); | ||
extract_option(crypto_split_options, "readonly"); | ||
extract_option(crypto_split_options, "read-only"); | ||
const crypto_extra_options = unparse_options(crypto_split_options); | ||
|
||
let [, old_dir, old_opts] = get_fstab_config(block, true, | ||
|
@@ -438,6 +440,8 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, | |
if (is_encrypted(vals)) { | ||
let opts = []; | ||
if (is_filesystem(vals)) { | ||
if (vals.mount_options?.ro) | ||
opts.push("readonly"); | ||
if (!mount_now || vals.at_boot == "never") | ||
opts.push("noauto"); | ||
if (vals.at_boot == "nofail") | ||
|
@@ -516,17 +520,24 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, | |
if (config_items.length > 0) | ||
options["config-items"] = { t: 'a(sa{sv})', v: config_items }; | ||
|
||
function maybe_unlock() { | ||
async function maybe_unlock() { | ||
const content_block = client.blocks_cleartext[path]; | ||
if (content_block) | ||
if (content_block) { | ||
if (content_block.ReadOnly) { | ||
const block_crypto = client.blocks_crypto[path]; | ||
await block_crypto.Lock({}); | ||
await unlock_with_type(client, block, vals.old_passphrase, existing_passphrase_type, false); | ||
} | ||
return content_block; | ||
} | ||
|
||
return (unlock_with_type(client, block, vals.old_passphrase, existing_passphrase_type) | ||
.catch(error => { | ||
dlg.set_values({ needs_explicit_passphrase: true }); | ||
return Promise.reject(error); | ||
}) | ||
.then(() => client.blocks_cleartext[path])); | ||
try { | ||
await unlock_with_type(client, block, vals.old_passphrase, existing_passphrase_type, false); | ||
return client.blocks_cleartext[path]; | ||
} catch (error) { | ||
dlg.set_values({ needs_explicit_passphrase: true }); | ||
throw error; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe throwing here will break, as it is unhandled by the caller. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also caught by the dialog machinery. |
||
} | ||
} | ||
|
||
function format() { | ||
|
@@ -588,6 +599,15 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, | |
if (is_encrypted(vals)) | ||
remember_passphrase(new_block, vals.passphrase); | ||
|
||
if (is_encrypted(vals) && is_filesystem(vals) && vals.mount_options?.ro) { | ||
const block_crypto = await client.wait_for(() => block_crypto_for_block(path)); | ||
await block_crypto.Lock({}); | ||
if (vals.passphrase) | ||
await block_crypto.Unlock(vals.passphrase, { "read-only": { t: "b", v: true } }); | ||
else | ||
await unlock_with_type(client, block, vals.old_passphrase, existing_passphrase_type, true); | ||
} | ||
|
||
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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
|
||
import cockpit from "cockpit"; | ||
import React from "react"; | ||
import client from "../client.js"; | ||
|
||
import { CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; | ||
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; | ||
|
@@ -38,7 +39,10 @@ import { | |
dialog_open, | ||
SelectOneRadio, TextInput, PassInput, Skip | ||
} from "../dialog.jsx"; | ||
import { decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async, get_children, parse_options, unparse_options, edit_crypto_config } from "../utils.js"; | ||
import { | ||
decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async, get_children, | ||
parse_options, extract_option, unparse_options, edit_crypto_config | ||
} from "../utils.js"; | ||
import { StorageButton } from "../storage-controls.jsx"; | ||
|
||
import clevis_luks_passphrase_sh from "./clevis-luks-passphrase.sh"; | ||
|
@@ -72,22 +76,51 @@ export function clevis_recover_passphrase(block, just_type) { | |
.then(output => output.trim()); | ||
} | ||
|
||
function clevis_unlock(block) { | ||
async function clevis_unlock(client, block, luksname, readonly) { | ||
const dev = decode_filename(block.Device); | ||
const clear_dev = "luks-" + block.IdUUID; | ||
return cockpit.spawn(["clevis", "luks", "unlock", "-d", dev, "-n", clear_dev], | ||
{ superuser: true }); | ||
const clear_dev = luksname || "luks-" + block.IdUUID; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This added line is not executed by any test. |
||
|
||
if (readonly) { | ||
// HACK - clevis-luks-unlock can not unlock things readonly. | ||
// But see https://github.com/latchset/clevis/pull/317 (merged | ||
// Feb 2023, unreleased as of Feb 2024). | ||
const passphrase = await clevis_recover_passphrase(block, false); | ||
const crypto = client.blocks_crypto[block.path]; | ||
const unlock_options = { "read-only": { t: "b", v: readonly } }; | ||
await crypto.Unlock(passphrase, unlock_options); | ||
return; | ||
} | ||
|
||
await cockpit.spawn(["clevis", "luks", "unlock", "-d", dev, "-n", clear_dev], | ||
{ superuser: true }); | ||
} | ||
|
||
export async function unlock_with_type(client, block, passphrase, passphrase_type) { | ||
export async function unlock_with_type(client, block, passphrase, passphrase_type, override_readonly) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit of a nit, but why not There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The default if not specified is "don't override". |
||
const crypto = client.blocks_crypto[block.path]; | ||
let readonly = false; | ||
let luksname = null; | ||
|
||
for (const c of block.Configuration) { | ||
if (c[0] == "crypttab") { | ||
const options = parse_options(decode_filename(c[1].options.v)); | ||
readonly = extract_option(options, "readonly") || extract_option(options, "read-only"); | ||
luksname = decode_filename(c[1].name.v); | ||
break; | ||
} | ||
} | ||
|
||
if (override_readonly !== null && override_readonly !== undefined) | ||
readonly = override_readonly; | ||
|
||
const unlock_options = { "read-only": { t: "b", v: readonly } }; | ||
|
||
if (passphrase) { | ||
await crypto.Unlock(passphrase, {}); | ||
await crypto.Unlock(passphrase, unlock_options); | ||
remember_passphrase(block, passphrase); | ||
} else if (passphrase_type == "stored") { | ||
await crypto.Unlock("", {}); | ||
await crypto.Unlock("", unlock_options); | ||
} else if (passphrase_type == "clevis") { | ||
await clevis_unlock(block); | ||
await clevis_unlock(client, block, luksname, readonly); | ||
} else { | ||
// This should always be caught and should never show up in the UI | ||
throw new Error("No passphrase"); | ||
|
@@ -187,7 +220,8 @@ export function init_existing_passphrase(block, just_type, callback) { | |
return { | ||
title: _("Unlocking disk"), | ||
func: dlg => { | ||
return get_existing_passphrase(block, just_type).then(passphrase => { | ||
const backing = client.blocks[block.CryptoBackingDevice]; | ||
return get_existing_passphrase(backing || block, just_type).then(passphrase => { | ||
if (!passphrase) | ||
dlg.set_values({ needs_explicit_passphrase: true }); | ||
if (callback) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 3 added lines are not executed by any test.