Skip to content

Commit

Permalink
LVM + FDE
Browse files Browse the repository at this point in the history
Allow selecting encryption in the "Advanced features" dialog, and show
the "Choose a security key" page when appropriate.

Ref: canonical#33, canonical#34
  • Loading branch information
jpnurmi committed Jun 27, 2022
1 parent 2637de3 commit 43f1756
Show file tree
Hide file tree
Showing 20 changed files with 373 additions and 85 deletions.
4 changes: 2 additions & 2 deletions packages/ubuntu_desktop_installer/lib/installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ class _UbuntuDesktopInstallerWizard extends StatelessWidget {
if (settings.arguments == InstallationType.erase) {
if (service.hasMultipleDisks) {
return Routes.selectGuidedStorage;
} else if (service.hasEncryption) {
} else if (service.useEncryption) {
return Routes.chooseSecurityKey;
} else {
return Routes.writeChangesToDisk;
Expand All @@ -300,7 +300,7 @@ class _UbuntuDesktopInstallerWizard extends StatelessWidget {
Routes.selectGuidedStorage: WizardRoute(
builder: SelectGuidedStoragePage.create,
onNext: (_) =>
!service.hasEncryption ? Routes.writeChangesToDisk : null,
!service.useEncryption ? Routes.writeChangesToDisk : null,
),
Routes.chooseSecurityKey: WizardRoute(
builder: ChooseSecurityKeyPage.create,
Expand Down
4 changes: 3 additions & 1 deletion packages/ubuntu_desktop_installer/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"dontInstallDriverSoftwareNowDescription": "You can install it later from Software & Updates.",
"configureSecureBootSecurityKeyRequired": "Security key is required",
"secureBootSecurityKeysDontMatch": "Security keys do not match",
"showSecurityKey": "Show security key",
"connectToInternetPageTitle": "Connect to internet",
"connectToInternetDescription": "Connecting this computer to the internet will help Ubuntu install any extra software needed and help choose your time zone.\n\nConnect by ethernet cable, or choose a Wi-Fi network",
"useWiredConnection": "Use wired connection",
Expand Down Expand Up @@ -175,6 +176,7 @@
}
},
"installationTypeLVMSelected": "LVM selected",
"installationTypeLVMEncryptionSelected": "LVM and encryption selected",
"installationTypeEncrypt": "Encrypt the new {RELEASE} installation for security",
"@installationTypeEncrypt": {
"type": "text",
Expand Down Expand Up @@ -445,4 +447,4 @@
"supportedSoftware": "Supported software",
"copyingFiles": "Copying files...",
"installationFailed": "Installation failed"
}
}
12 changes: 12 additions & 0 deletions packages/ubuntu_desktop_installer/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,12 @@ abstract class AppLocalizations {
/// **'Security keys do not match'**
String get secureBootSecurityKeysDontMatch;

/// No description provided for @showSecurityKey.
///
/// In en, this message translates to:
/// **'Show security key'**
String get showSecurityKey;

/// No description provided for @connectToInternetPageTitle.
///
/// In en, this message translates to:
Expand Down Expand Up @@ -741,6 +747,12 @@ abstract class AppLocalizations {
/// **'LVM selected'**
String get installationTypeLVMSelected;

/// No description provided for @installationTypeLVMEncryptionSelected.
///
/// In en, this message translates to:
/// **'LVM and encryption selected'**
String get installationTypeLVMEncryptionSelected;

/// No description provided for @installationTypeEncrypt.
///
/// In en, this message translates to:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:safe_change_notifier/safe_change_notifier.dart';
import 'package:subiquity_client/subiquity_client.dart';

import '../../services.dart';

/// View model for [ChooseSecurityKeyPage].
class ChooseSecurityKeyModel extends SafeChangeNotifier {
/// Creates the model with the given client.
ChooseSecurityKeyModel(this._client) {
ChooseSecurityKeyModel(this._service) {
Listenable.merge([
_securityKey,
_confirmedSecurityKey,
_showSecurityKey,
]).addListener(notifyListeners);
}

// ignore: unused_field, will be used for sending the security key to subiquity
final SubiquityClient _client;
final DiskStorageService _service;

final _securityKey = ValueNotifier('');
final _confirmedSecurityKey = ValueNotifier('');
final _showSecurityKey = ValueNotifier(false);

/// The current security key.
String get securityKey => _securityKey.value;
Expand All @@ -25,17 +28,19 @@ class ChooseSecurityKeyModel extends SafeChangeNotifier {
String get confirmedSecurityKey => _confirmedSecurityKey.value;
set confirmedSecurityKey(String value) => _confirmedSecurityKey.value = value;

/// Defines if the security is shown.
bool get showSecurityKey => _showSecurityKey.value;
set showSecurityKey(bool value) => _showSecurityKey.value = value;

/// Whether the current input is valid.
bool get isValid =>
securityKey.isNotEmpty && securityKey == confirmedSecurityKey;

/// Loads the security key.
Future<void> loadSecurityKey() async {
// TODO: fetch from subiquity
}

/// Saves the security key.
Future<void> saveSecurityKey() async {
// TODO: send to subiquity
_service.setSecurityKey(securityKey);
if (!_service.hasMultipleDisks) {
return _service.setGuidedStorage();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:form_field_validator/form_field_validator.dart';
import 'package:provider/provider.dart';
import 'package:subiquity_client/subiquity_client.dart';
import 'package:ubuntu_widgets/ubuntu_widgets.dart';
import 'package:ubuntu_wizard/constants.dart';
import 'package:ubuntu_wizard/utils.dart';
import 'package:ubuntu_wizard/widgets.dart';
Expand All @@ -17,7 +17,7 @@ part 'choose_security_key_widgets.dart';
///
/// See also:
/// * [ChooseSecurityKeyModel]
class ChooseSecurityKeyPage extends StatefulWidget {
class ChooseSecurityKeyPage extends StatelessWidget {
/// Use [create] instead.
@visibleForTesting
const ChooseSecurityKeyPage({
Expand All @@ -26,26 +26,13 @@ class ChooseSecurityKeyPage extends StatefulWidget {

/// Creates an instance with [ChooseSecurityKeyModel].
static Widget create(BuildContext context) {
final client = getService<SubiquityClient>();
final service = getService<DiskStorageService>();
return ChangeNotifierProvider(
create: (_) => ChooseSecurityKeyModel(client),
create: (_) => ChooseSecurityKeyModel(service),
child: const ChooseSecurityKeyPage(),
);
}

@override
State<ChooseSecurityKeyPage> createState() => _ChooseSecurityKeyPageState();
}

class _ChooseSecurityKeyPageState extends State<ChooseSecurityKeyPage> {
@override
void initState() {
super.initState();

final model = Provider.of<ChooseSecurityKeyModel>(context, listen: false);
model.loadSecurityKey();
}

@override
Widget build(BuildContext context) {
final lang = AppLocalizations.of(context);
Expand All @@ -63,6 +50,8 @@ class _ChooseSecurityKeyPageState extends State<ChooseSecurityKeyPage> {
_SecurityKeyFormField(fieldWidth: fieldWidth),
const SizedBox(height: kContentSpacing),
_ConfirmSecurityKeyFormField(fieldWidth: fieldWidth),
const SizedBox(height: kContentSpacing / 2),
const _SecurityKeyShowButton(),
const SizedBox(height: kContentSpacing),
Align(
alignment: Alignment.centerLeft,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ class _SecurityKeyFormField extends StatelessWidget {
final lang = AppLocalizations.of(context);
final securityKey = context
.select<ChooseSecurityKeyModel, String>((model) => model.securityKey);
final showSecurityKey = context
.select<ChooseSecurityKeyModel, bool>((model) => model.showSecurityKey);

return ValidatedFormField(
fieldWidth: fieldWidth,
labelText: lang.chooseSecurityKeyHint,
obscureText: true,
obscureText: !showSecurityKey,
successWidget: securityKey.isNotEmpty ? const SuccessIcon() : null,
initialValue: securityKey,
validator: RequiredValidator(
Expand Down Expand Up @@ -47,11 +49,13 @@ class _ConfirmSecurityKeyFormField extends StatelessWidget {
.select<ChooseSecurityKeyModel, String>((model) => model.securityKey);
final confirmedSecurityKey = context.select<ChooseSecurityKeyModel, String>(
(model) => model.confirmedSecurityKey);
final showSecurityKey = context
.select<ChooseSecurityKeyModel, bool>((model) => model.showSecurityKey);

return ValidatedFormField(
fieldWidth: fieldWidth,
labelText: lang.chooseSecurityKeyConfirmHint,
obscureText: true,
obscureText: !showSecurityKey,
successWidget:
confirmedSecurityKey.isNotEmpty ? const SuccessIcon() : null,
initialValue: confirmedSecurityKey,
Expand All @@ -68,3 +72,22 @@ class _ConfirmSecurityKeyFormField extends StatelessWidget {
);
}
}

class _SecurityKeyShowButton extends StatelessWidget {
const _SecurityKeyShowButton({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final lang = AppLocalizations.of(context);
final showSecurityKey = context
.select<ChooseSecurityKeyModel, bool>((model) => model.showSecurityKey);

return CheckButton(
value: showSecurityKey,
title: Text(lang.showSecurityKey),
onChanged: (value) {
context.read<ChooseSecurityKeyModel>().showSecurityKey = value!;
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ Future<void> showAdvancedFeaturesDialog(

return AlertDialog(
title: Text(lang.installationTypeAdvancedTitle),
titlePadding: kHeaderPadding,
contentPadding: kContentPadding.copyWith(
top: kContentSpacing, bottom: kContentSpacing),
actionsPadding: kFooterPadding,
buttonPadding: EdgeInsets.zero,
scrollable: true,
content: AnimatedBuilder(
animation: Listenable.merge([advancedFeature, encryption]),
Expand All @@ -43,19 +38,15 @@ Future<void> showAdvancedFeaturesDialog(
value: AdvancedFeature.lvm,
groupValue: advancedFeature.value,
onChanged: (v) => advancedFeature.value = v!,
subtitle: CheckButton(
title: Text(lang.installationTypeEncrypt(flavor.name)),
subtitle: Text(lang.installationTypeEncryptInfo),
value: encryption.value,
onChanged: advancedFeature.value == AdvancedFeature.lvm
? (v) => encryption.value = v!
: null,
),
),
// https://github.com/canonical/ubuntu-desktop-installer/issues/373
// RadioIconTile(
// contentPadding: EdgeInsets.zero,
// title: CheckButton(
// title: Text(lang.installationTypeEncrypt('Ubuntu')),
// subtitle: Text(lang.installationTypeEncrypt(flavor.name)),
// value: encryption.value,
// onChanged: model.advancedFeature == AdvancedFeature.lvm
// ? (v) => encryption.value = v!
// : null,
// ),
// ),
const SizedBox(height: kContentSpacing),
// https://github.com/canonical/ubuntu-desktop-installer/issues/373
// RadioButton<AdvancedFeature>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ class InstallationTypeModel extends SafeChangeNotifier {
/// if appropriate (single guided storage).
Future<void> save() async {
_diskService.useLvm = advancedFeature == AdvancedFeature.lvm;
_diskService.useEncryption =
encryption && advancedFeature == AdvancedFeature.lvm;

if (!_diskService.hasMultipleDisks &&
_installationType == InstallationType.erase) {
await _diskService.setGuidedStorage();
Expand All @@ -117,7 +120,7 @@ class InstallationTypeModel extends SafeChangeNotifier {
} else if (advancedFeature == AdvancedFeature.zfs) {
_telemetryService.setPartitionMethod('use_zfs');
}
if (_diskService.hasEncryption && advancedFeature != AdvancedFeature.zfs) {
if (_diskService.useEncryption) {
_telemetryService.setPartitionMethod('use_crypto');
}
// TODO: map upgrading the current Ubuntu installation without
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,7 @@ class _InstallationTypePageState extends State<InstallationTypePage> {
child: Text(lang.installationTypeAdvancedLabel),
),
const SizedBox(width: kContentSpacing),
Text(model.advancedFeature == AdvancedFeature.lvm
? lang.installationTypeLVMSelected
: model.advancedFeature == AdvancedFeature.zfs
? lang.installationTypeZFSSelected
: lang.installationTypeNoneSelected),
Text(model.advancedFeature.localize(lang, model.encryption)),
],
),
),
Expand All @@ -145,3 +141,18 @@ class _InstallationTypePageState extends State<InstallationTypePage> {
);
}
}

extension _AdvancedFeatureL10n on AdvancedFeature {
String localize(AppLocalizations lang, bool encryption) {
switch (this) {
case AdvancedFeature.none:
return lang.installationTypeNoneSelected;
case AdvancedFeature.lvm:
return encryption
? lang.installationTypeLVMEncryptionSelected
: lang.installationTypeLVMSelected;
case AdvancedFeature.zfs:
return lang.installationTypeZFSSelected;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ class DiskStorageService {
bool? _needRoot;
bool? _needBoot;
bool? _useLvm;
bool? _useEncryption;
bool? _hasRst;
bool? _hasBitLocker;
String? _securityKey;

int get _diskCount => guidedStorage?.length ?? 0;

/// Whether the system has multiple disks available for guided partitioning.
Expand All @@ -52,8 +55,9 @@ class DiskStorageService {

bool get hasBitLocker => _hasBitLocker ?? false;

/// Whether FDE (Full Disk Encryption) is enabled.
bool get hasEncryption => false; // TODO: add support for it
/// Whether FDE (Full Disk Encryption) should be used.
bool get useEncryption => _useEncryption ?? false;
set useEncryption(bool useEncryption) => _useEncryption = useEncryption;

/// Whether Secure Boot is enabled.
bool get hasSecureBoot => false; // TODO: add support for it
Expand All @@ -62,6 +66,10 @@ class DiskStorageService {
bool get useLvm => _useLvm ?? false;
set useLvm(bool useLvm) => _useLvm = useLvm;

/// A security key for full disk encryption.
String? get securityKey => _securityKey;
void setSecurityKey(String? securityKey) => _securityKey = securityKey;

List<Disk> _updateGuidedStorage(GuidedStorageResponse response) {
log.debug('Update guided storage: $response');
guidedStorage = response.disks;
Expand All @@ -81,6 +89,7 @@ class DiskStorageService {
final choice = GuidedChoice(
diskId: disk?.id ?? guidedStorage![0].id,
useLvm: useLvm,
password: useEncryption ? _securityKey : null,
);
return _client.setGuidedStorageV2(choice).then(_updateStorage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ class MockDiskStorageService extends _i1.Mock
(super.noSuchMethod(Invocation.getter(#hasBitLocker), returnValue: false)
as bool);
@override
bool get hasEncryption =>
(super.noSuchMethod(Invocation.getter(#hasEncryption), returnValue: false)
bool get useEncryption =>
(super.noSuchMethod(Invocation.getter(#useEncryption), returnValue: false)
as bool);
@override
set useEncryption(bool? useEncryption) =>
super.noSuchMethod(Invocation.setter(#useEncryption, useEncryption),
returnValueForMissingStub: null);
@override
bool get hasSecureBoot =>
(super.noSuchMethod(Invocation.getter(#hasSecureBoot), returnValue: false)
as bool);
Expand All @@ -77,6 +81,10 @@ class MockDiskStorageService extends _i1.Mock
returnValue: Future<void>.value(),
returnValueForMissingStub: Future<void>.value()) as _i4.Future<void>);
@override
void setSecurityKey(String? securityKey) =>
super.noSuchMethod(Invocation.method(#setSecurityKey, [securityKey]),
returnValueForMissingStub: null);
@override
_i4.Future<List<_i3.Disk>> getGuidedStorage() =>
(super.noSuchMethod(Invocation.method(#getGuidedStorage, []),
returnValue: Future<List<_i3.Disk>>.value(<_i3.Disk>[]))
Expand Down

0 comments on commit 43f1756

Please sign in to comment.