Skip to content

Commit

Permalink
Encrypted guided LVM
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 Sep 21, 2022
1 parent b8fcc5b commit 215f465
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 62 deletions.
5 changes: 4 additions & 1 deletion packages/subiquity_client/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,11 @@ class SubiquityClient {
request.write(jsonEncode(choice.toJson()));
final response = await request.close();

final hiddenPassword =
choice.password == null ? null : '*' * choice.password!.length;
final responseJson = await _receiveJson(
'setGuidedStorageV2(${jsonEncode(choice.toJson())})', response);
'setGuidedStorageV2(${jsonEncode(choice.copyWith(password: hiddenPassword).toJson())})',
response);
return GuidedStorageResponseV2.fromJson(responseJson);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:ubuntu_desktop_installer/l10n.dart';
import 'package:ubuntu_desktop_installer/main.dart' as app;
import 'package:ubuntu_desktop_installer/pages.dart';
import 'package:ubuntu_desktop_installer/pages/connect_to_internet/connect_model.dart';
import 'package:ubuntu_desktop_installer/pages/installation_type/installation_type_model.dart';
import 'package:ubuntu_desktop_installer/pages/updates_other_software/updates_other_software_model.dart';
import 'package:ubuntu_desktop_installer/routes.dart';
import 'package:ubuntu_desktop_installer/services.dart';
Expand Down Expand Up @@ -93,6 +94,68 @@ void main() {
);
});

testWidgets('guided lvm + encryption', (tester) async {
const identity = IdentityData(
realname: 'User',
hostname: 'ubuntu',
username: 'user',
);

await app.main(<String>[]);
await tester.pumpAndSettle();

await testWelcomePage(tester);
await tester.pumpAndSettle();

await testTryOrInstallPage(tester, option: Option.installUbuntu);
await tester.pumpAndSettle();

await testKeyboardLayoutPage(tester);
await tester.pumpAndSettle();

await testConnectToInternetPage(tester, mode: ConnectMode.none);
await tester.pumpAndSettle();

await testUpdatesOtherSoftwarePage(tester);
await tester.pumpAndSettle();

await testInstallationTypePage(
tester,
type: InstallationType.erase,
advancedFeature: AdvancedFeature.lvm,
useEncryption: true,
);
await tester.pumpAndSettle();

await testChooseSecurityKeyPage(tester, securityKey: 'password');
await tester.pumpAndSettle();

await testWriteChangesToDiskPage(tester);
await tester.pumpAndSettle();

await testWhereAreYouPage(tester);
await tester.pump();

await testWhoAreYouPage(
tester,
identity: identity,
password: 'password',
);
await tester.pump();

await testInstallationSlidesPage(tester);
await tester.pumpAndSettle();

await testInstallationCompletePage(tester);
await tester.pumpAndSettle();

await verifyConfig(
identity: identity,
useLvm: true,
useEncryption: true,
);
});

