Line data Source code
1 : /*
2 : * Copyright (C) 2023 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";
9 :
10 : import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
11 : import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
12 : import { useObject, useEvent } from "hooks";
13 : import * as python from "python.js";
14 : import * as timeformat from "timeformat";
15 :
16 : import { dialog_open, TextInput, PassInput } from "../dialog.jsx";
17 : import { block_name, encode_filename, decode_filename, parse_options, unparse_options, extract_option, edit_crypto_config } from "../utils.js";
18 : import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
19 : import luksmeta_monitor_hack_py from "./luksmeta-monitor-hack.py";
20 : import { is_mounted } from "../filesystem/utils.jsx";
21 : import { StorageLink } from "../storage-controls.jsx";
22 : import { CryptoKeyslots } from "./keyslots.jsx";
23 :
24 113 : const _ = cockpit.gettext;
25 :
26 24 : export function make_encryption_card(next, block) {
27 24 : return new_card({
28 24 : title: _("Encryption"),
29 24 : next,
30 24 : type_extra: _("encrypted"),
31 24 : component: EncryptionCard,
32 24 : props: { block },
33 24 : });
34 24 : }
35 :
36 17 : function monitor_luks(block) {
37 17 : const self = {
38 17 : stop,
39 :
40 17 : luks_version: null,
41 17 : slots: null,
42 17 : slot_error: null,
43 17 : max_slots: null,
44 17 : };
45 :
46 17 : cockpit.event_target(self);
47 :
48 17 : const dev = decode_filename(block.Device);
49 17 : const channel = python.spawn(luksmeta_monitor_hack_py, [dev], { superuser: "require" });
50 17 : let buf = "";
51 :
52 15 : channel.stream(output => {
53 15 : buf += output;
54 15 : const lines = buf.split("\n");
55 15 : buf = lines[lines.length - 1];
56 15 : if (lines.length >= 2) {
57 15 : const data = JSON.parse(lines[lines.length - 2]);
58 15 : self.slots = data.slots;
59 15 : self.luks_version = data.version;
60 15 : self.max_slots = data.max_slots;
61 15 : self.dispatchEvent("changed");
62 15 : }
63 15 : });
64 :
65 0 : channel.catch(err => {
66 0 : self.slots = [];
67 0 : self.slot_error = err;
68 0 : self.dispatchEvent("changed");
69 0 : });
70 :
71 5 : function stop() {
72 5 : channel.close();
73 5 : }
74 :
75 17 : return self;
76 17 : }
77 :
78 0 : function parse_tag_mtime(tag) {
79 0 : if (tag && tag.indexOf("1:") == 0) {
80 0 : try {
81 0 : const parts = tag.split("-")[1].split(".");
82 : // s:ns → ms
83 0 : const mtime = parseInt(parts[0]) * 1000 + parseInt(parts[1]) * 1e-6;
84 0 : return cockpit.format(_("Last modified: $0"), timeformat.dateTime(mtime));
85 0 : } catch {
86 0 : return null;
87 0 : }
88 0 : } else
89 0 : return null;
90 0 : }
91 :
92 17 : function monitor_mtime(path) {
93 17 : const self = {
94 17 : stop,
95 :
96 17 : mtime: 0
97 17 : };
98 :
99 17 : cockpit.event_target(self);
100 :
101 17 : let file = null;
102 2 : if (path) {
103 2 : file = cockpit.file(path, { superuser: "require" });
104 0 : file.watch((_, tag) => { self.mtime = parse_tag_mtime(tag); self.dispatchEvent("changed") },
105 2 : { read: false });
106 2 : }
107 :
108 6 : function stop() {
109 6 : if (file)
110 0 : file.close();
111 6 : }
112 :
113 17 : return self;
114 17 : }
115 :
116 17 : const EncryptionCard = ({ card, block }) => {
117 17 : const luks_info = useObject(() => monitor_luks(block),
118 5 : m => m.stop(),
119 17 : [block]);
120 17 : useEvent(luks_info, "changed");
121 :
122 17 : let old_options;
123 17 : let passphrase_path;
124 16 : const old_config = block.Configuration.find(c => c[0] == "crypttab");
125 16 : if (old_config) {
126 16 : old_options = (decode_filename(old_config[1].options.v)
127 16 : .split(",")
128 16 : .filter(function (s) { return s.indexOf("x-parent") !== 0 })
129 16 : .join(","));
130 16 : passphrase_path = decode_filename(old_config[1]["passphrase-path"].v);
131 16 : }
132 :
133 17 : const stored_passphrase_info = useObject(() => monitor_mtime(passphrase_path),
134 6 : m => m.stop(),
135 17 : [passphrase_path]);
136 17 : useEvent(stored_passphrase_info, "changed");
137 :
138 17 : const split_options = parse_options(old_options);
139 17 : let opt_noauto = extract_option(split_options, "noauto");
140 17 : const extra_options = unparse_options(split_options);
141 :
142 0 : function edit_stored_passphrase() {
143 0 : edit_crypto_config(block, function (config, commit) {
144 0 : dialog_open({
145 0 : Title: _("Stored passphrase"),
146 0 : Fields: [
147 0 : PassInput("passphrase", _("Stored passphrase"),
148 0 : {
149 0 : value: (config && config['passphrase-contents']
150 0 : ? decode_filename(config['passphrase-contents'].v)
151 0 : : "")
152 0 : })
153 0 : ],
154 0 : Action: {
155 0 : Title: _("Save"),
156 0 : action: function (vals) {
157 0 : config["passphrase-contents"] = {
158 0 : t: 'ay',
159 0 : v: encode_filename(vals.passphrase)
160 0 : };
161 0 : delete config["passphrase-path"];
162 0 : return commit();
163 0 : }
164 0 : }
165 0 : });
166 0 : });
167 0 : }
168 :
169 1 : function edit_options() {
170 0 : const fsys_config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab");
171 1 : const content_block = client.blocks_cleartext[block.path];
172 0 : const is_fsys = fsys_config || (content_block && content_block.IdUsage == "filesystem");
173 :
174 1 : edit_crypto_config(block, function (config, commit) {
175 1 : dialog_open({
176 1 : Title: _("Encryption options"),
177 1 : Fields: [
178 1 : TextInput("options", "", { value: extra_options }),
179 1 : ],
180 1 : isFormHorizontal: false,
181 1 : Action: {
182 1 : Title: _("Save"),
183 1 : action: function (vals) {
184 1 : let opts = [];
185 0 : if (is_fsys && content_block)
186 0 : opt_noauto = !is_mounted(client, content_block);
187 1 : if (opt_noauto)
188 1 : opts.push("noauto");
189 1 : opts = opts.concat(parse_options(vals.options));
190 1 : config.options = {
191 1 : t: 'ay',
192 1 : v: encode_filename(unparse_options(opts))
193 1 : };
194 1 : return commit();
195 1 : }
196 1 : }
197 1 : });
198 1 : });
199 1 : }
200 :
201 17 : const cleartext = client.blocks_cleartext[block.path];
202 :
203 17 : const option_parts = [];
204 17 : if (extra_options)
205 13 : option_parts.push(extra_options);
206 17 : const options = option_parts.join(", ");
207 :
208 17 : return (
209 17 : <StorageCard card={card}>
210 17 : <CardBody>
211 17 : <DescriptionList className="pf-m-horizontal-on-sm wide-label">
212 17 : <StorageDescription title={_("Encryption type")}>
213 16 : { luks_info.luks_version ? "LUKS" + luks_info.luks_version : "-" }
214 17 : </StorageDescription>
215 17 : <StorageDescription title={_("Cleartext device")}>
216 10 : {cleartext ? block_name(cleartext) : "-"}
217 17 : </StorageDescription>
218 17 : <StorageDescription title={_("Stored passphrase")}
219 2 : value={ passphrase_path ? stored_passphrase_info.mtime || _("yes") : _("none") }
220 17 : action={<StorageLink onClick={edit_stored_passphrase}>{_("edit")}</StorageLink>} />
221 17 : <StorageDescription title={_("Options")}
222 9 : value={options || _("none")}
223 17 : action={<StorageLink onClick={edit_options}>{_("edit")}</StorageLink>} />
224 17 : </DescriptionList>
225 17 : </CardBody>
226 17 : <CryptoKeyslots client={client} block={block}
227 17 : slots={luks_info.slots} slot_error={luks_info.slot_error}
228 17 : max_slots={luks_info.max_slots} />
229 17 : </StorageCard>);
230 17 : };
|