LCOV - code coverage report
Current view: top level - pkg/storaged/crypto - keyslots.jsx Coverage Total Hit
Test: cockpit Lines: 53.7 % 663 356
Test Date: 2026-06-25 11:17:56

            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 : }
        

Generated by: LCOV version 2.0-1