testWidgets('manual partitioning', (tester) async {
final storage = [
testDisk(
Expand Down Expand Up @@ -338,13 +401,53 @@ Future<void> testUpdatesOtherSoftwarePage(
Future<void> testInstallationTypePage(
WidgetTester tester, {
InstallationType? type,
AdvancedFeature? advancedFeature,
bool? useEncryption,
}) async {
await expectPage(
tester, InstallationTypePage, (lang) => lang.installationTypeTitle);

if (type != null) {
await tester.tapRadioButton<InstallationType>(type);
}
if (advancedFeature != null) {
await tester.tapButton(label: tester.lang.installationTypeAdvancedLabel);
await tester.pumpAndSettle();

await tester.tapRadioButton<AdvancedFeature>(advancedFeature);
await tester.pump();

if (useEncryption != null) {
await tester.toggleCheckbox(
label: tester.lang.installationTypeEncrypt('Ubuntu'),
value: true,
);
}

await tester.tapButton(label: tester.lang.okButtonText);
}

await tester.pumpAndSettle();

await tester.tapContinue();
}

Future<void> testChooseSecurityKeyPage(
WidgetTester tester, {
required String securityKey,
}) async {
await expectPage(
tester, ChooseSecurityKeyPage, (lang) => lang.chooseSecurityKeyTitle);

await tester.enterTextValue(
label: tester.lang.chooseSecurityKey,
value: securityKey,
);
await tester.enterTextValue(
label: tester.lang.confirmSecurityKey,
value: securityKey,
);

await tester.pumpAndSettle();

await tester.tapContinue();
Expand Down Expand Up @@ -556,6 +659,8 @@ Future<void> verifyConfig({
String? locale,
String? timezone,
List<Disk>? storage,
bool? useLvm,
bool? useEncryption,
}) async {
final path = p.join(
await subiquityPath,
Expand Down Expand Up @@ -596,9 +701,9 @@ Future<void> verifyConfig({
expect(actualTimezone, equals(timezone));
}

if (storage != null) {
final actualStorage = yaml['autoinstall']['storage']['config'] as YamlList;
final actualStorage = yaml['autoinstall']['storage']['config'] as YamlList;

if (storage != null) {
for (final disk in storage) {
final actualDisk = actualStorage.firstWhereOrNull(
(d) => d['type'] == 'disk' && d['path'] == disk.path);
Expand All @@ -617,4 +722,20 @@ Future<void> verifyConfig({
}
}
}

if (useLvm != null) {
expect(
actualStorage
.where((config) => config['type'] == 'lvm_volgroup')
.isNotEmpty,
useLvm);
}

if (useEncryption != null) {
expect(
actualStorage
.where((config) => config['type'] == 'dm_crypt')
.isNotEmpty,
useEncryption);
}
}
42 changes: 21 additions & 21 deletions packages/ubuntu_desktop_installer/lib/installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -282,37 +282,20 @@ class _UbuntuDesktopInstallerWizard extends StatelessWidget {
),
Routes.installationType: WizardRoute(
builder: InstallationTypePage.create,
onNext: (settings) {
if (settings.arguments == InstallationType.manual) {
return Routes.allocateDiskSpace;
} else if (service.guidedTarget == null) {
if (settings.arguments == InstallationType.erase) {
return Routes.selectGuidedStorage;
} else if (settings.arguments == InstallationType.alongside) {
return Routes.installAlongside;
}
} else if (service.hasEncryption) {
return Routes.chooseSecurityKey;
}
return Routes.writeChangesToDisk;
},
onNext: (settings) => _nextStorageRoute(service, settings.arguments),
),
Routes.installAlongside: WizardRoute(
builder: InstallAlongsidePage.create,
onReplace: (_) => Routes.allocateDiskSpace,
onNext: (_) => service.hasEncryption
? Routes.chooseSecurityKey
: Routes.writeChangesToDisk,
onNext: (settings) => _nextStorageRoute(service, settings.arguments),
),
Routes.selectGuidedStorage: WizardRoute(
builder: SelectGuidedStoragePage.create,
onNext: (_) => service.hasEncryption
? Routes.chooseSecurityKey
: Routes.writeChangesToDisk,
onNext: (settings) => _nextStorageRoute(service, settings.arguments),
),
Routes.chooseSecurityKey: WizardRoute(
builder: ChooseSecurityKeyPage.create,
onNext: (_) => Routes.writeChangesToDisk,
onNext: (settings) => _nextStorageRoute(service, settings.arguments),
),
Routes.allocateDiskSpace: const WizardRoute(
builder: AllocateDiskSpacePage.create,
Expand Down Expand Up @@ -349,6 +332,23 @@ class _UbuntuDesktopInstallerWizard extends StatelessWidget {
],
);
}

String? _nextStorageRoute(DiskStorageService service, dynamic arguments) {
if (arguments == InstallationType.manual) {
return Routes.allocateDiskSpace;
}
if (service.useEncryption && service.securityKey == null) {
return Routes.chooseSecurityKey;
}
if (service.guidedTarget == null) {
if (arguments == InstallationType.erase) {
return Routes.selectGuidedStorage;
} else if (arguments == InstallationType.alongside) {
return Routes.installAlongside;
}
}
return Routes.writeChangesToDisk;
}
}

class _UbuntuDesktopInstallerWizardObserver extends WizardObserver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,17 @@ Future<void> showAdvancedFeaturesDialog(
groupValue: advancedFeature.value,
onChanged: (v) => advancedFeature.value = v!,
),
// https://github.com/canonical/ubuntu-desktop-installer/issues/373
// Padding(
// padding: kContentIndentation,
// child: 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,
// ),
// ),
Padding(
padding: kContentIndentation,
child: CheckButton(
title: Text(lang.installationTypeEncrypt(flavor.name)),
subtitle: Text(lang.installationTypeEncryptInfo),
value: encryption.value,
onChanged: advancedFeature.value == 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 @@ -122,6 +122,8 @@ class InstallationTypeModel extends SafeChangeNotifier {
/// if appropriate (single guided storage).
Future<void> save() async {
_diskService.useLvm = advancedFeature == AdvancedFeature.lvm;
_diskService.useEncryption =
encryption && advancedFeature == AdvancedFeature.lvm;

// automatically pre-select a guided storage target if possible
_diskService.guidedTarget = preselectTarget(installationType);
Expand All @@ -143,7 +145,7 @@ class InstallationTypeModel extends SafeChangeNotifier {
} else if (advancedFeature == AdvancedFeature.zfs) {
_telemetryService.setPartitionMethod('use_zfs');
}
if (_diskService.hasEncryption && advancedFeature != AdvancedFeature.zfs) {
if (_diskService.useEncryption && advancedFeature != AdvancedFeature.zfs) {
_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 @@ -136,11 +136,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 Down Expand Up @@ -170,6 +166,21 @@ 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;
}
}
}

extension _OsProberList on List<OsProber> {
/// Whether the system has any OS installed multiple times.
bool get hasDuplicates =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class DiskStorageService {
bool? _needRoot;
bool? _needBoot;
bool? _useLvm;
bool? _useEncryption;
bool? _hasRst;
bool? _hasBitLocker;
bool? _hasMultipleDisks;
Expand All @@ -50,8 +51,12 @@ 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) {
log.debug('use encryption: $useEncryption');
_useEncryption = useEncryption;
}

/// Whether Secure Boot is enabled.
bool get hasSecureBoot => false; // TODO: add support for it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,19 @@ class MockDiskStorageService extends _i1.Mock
returnValue: false,
) as bool);
@override
bool get hasEncryption => (super.noSuchMethod(
Invocation.getter(#hasEncryption),
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,
Expand Down

0 comments on commit 215f465

Please sign in to comment.