Line data Source code
1 : /*
2 : * Copyright (C) 2018 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 113 : import cockpit from "cockpit";
7 113 : import React from "react";
8 : import client from "../client.js";
9 :
10 : import { CardHeader } from '@patternfly/react-core/dist/esm/components/Card/index.js';
11 : import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
12 : import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
13 : import { Table, Tbody, Tr, Td } from '@patternfly/react-table';
14 : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
15 : import { TextInput as TextInputPF } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
16 : import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
17 : import { EditIcon, MinusIcon, PlusIcon } from "@patternfly/react-icons";
18 : import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
19 :
20 : import { fmt_to_fragments } from "utils.jsx";
21 : import kernelopt_sh from "kernelopt.sh";
22 :
23 : import { remember_passphrase } from "../anaconda.jsx";
24 :
25 : import {
26 : dialog_open,
27 : SelectOneRadio, TextInput, PassInput, Skip
28 : } from "../dialog.jsx";
29 : import {
30 : decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async,
31 : parse_options, extract_option, unparse_options, edit_crypto_config,
32 : contains_rootfs,
33 : } from "../utils.js";
34 : import { StorageButton } from "../storage-controls.jsx";
35 :
36 : import clevis_luks_passphrase_sh from "./clevis-luks-passphrase.sh";
37 : import { validate_url, get_tang_adv, TangKeyVerification } from "./tang.jsx";
38 : import { getPackageManager, InstallProgressType } from "packagemanager.js";
39 :
40 113 : const _ = cockpit.gettext;
41 :
42 : /* Clevis operations
43 : */
44 :
45 0 : function clevis_add(block, pin, cfg, passphrase) {
46 0 : const dev = decode_filename(block.Device);
47 0 : return cockpit.spawn(["clevis", "luks", "bind", "-f", "-k", "-", "-d", dev, pin, JSON.stringify(cfg)],
48 0 : { superuser: "require", err: "message" }).input(passphrase);
49 0 : }
50 :
51 1 : function clevis_remove(block, key) {
52 : // clevis-luks-unbind needs a tty on stdin for some reason.
53 1 : return cockpit.spawn(["clevis", "luks", "unbind", "-d", decode_filename(block.Device), "-s", key.slot, "-f"],
54 1 : { superuser: "require", pty: true, err: "message" });
55 1 : }
56 :
57 27 : export function clevis_recover_passphrase(block, just_type) {
58 27 : const dev = decode_filename(block.Device);
59 27 : const args = [];
60 27 : if (just_type)
61 25 : args.push("--type");
62 27 : args.push(dev);
63 27 : return cockpit.script(clevis_luks_passphrase_sh, args,
64 27 : { superuser: "require", err: "message" })
65 27 : .then(output => output.trim());
66 27 : }
67 :
68 0 : async function clevis_unlock(client, block, luksname, readonly) {
69 0 : const dev = decode_filename(block.Device);
70 0 : const clear_dev = luksname || "luks-" + block.IdUUID;
71 :
72 0 : if (readonly) {
73 : // HACK - clevis-luks-unlock can not unlock things readonly.
74 : // But see https://github.com/latchset/clevis/pull/317 (merged
75 : // Feb 2023, unreleased as of Feb 2024).
76 0 : const passphrase = await clevis_recover_passphrase(block, false);
77 0 : const crypto = client.blocks_crypto[block.path];
78 0 : const unlock_options = { "read-only": { t: "b", v: readonly } };
79 0 : await crypto.Unlock(passphrase, unlock_options);
80 0 : return;
81 0 : }
82 :
83 0 : await cockpit.spawn(["clevis", "luks", "unlock", "-d", dev, "-n", clear_dev],
84 0 : { superuser: "require" });
85 0 : }
86 :
87 11 : export async function unlock_with_type(client, block, passphrase, passphrase_type, override_readonly) {
88 11 : const crypto = client.blocks_crypto[block.path];
89 11 : let readonly = false;
90 11 : let luksname = null;
91 :
92 10 : for (const c of block.Configuration) {
93 10 : if (c[0] == "crypttab") {
94 10 : const options = parse_options(decode_filename(c[1].options.v));
95 8 : readonly = extract_option(options, "readonly") || extract_option(options, "read-only");
96 10 : luksname = decode_filename(c[1].name.v);
97 10 : break;
98 10 : }
99 10 : }
100 :
101 11 : if (override_readonly !== null && override_readonly !== undefined)
102 8 : readonly = override_readonly;
103 :
104 11 : const unlock_options = { "read-only": { t: "b", v: readonly } };
105 :
106 11 : if (passphrase) {
107 11 : await crypto.Unlock(passphrase, unlock_options);
108 11 : remember_passphrase(block, passphrase);
109 0 : } else if (passphrase_type == "stored") {
110 0 : await crypto.Unlock("", unlock_options);
111 0 : } else if (passphrase_type == "clevis") {
112 0 : await clevis_unlock(client, block, luksname, readonly);
113 0 : } else {
114 : // This should always be caught and should never show up in the UI
115 3 : throw new Error("No passphrase");
116 3 : }
117 11 : }
118 :
119 : /* Passphrase operations
120 : */
121 :
122 1 : function passphrase_add(block, new_passphrase, old_passphrase) {
123 1 : const dev = decode_filename(block.Device);
124 1 : return cockpit.spawn(["cryptsetup", "luksAddKey", dev],
125 1 : { superuser: "require", err: "message" }).input(old_passphrase + "\n" + new_passphrase);
126 1 : }
127 :
128 1 : function passphrase_change(block, key, new_passphrase, old_passphrase) {
129 1 : const dev = decode_filename(block.Device);
130 1 : return cockpit.spawn(["cryptsetup", "luksChangeKey", dev, "--key-slot", key.slot.toString()],
131 1 : { superuser: "require", err: "message" }).input(old_passphrase + "\n" + new_passphrase + "\n");
132 1 : }
133 :
134 1 : function slot_remove(block, slot, passphrase) {
135 1 : const dev = decode_filename(block.Device);
136 1 : const opts = { superuser: "require", err: "message" };
137 1 : const cmd = ["cryptsetup", "luksKillSlot", dev, slot.toString()];
138 1 : if (passphrase === false) {
139 1 : cmd.splice(2, 0, "-q");
140 1 : opts.pty = true;
141 1 : }
142 :
143 1 : const spawn = cockpit.spawn(cmd, opts);
144 1 : if (passphrase !== false)
145 1 : spawn.input(passphrase + "\n");
146 :
147 1 : return spawn;
148 1 : }
149 :
150 0 : function passphrase_test(block, passphrase) {
151 0 : const dev = decode_filename(block.Device);
152 0 : return (cockpit.spawn(["cryptsetup", "luksOpen", "--test-passphrase", dev],
153 0 : { superuser: "require", err: "message" }).input(passphrase)
154 0 : .then(() => true)
155 0 : .catch(() => false));
156 0 : }
157 :
158 : /* Dialogs
159 : */
160 :
161 3 : export function existing_passphrase_fields(explanation) {
162 3 : return [
163 3 : Skip("medskip", { visible: vals => vals.needs_explicit_passphrase }),
164 3 : PassInput("passphrase", _("Disk passphrase"),
165 3 : {
166 3 : visible: vals => vals.needs_explicit_passphrase,
167 0 : validate: val => !val.length && _("Passphrase cannot be empty"),
168 3 : explanation
169 3 : })
170 3 : ];
171 3 : }
172 :
173 27 : function get_stored_passphrase(block, just_type) {
174 24 : const pub_config = block.Configuration.find(c => c[0] == "crypttab");
175 0 : if (pub_config && pub_config[1]["passphrase-path"] && decode_filename(pub_config[1]["passphrase-path"].v) != "") {
176 0 : if (just_type)
177 0 : return Promise.resolve("stored");
178 0 : return block.GetSecretConfiguration({}).then(function (items) {
179 0 : for (let i = 0; i < items.length; i++) {
180 0 : if (items[i][0] == 'crypttab' && items[i][1]['passphrase-contents'])
181 0 : return decode_filename(items[i][1]['passphrase-contents'].v);
182 0 : }
183 0 : return "";
184 0 : });
185 0 : }
186 27 : }
187 :
188 27 : export function get_existing_passphrase(block, just_type) {
189 27 : return clevis_recover_passphrase(block, just_type).then(passphrase => {
190 27 : return passphrase || get_stored_passphrase(block, just_type);
191 27 : });
192 27 : }
193 :
194 11 : export function request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block) {
195 0 : return function (error) {
196 0 : if (vals.passphrase === undefined && block) {
197 0 : return (passphrase_test(block, recovered_passphrase)
198 0 : .then(good => {
199 0 : if (!good)
200 0 : dlg.set_values({ needs_explicit_passphrase: true });
201 0 : return Promise.reject(error);
202 0 : }));
203 0 : } else
204 0 : return Promise.reject(error);
205 0 : };
206 11 : }
207 :
208 25 : export function init_existing_passphrase(block, just_type, callback) {
209 25 : return {
210 25 : title: _("Unlocking disk"),
211 25 : func: dlg => {
212 25 : const backing = client.blocks[block.CryptoBackingDevice];
213 24 : return get_existing_passphrase(backing || block, just_type).then(passphrase => {
214 25 : if (!passphrase)
215 25 : dlg.set_values({ needs_explicit_passphrase: true });
216 25 : if (callback)
217 25 : callback(passphrase);
218 25 : return passphrase;
219 25 : });
220 25 : }
221 25 : };
222 25 : }
223 :
224 : /* Getting the system ready for NBDE on the root filesystem.
225 :
226 : We need the clevis module in the initrd. If it is not there, the
227 : clevis-dracut package should be installed and the initrd needs to
228 : be regenerated. We do this only after the user has agreed to it.
229 :
230 : The kernel command line needs to have rd.neednet=1 in it. We just
231 : do this unconditionally because it's so fast.
232 : */
233 :
234 0 : function ensure_package_installed(steps, progress, package_name) {
235 0 : function status_callback(progress) {
236 0 : return p => {
237 0 : let text = null;
238 0 : if (p.waiting) {
239 0 : text = _("Waiting for other software management operations to finish");
240 0 : } else if (p.package) {
241 0 : let fmt;
242 0 : if (p.info == InstallProgressType.DOWNLOADING)
243 0 : fmt = _("Downloading $0");
244 0 : else if (p.info == InstallProgressType.REMOVING)
245 0 : fmt = _("Removing $0");
246 : else
247 0 : fmt = _("Installing $0");
248 0 : text = cockpit.format(fmt, p.package);
249 0 : }
250 0 : progress(text, p.cancel);
251 0 : };
252 0 : }
253 :
254 0 : progress(cockpit.format(_("Checking for $0 package"), package_name), null);
255 0 : return getPackageManager().then(pk => {
256 0 : return pk.check_missing_packages([package_name]).then(data => {
257 0 : progress(null, null);
258 0 : if (data.missing_names.length + data.unavailable_names.length > 0)
259 0 : steps.push({
260 0 : title: cockpit.format(_("The $0 package must be installed."), package_name),
261 0 : func: progress => {
262 0 : if (data.remove_names.length > 0)
263 0 : return Promise.reject(cockpit.format(_("Installing $0 would remove $1."), name, data.remove_names[0]));
264 0 : else if (data.unavailable_names.length > 0)
265 0 : return Promise.reject(cockpit.format(_("The $0 package is not available from any repository."), name));
266 : else
267 0 : return pk.install_missing_packages(data, status_callback(progress));
268 0 : }
269 0 : });
270 0 : });
271 0 : })
272 0 : .catch(error => {
273 : // Something wrong with PackageKit or dnf5daemon, maybe it is not even
274 : // installed. Let's show the error during fixing.
275 0 : progress(null, null);
276 0 : steps.push({
277 0 : title: cockpit.format(_("The $0 package must be installed."), package_name),
278 0 : func: progress => {
279 0 : if (error.problem == "not-found") {
280 0 : return Promise.reject(cockpit.format(_("Error installing $0: PackageKit or dnf5daemon-server is not installed"), package_name));
281 0 : } else {
282 0 : return Promise.reject(cockpit.format(_("Unexpected PackageManager error during installation of $0: $1"), package_name, error.toString())); // not-covered: OS error
283 0 : }
284 0 : }
285 0 : });
286 0 : });
287 0 : }
288 :
289 0 : function ensure_initrd_clevis_support(steps, progress, package_name) {
290 0 : const task = cockpit.spawn(["lsinitrd", "-m"], { superuser: "require", err: "message" });
291 0 : progress(_("Checking for NBDE support in the initrd"), () => task.close());
292 0 : return task.then(data => {
293 0 : progress(null, null);
294 0 : if (data.indexOf("clevis") < 0) {
295 0 : return ensure_package_installed(steps, progress, package_name)
296 0 : .then(() => {
297 0 : steps.push({
298 0 : title: _("The initrd must be regenerated."),
299 0 : func: progress => {
300 : // dracut doesn't react to SIGINT, so let's not enable our Cancel button
301 0 : progress(_("Regenerating initrd"), null);
302 0 : return cockpit.spawn(["dracut", "--force", "--regenerate-all"],
303 0 : { superuser: "require", err: "message" });
304 0 : }
305 0 : });
306 0 : });
307 0 : }
308 0 : });
309 0 : }
310 :
311 0 : function ensure_root_nbde_support(steps, progress) {
312 0 : progress(_("Adding rd.neednet=1 to kernel command line"), null);
313 0 : return cockpit.script(kernelopt_sh, ["set", "rd.neednet=1"],
314 0 : { superuser: "require", err: "message" })
315 0 : .then(() => ensure_initrd_clevis_support(steps, progress, "clevis-dracut"));
316 0 : }
317 :
318 0 : function ensure_fstab_option(steps, progress, client, block, option) {
319 0 : const cleartext = client.blocks_cleartext[block.path];
320 0 : const crypto = client.blocks_crypto[block.path];
321 0 : const fsys_config = cleartext
322 0 : ? cleartext.Configuration.find(c => c[0] == "fstab")
323 0 : : crypto?.ChildConfiguration.find(c => c[0] == "fstab");
324 0 : const fsys_options = fsys_config && parse_options(get_block_mntopts(fsys_config[1]));
325 :
326 0 : if (!fsys_options || fsys_options.indexOf(option) >= 0)
327 0 : return Promise.resolve();
328 :
329 0 : const new_fsys_options = fsys_options.concat([option]);
330 0 : const new_fsys_config = [
331 0 : "fstab",
332 0 : Object.assign({ }, fsys_config[1],
333 0 : {
334 0 : opts: {
335 0 : t: 'ay',
336 0 : v: encode_filename(unparse_options(new_fsys_options))
337 0 : }
338 0 : })
339 0 : ];
340 0 : progress(cockpit.format(_("Adding \"$0\" to filesystem options"), option), null);
341 0 : return block.UpdateConfigurationItem(fsys_config, new_fsys_config, { });
342 0 : }
343 :
344 0 : function ensure_crypto_option(steps, progress, client, block, option) {
345 0 : const crypto_config = block.Configuration.find(c => c[0] == "crypttab");
346 0 : const crypto_options = crypto_config && parse_options(decode_filename(crypto_config[1].options.v));
347 0 : if (!crypto_options || crypto_options.indexOf(option) >= 0)
348 0 : return Promise.resolve();
349 :
350 0 : const new_crypto_options = crypto_options.concat([option]);
351 0 : progress(cockpit.format(_("Adding \"$0\" to encryption options"), option), null);
352 0 : return edit_crypto_config(block, (config, commit) => {
353 0 : config.options = { t: 'ay', v: encode_filename(unparse_options(new_crypto_options)) };
354 0 : return commit();
355 0 : });
356 0 : }
357 :
358 0 : function ensure_systemd_unit_enabled(steps, progress, name, package_name) {
359 0 : progress(cockpit.format(_("Enabling $0"), name));
360 0 : return cockpit.spawn(["systemctl", "is-enabled", name], { err: "message" })
361 0 : .catch((err, output) => {
362 0 : if (err && (output == "" || output.trim() == "not-found") && package_name) {
363 : // We assume that installing the package will enable the unit.
364 0 : return ensure_package_installed(steps, progress, package_name);
365 0 : } else
366 0 : return cockpit.spawn(["systemctl", "enable", name],
367 0 : { superuser: "require", err: "message" });
368 0 : });
369 0 : }
370 :
371 0 : function ensure_non_root_nbde_support(steps, progress, client, block) {
372 0 : return ensure_systemd_unit_enabled(steps, progress, "remote-cryptsetup.target")
373 0 : .then(() => ensure_systemd_unit_enabled(steps, progress, "clevis-luks-askpass.path", "clevis-systemd"))
374 0 : .then(() => ensure_fstab_option(steps, progress, client, block, "_netdev"))
375 0 : .then(() => ensure_crypto_option(steps, progress, client, block, "_netdev"));
376 0 : }
377 :
378 0 : function ensure_nbde_support(steps, progress, client, block) {
379 0 : if (contains_rootfs(client, block.path)) {
380 0 : if (client.get_config("nbde_root_help", false)) {
381 0 : steps.is_root = true;
382 0 : return ensure_root_nbde_support(steps, progress);
383 0 : } else
384 0 : return Promise.resolve();
385 0 : } else
386 0 : return ensure_non_root_nbde_support(steps, progress, client, block);
387 0 : }
388 :
389 0 : function ensure_nbde_support_dialog(steps, client, block, url, adv, old_key, existing_passphrase) {
390 0 : const dlg = dialog_open({
391 0 : Title: _("Add Network Bound Disk Encryption"),
392 0 : Body: (
393 0 : <>
394 0 : <Content component={ContentVariants.p}>
395 0 : { steps.is_root
396 0 : ? _("The system does not currently support unlocking the root filesystem with a Tang keyserver.")
397 0 : : _("The system does not currently support unlocking a filesystem with a Tang keyserver during boot.")
398 : }
399 0 : </Content>
400 0 : <Content component={ContentVariants.p}>
401 0 : {_("These additional steps are necessary:")}
402 0 : </Content>
403 0 : <Content component="ul">
404 0 : { steps.map((s, i) => <Content component="li" key={i}>{s.title}</Content>) }
405 0 : </Content>
406 0 : </>),
407 0 : Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")),
408 0 : Action: {
409 0 : Title: _("Fix NBDE support"),
410 0 : action: (vals, progress) => {
411 0 : return for_each_async(steps, s => s.func(progress))
412 0 : .then(() => {
413 0 : steps = [];
414 0 : progress(_("Adding key"), null);
415 0 : return add_or_update_tang(dlg, vals, block,
416 0 : url, adv, old_key,
417 0 : vals.passphrase || existing_passphrase);
418 0 : });
419 0 : }
420 0 : }
421 0 : });
422 0 : }
423 :
424 1 : function add_dialog(client, block) {
425 1 : let recovered_passphrase;
426 :
427 1 : dialog_open({
428 1 : Title: _("Add key"),
429 1 : Fields: [
430 1 : SelectOneRadio("type", _("Key source"),
431 1 : {
432 1 : value: "luks-passphrase",
433 1 : visible: vals => client.features.clevis,
434 1 : widest_title: _("Repeat passphrase"),
435 1 : choices: [
436 1 : { value: "luks-passphrase", title: _("Passphrase") },
437 1 : { value: "tang", title: _("Tang keyserver") }
438 1 : ]
439 1 : }),
440 1 : Skip("medskip"),
441 1 : PassInput("new_passphrase", _("New passphrase"),
442 1 : {
443 0 : visible: vals => !client.features.clevis || vals.type == "luks-passphrase",
444 0 : validate: val => !val.length && _("Passphrase cannot be empty"),
445 1 : new_password: true
446 1 : }),
447 1 : PassInput("new_passphrase2", _("Repeat passphrase"),
448 1 : {
449 0 : visible: vals => !client.features.clevis || vals.type == "luks-passphrase",
450 1 : validate: (val, vals) => {
451 1 : return (vals.new_passphrase.length &&
452 1 : vals.new_passphrase != val &&
453 0 : _("Passphrases do not match"));
454 1 : },
455 1 : new_password: true
456 1 : }),
457 1 : TextInput("tang_url", _("Keyserver address"),
458 1 : {
459 0 : visible: vals => client.features.clevis && vals.type == "tang",
460 1 : validate: validate_url
461 1 : })
462 1 : ].concat(existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase."))),
463 1 : Action: {
464 1 : Title: _("Add"),
465 1 : action: function (vals, progress) {
466 0 : const existing_passphrase = vals.passphrase || recovered_passphrase;
467 0 : if (!client.features.clevis || vals.type == "luks-passphrase") {
468 1 : return passphrase_add(block, vals.new_passphrase, existing_passphrase);
469 0 : } else {
470 0 : return get_tang_adv(vals.tang_url)
471 0 : .then(adv => {
472 0 : edit_tang_adv(client, block, null,
473 0 : vals.tang_url, adv, existing_passphrase);
474 0 : });
475 0 : }
476 1 : }
477 1 : },
478 1 : Inits: [
479 1 : init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
480 1 : ]
481 1 : });
482 1 : }
483 :
484 1 : function edit_passphrase_dialog(block, key) {
485 1 : dialog_open({
486 1 : Title: _("Change passphrase"),
487 1 : Fields: [
488 1 : PassInput("old_passphrase", _("Old passphrase"),
489 0 : { validate: val => !val.length && _("Passphrase cannot be empty") }),
490 1 : Skip("medskip"),
491 1 : PassInput("new_passphrase", _("New passphrase"),
492 1 : {
493 0 : validate: val => !val.length && _("Passphrase cannot be empty"),
494 1 : new_password: true
495 1 : }),
496 1 : PassInput("new_passphrase2", _("Repeat passphrase"),
497 1 : {
498 0 : validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match"),
499 1 : new_password: true
500 1 : })
501 1 : ],
502 1 : Action: {
503 1 : Title: _("Save"),
504 1 : action: vals => passphrase_change(block, key, vals.new_passphrase, vals.old_passphrase)
505 1 : }
506 1 : });
507 1 : }
508 :
509 0 : function edit_clevis_dialog(client, block, key) {
510 0 : let recovered_passphrase;
511 :
512 0 : dialog_open({
513 0 : Title: _("Edit Tang keyserver"),
514 0 : Fields: [
515 0 : TextInput("tang_url", _("Keyserver address"),
516 0 : {
517 0 : validate: validate_url,
518 0 : value: key.url
519 0 : })
520 0 : ].concat(existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase."))),
521 0 : Action: {
522 0 : Title: _("Save"),
523 0 : action: function (vals) {
524 0 : const existing_passphrase = vals.passphrase || recovered_passphrase;
525 0 : return get_tang_adv(vals.tang_url).then(adv => {
526 0 : edit_tang_adv(client, block, key, vals.tang_url, adv, existing_passphrase);
527 0 : });
528 0 : }
529 0 : },
530 0 : Inits: [
531 0 : init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
532 0 : ]
533 0 : });
534 0 : }
535 :
536 0 : function add_or_update_tang(dlg, vals, block, url, adv, old_key, passphrase) {
537 0 : return clevis_add(block, "tang", { url, adv }, vals.passphrase || passphrase).then(() => {
538 0 : if (old_key)
539 0 : return clevis_remove(block, old_key);
540 0 : })
541 0 : .catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block));
542 0 : }
543 :
544 0 : function edit_tang_adv(client, block, key, url, adv, passphrase) {
545 0 : const dlg = dialog_open({
546 0 : Title: _("Verify key"),
547 0 : Body: <TangKeyVerification url={url} adv={adv} />,
548 0 : Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")),
549 0 : Action: {
550 0 : Title: _("Trust key"),
551 0 : action: function (vals, progress) {
552 0 : if (key) {
553 0 : return add_or_update_tang(dlg, vals, block,
554 0 : url, adv, key,
555 0 : passphrase);
556 0 : } else {
557 0 : const steps = [];
558 0 : return ensure_nbde_support(steps, progress, client, block)
559 0 : .then(() => {
560 0 : if (steps.length > 0)
561 0 : ensure_nbde_support_dialog(steps, client, block, url,
562 0 : adv, key, passphrase);
563 0 : else {
564 0 : progress(null, null);
565 0 : return add_or_update_tang(dlg, vals, block,
566 0 : url, adv, key,
567 0 : passphrase);
568 0 : }
569 0 : });
570 0 : }
571 0 : }
572 0 : }
573 0 : });
574 0 : }
575 :
576 1 : const RemovePassphraseField = (tag, key, dev) => {
577 1 : function validate(val) {
578 1 : if (val === "")
579 0 : return _("Passphrase can not be empty");
580 1 : }
581 :
582 1 : return {
583 1 : tag,
584 1 : title: null,
585 1 : options: { validate },
586 1 : initial_value: "",
587 1 : bare: true,
588 :
589 1 : render: (val, change, validated, error) => {
590 1 : return (
591 1 : <Stack hasGutter>
592 1 : <p>{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), <b>{dev}</b>) }</p>
593 1 : <Checkbox id="force-remove-passphrase"
594 1 : isChecked={val !== false}
595 1 : label={_("Confirm removal with an alternate passphrase")}
596 0 : onChange={(_event, checked) => change(checked ? "" : false)}
597 1 : body={val === false
598 1 : ? <p className="slot-warning">
599 1 : {_("Removing a passphrase without confirmation of another passphrase may prevent unlocking or key management, if other passphrases are forgotten or lost.")}
600 1 : </p>
601 1 : : <FormGroup label={_("Passphrase from any other key slot")} fieldId="remove-passphrase">
602 1 : <TextInputPF id="remove-passphrase" type="password" value={val} onChange={(_event, value) => change(value)} />
603 1 : </FormGroup>
604 : }
605 1 : />
606 1 : </Stack>
607 : );
608 1 : }
609 1 : };
610 1 : };
611 :
612 1 : function remove_passphrase_dialog(block, key) {
613 1 : dialog_open({
614 1 : Title: cockpit.format(_("Remove passphrase in key slot $0?"), key.slot),
615 1 : Fields: [
616 1 : RemovePassphraseField("passphrase", key, block_name(block))
617 1 : ],
618 1 : isFormHorizontal: false,
619 1 : Action: {
620 1 : DangerButton: true,
621 1 : Title: _("Remove"),
622 1 : action: function (vals) {
623 1 : return slot_remove(block, key.slot, vals.passphrase);
624 1 : }
625 1 : }
626 1 : });
627 1 : }
628 :
629 1 : const RemoveClevisField = (tag, key, dev) => {
630 1 : return {
631 1 : tag,
632 1 : title: null,
633 1 : options: { },
634 1 : initial_value: "",
635 1 : bare: true,
636 :
637 1 : render: (val, change) => {
638 1 : return (
639 1 : <div data-field={tag}>
640 1 : { key.url && <p>{ fmt_to_fragments(_("Remove $0?"), <b>{key.url}</b>) }</p> }
641 1 : <p className="slot-warning">{ fmt_to_fragments(_("Removal may prevent unlocking $0."), <b>{dev}</b>) }</p>
642 1 : </div>
643 : );
644 1 : }
645 1 : };
646 1 : };
647 :
648 1 : function remove_clevis_dialog(client, block, key) {
649 1 : dialog_open({
650 1 : Title: key.url ? _("Remove Tang keyserver?") : cockpit.format(_("Remove key in slot $0?"), key.slot),
651 1 : Fields: [
652 1 : RemoveClevisField("keyserver", key, block_name(block))
653 1 : ],
654 1 : Action: {
655 1 : DangerButton: true,
656 1 : Title: _("Remove"),
657 1 : action: function () {
658 1 : return clevis_remove(block, key);
659 1 : }
660 1 : }
661 1 : });
662 1 : }
663 :
664 113 : export class CryptoKeyslots extends React.Component {
665 17 : render() {
666 17 : const { client, block, slots, slot_error, max_slots } = this.props;
667 :
668 17 : if ((slots == null && slot_error == null) || slot_error == "not-found")
669 17 : return null;
670 :
671 16 : function decode_clevis_slot(slot) {
672 1 : if (slot.ClevisConfig) {
673 1 : const clevis = JSON.parse(slot.ClevisConfig.v);
674 1 : if (clevis.pin && clevis.pin == "tang" && clevis.tang) {
675 1 : return {
676 1 : slot: slot.Index.v,
677 1 : type: "tang",
678 1 : url: clevis.tang.url
679 1 : };
680 1 : } else {
681 1 : return {
682 1 : slot: slot.Index.v,
683 1 : type: "unknown",
684 1 : pin: clevis.pin
685 1 : };
686 1 : }
687 1 : } else {
688 16 : return {
689 16 : slot: slot.Index.v,
690 16 : type: "luks-passphrase"
691 16 : };
692 16 : }
693 16 : }
694 :
695 1 : const keys = slots ? slots.map(decode_clevis_slot).filter(k => !!k) : [];
696 :
697 17 : let table;
698 5 : if (keys.length == 0) {
699 5 : let text;
700 2 : if (slot_error) {
701 2 : if (slot_error.problem == "access-denied")
702 2 : text = _("The currently logged in user is not permitted to see information about keys.");
703 : else
704 2 : text = slot_error.toString();
705 2 : } else {
706 5 : text = _("No keys added");
707 5 : }
708 5 : table = <EmptyState>
709 5 : <EmptyStateBody>
710 5 : {text}
711 5 : </EmptyStateBody>
712 5 : </EmptyState>;
713 5 : } else {
714 17 : const rows = [];
715 :
716 16 : const add_row = (slot, type, desc, edit, edit_excuse, remove) => {
717 16 : rows.push(
718 16 : <Tr key={slot} data-row-slot={slot}>
719 16 : <Td>{type}</Td>
720 16 : <Td>{desc}</Td>
721 16 : <Td>{cockpit.format(_("Slot $0"), slot)}</Td>
722 16 : <Td modifier="nowrap" className="pf-v6-c-table__action">
723 16 : <StorageButton
724 16 : onClick={edit}
725 16 : ariaLabel={_("Edit")}
726 16 : excuse={
727 16 : edit_excuse ||
728 16 : ((keys.length == max_slots)
729 2 : ? _("Editing a key requires a free slot")
730 16 : : null)
731 : }
732 : >
733 16 : <EditIcon />
734 16 : </StorageButton>
735 16 : { "\n" }
736 16 : <StorageButton onClick={remove}
737 16 : ariaLabel={_("Remove")}
738 2 : excuse={keys.length == 1 ? _("The last key slot can not be removed") : null}>
739 16 : <MinusIcon />
740 16 : </StorageButton>
741 16 : </Td>
742 16 : </Tr>
743 16 : );
744 16 : };
745 :
746 2 : keys.sort((a, b) => a.slot - b.slot).forEach(key => {
747 16 : if (key.type == "luks-passphrase") {
748 16 : add_row(key.slot,
749 16 : _("Passphrase"), "",
750 1 : () => edit_passphrase_dialog(block, key), null,
751 1 : () => remove_passphrase_dialog(block, key));
752 1 : } else if (key.type == "tang") {
753 1 : add_row(key.slot,
754 1 : _("Keyserver"), key.url,
755 0 : () => edit_clevis_dialog(client, block, key), null,
756 1 : () => remove_clevis_dialog(client, block, key));
757 1 : } else {
758 1 : add_row(key.slot,
759 1 : _("Unknown type"), key.pin,
760 1 : null, _("Key slots with unknown types can not be edited here"),
761 0 : () => remove_clevis_dialog(client, block, key));
762 1 : }
763 16 : });
764 :
765 17 : table = (
766 17 : <Table id="encryption-keys" aria-label={_("Keys")}>
767 17 : <Tbody>
768 17 : {rows}
769 17 : </Tbody>
770 17 : </Table>
771 : );
772 17 : }
773 :
774 17 : const remaining = max_slots - keys.length;
775 :
776 17 : return (
777 17 : <>
778 17 : <CardHeader actions={{
779 17 : actions: <>
780 17 : <span className="key-slot-panel-remaining">
781 3 : { remaining < 6 ? (remaining ? cockpit.format(cockpit.ngettext("$0 slot remains", "$0 slots remain", remaining), remaining) : _("No available slots")) : null }
782 17 : </span>
783 1 : <StorageButton onClick={() => add_dialog(client, block)}
784 17 : ariaLabel={_("Add")}
785 17 : excuse={(keys.length == max_slots)
786 3 : ? _("No free key slots")
787 17 : : null}>
788 17 : <PlusIcon />
789 17 : </StorageButton>
790 17 : </>,
791 17 : }}>
792 17 : <strong>{_("Keys")}</strong>
793 17 : </CardHeader>
794 17 : {table}
795 17 : </>
796 : );
797 17 : }
798 113 : }
|