Skip to content

Commit

Permalink
[Web] Allow to select TPM_FDE encryption if available
Browse files Browse the repository at this point in the history
  • Loading branch information
ancorgs committed Jan 12, 2024
1 parent 2c7053a commit f6b646d
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 17 deletions.
15 changes: 14 additions & 1 deletion web/src/client/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class ProposalManager {
* @typedef {object} ProposalSettings
* @property {string} bootDevice
* @property {string} encryptionPassword
* @property {string} encryptionMethod
* @property {boolean} lvm
* @property {string} spacePolicy
* @property {string[]} systemVGDevices
Expand Down Expand Up @@ -283,6 +284,16 @@ class ProposalManager {
return proxy.ProductMountPoints;
}

/**
* Gets the list of encryption methods accepted by the proposal
*
* @returns {Promise<string[]>}
*/
async getEncryptionMethods() {
const proxy = await this.proxies.proposalCalculator;
return proxy.EncryptionMethods;
}

/**
* Obtains the default volume for the given mount path
*
Expand Down Expand Up @@ -345,6 +356,7 @@ class ProposalManager {
spacePolicy: proxy.SpacePolicy,
systemVGDevices: proxy.SystemVGDevices,
encryptionPassword: proxy.EncryptionPassword,
encryptionMethod: proxy.EncryptionMethod,
volumes: proxy.Volumes.map(this.buildVolume),
// NOTE: strictly speaking, installation devices does not belong to the settings. It
// should be a separate method instead of an attribute in the settings object.
Expand All @@ -365,7 +377,7 @@ class ProposalManager {
* @param {ProposalSettings} settings
* @returns {Promise<number>} 0 on success, 1 on failure
*/
async calculate({ bootDevice, encryptionPassword, lvm, spacePolicy, systemVGDevices, volumes }) {
async calculate({ bootDevice, encryptionPassword, encryptionMethod, lvm, spacePolicy, systemVGDevices, volumes }) {
const dbusVolume = (volume) => {
return removeUndefinedCockpitProperties({
MountPath: { t: "s", v: volume.mountPath },
Expand All @@ -381,6 +393,7 @@ class ProposalManager {
const settings = removeUndefinedCockpitProperties({
BootDevice: { t: "s", v: bootDevice },
EncryptionPassword: { t: "s", v: encryptionPassword },
EncryptionMethod: { t: "s", v: encryptionMethod },
LVM: { t: "b", v: lvm },
SpacePolicy: { t: "s", v: spacePolicy },
SystemVGDevices: { t: "as", v: systemVGDevices },
Expand Down
15 changes: 14 additions & 1 deletion web/src/components/storage/ProposalPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const reducer = (state, action) => {
return { ...state, availableDevices };
}

case "UPDATE_ENCRYPTION_METHODS": {
const { encryptionMethods } = action.payload;
return { ...state, encryptionMethods };
}

case "UPDATE_VOLUME_TEMPLATES": {
const { volumeTemplates } = action.payload;
return { ...state, volumeTemplates };
Expand Down Expand Up @@ -89,6 +94,10 @@ export default function ProposalPage() {
return await cancellablePromise(client.proposal.getAvailableDevices());
}, [client, cancellablePromise]);

const loadEncryptionMethods = useCallback(async () => {
return await cancellablePromise(client.proposal.getEncryptionMethods());
}, [client, cancellablePromise]);

const loadVolumeTemplates = useCallback(async () => {
const mountPoints = await cancellablePromise(client.proposal.getProductMountPoints());
const volumeTemplates = [];
Expand Down Expand Up @@ -127,6 +136,9 @@ export default function ProposalPage() {
const availableDevices = await loadAvailableDevices();
dispatch({ type: "UPDATE_AVAILABLE_DEVICES", payload: { availableDevices } });

const encryptionMethods = await loadEncryptionMethods();
dispatch({ type: "UPDATE_ENCRYPTION_METHODS", payload: { encryptionMethods } });

const volumeTemplates = await loadVolumeTemplates();
dispatch({ type: "UPDATE_VOLUME_TEMPLATES", payload: { volumeTemplates } });

Expand All @@ -137,7 +149,7 @@ export default function ProposalPage() {
dispatch({ type: "UPDATE_ERRORS", payload: { errors } });

if (result !== undefined) dispatch({ type: "STOP_LOADING" });
}, [calculateProposal, cancellablePromise, client, loadAvailableDevices, loadErrors, loadProposalResult, loadVolumeTemplates]);
}, [calculateProposal, cancellablePromise, client, loadAvailableDevices, loadEncryptionMethods, loadErrors, loadProposalResult, loadVolumeTemplates]);

const calculate = useCallback(async (settings) => {
dispatch({ type: "START_LOADING" });
Expand Down Expand Up @@ -200,6 +212,7 @@ export default function ProposalPage() {
<ProposalSettingsSection
availableDevices={state.availableDevices}
volumeTemplates={usefulTemplates()}
encryptionMethods={state.encryptionMethods}
settings={state.settings}
onChange={changeSettings}
isLoading={state.loading}
Expand Down
62 changes: 47 additions & 15 deletions web/src/components/storage/ProposalSettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,23 +385,32 @@ created in a logical volume of the system volume group.");
* @callback onValidateFn
* @param {boolean} valid
*/
const EncryptionPasswordForm = ({
const EncryptionSettingsForm = ({
id,
password: passwordProp,
method: methodProp,
methods,
onSubmit = noop,
onValidate = noop
}) => {
const [password, setPassword] = useState(passwordProp || "");
const [method, setMethod] = useState(methodProp);
const tpmId = "tpm_fde";
const luks2Id = "luks2";

useEffect(() => {
if (password.length === 0) onValidate(false);
}, [password, onValidate]);

const changePassword = (_, v) => setPassword(v);

const changeMethod = (_, value) => {
value ? setMethod(tpmId) : setMethod(luks2Id);
};

const submitForm = (e) => {
e.preventDefault();
onSubmit(password);
onSubmit(password, method);
};

return (
Expand All @@ -412,42 +421,59 @@ const EncryptionPasswordForm = ({
onChange={changePassword}
onValidation={onValidate}
/>
<If
condition={methods.includes(tpmId)}
then={
<Switch
id="encryption_method"
label={_("Use the TPM to decrypt automatically on each boot")}
isReversed
isChecked={method === tpmId}
onChange={changeMethod}
/>
}
/>
</Form>
);
};

/**
* Allows to selected encryption
* Allows to define encryption
* @component
*
* @param {object} props
* @param {string} [props.password=""] - Password for encryption
* @param {string} [props.method=""] - Encryption method
* @param {boolean} [props.isChecked=false] - Whether encryption is selected
* @param {boolean} [props.isLoading=false] - Whether to show the selector as loading
* @param {onChangeFn} [props.onChange=noop] - On change callback
*
* @callback onChangeFn
* @param {object} settings
*/
const EncryptionPasswordField = ({
const EncryptionField = ({
password: passwordProp = "",
method: methodProp = "",
methods,
isChecked: isCheckedProp = false,
isLoading = false,
onChange = noop
}) => {
const [isChecked, setIsChecked] = useState(isCheckedProp);
const [password, setPassword] = useState(passwordProp);
const [method, setMethod] = useState(methodProp);
const [isFormOpen, setIsFormOpen] = useState(false);
const [isFormValid, setIsFormValid] = useState(true);

const openForm = () => setIsFormOpen(true);

const closeForm = () => setIsFormOpen(false);

const acceptForm = (newPassword) => {
const acceptForm = (newPassword, newMethod) => {
closeForm();
setPassword(newPassword);
onChange({ isChecked, password: newPassword });
setMethod(newMethod);
onChange({ isChecked, password: newPassword, method: newMethod });
};

const cancelForm = () => {
Expand All @@ -468,10 +494,10 @@ const EncryptionPasswordField = ({
}
};

const ChangePasswordButton = () => {
const ChangeSettingsButton = () => {
return (
<Tooltip
content={_("Change encryption password")}
content={_("Change encryption settings")}
entryDelay={400}
exitDelay={50}
position="right"
Expand All @@ -495,17 +521,19 @@ const EncryptionPasswordField = ({
isChecked={isChecked}
onChange={changeSelected}
/>
{ isChecked && <ChangePasswordButton /> }
{ isChecked && <ChangeSettingsButton /> }
</div>
<Popup aria-label={_("Encryption settings")} title={_("Encryption settings")} isOpen={isFormOpen}>
<EncryptionPasswordForm
id="encryptionPasswordForm"
<EncryptionSettingsForm
id="encryptionSettingsForm"
password={password}
method={method}
methods={methods}
onSubmit={acceptForm}
onValidate={validateForm}
/>
<Popup.Actions>
<Popup.Confirm form="encryptionPasswordForm" type="submit" isDisabled={!isFormValid}>{_("Accept")}</Popup.Confirm>
<Popup.Confirm form="encryptionSettingsForm" type="submit" isDisabled={!isFormValid}>{_("Accept")}</Popup.Confirm>
<Popup.Cancel onClick={cancelForm} />
</Popup.Actions>
</Popup>
Expand Down Expand Up @@ -618,6 +646,7 @@ const SpacePolicyField = ({
* @param {ProposalSettings} props.settings
* @param {StorageDevice[]} [props.availableDevices=[]]
* @param {Volume[]} [props.volumeTemplates=[]]
* @param {String[]} [props.encryptionMethods=[]]
* @param {boolean} [isLoading=false]
* @param {onChangeFn} [props.onChange=noop]
*
Expand All @@ -628,6 +657,7 @@ export default function ProposalSettingsSection({
settings,
availableDevices = [],
volumeTemplates = [],
encryptionMethods = [],
isLoading = false,
onChange = noop
}) {
Expand All @@ -643,8 +673,8 @@ export default function ProposalSettingsSection({
onChange(settings);
};

const changeEncryption = ({ password }) => {
onChange({ encryptionPassword: password });
const changeEncryption = ({ password, method }) => {
onChange({ encryptionPassword: password, encryptionMethod: method });
};

const changeSpacePolicy = (policy) => {
Expand Down Expand Up @@ -673,8 +703,10 @@ export default function ProposalSettingsSection({
isLoading={settings.lvm === undefined}
onChange={changeLVM}
/>
<EncryptionPasswordField
<EncryptionField
password={settings.encryptionPassword || ""}
method={settings.encryptionMethod}
methods={encryptionMethods}
isChecked={encryption}
isLoading={settings.encryptionPassword === undefined}
onChange={changeEncryption}
Expand Down

0 comments on commit f6b646d

Please sign in to comment.