diff --git a/rust/agama-lib/share/examples/storage/encryption.json b/rust/agama-lib/share/examples/storage/encryption.json new file mode 100644 index 000000000..96053d41e --- /dev/null +++ b/rust/agama-lib/share/examples/storage/encryption.json @@ -0,0 +1,53 @@ +{ + "storage": { + "drives": [ + { + "encryption": { + "luks1": { + "password": "12345", + "cipher": "aes-xts-plain64", + "keySize": 512 + } + } + }, + { + "partitions": [ + { + "encryption": { + "luks2": { + "password": "12345", + "cipher": "aes-xts-plain64", + "keySize": 512, + "pbkdFunction": "argon2i", + "label": "data" + } + } + }, + { + "encryption": { + "pervasiveLuks2": { + "password": "12345" + } + } + }, + { + "encryption": { + "tpmFde": { + "password": "12345" + } + } + }, + { + "encryption": "protected_swap" + }, + { + "encryption": "secure_swap" + }, + { + "encryption": "random_swap" + } + ] + } + ] + } +} diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 674cd50f5..d7a0cd13a 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -1180,6 +1180,24 @@ } } }, + "encryptionTPM": { + "title": "TPM-Based Full Disk Encrytion", + "type": "object", + "additionalProperties": false, + "required": ["tpmFde"], + "properties": { + "tpmFde": { + "type": "object", + "additionalProperties": false, + "required": ["password"], + "properties": { + "password": { + "$ref": "#/$defs/encryptionPassword" + } + } + } + } + }, "encryptionSwap": { "title": "Swap encryptions", "enum": ["protected_swap", "secure_swap", "random_swap"] @@ -1189,6 +1207,7 @@ { "$ref": "#/$defs/encryptionLUKS1" }, { "$ref": "#/$defs/encryptionLUKS2" }, { "$ref": "#/$defs/encryptionPervasiveLUKS2" }, + { "$ref": "#/$defs/encryptionTPM" }, { "$ref": "#/$defs/encryptionSwap" } ] }, diff --git a/service/lib/agama/storage/config_checker.rb b/service/lib/agama/storage/config_checker.rb index 25c3c6a47..cead5a632 100644 --- a/service/lib/agama/storage/config_checker.rb +++ b/service/lib/agama/storage/config_checker.rb @@ -192,7 +192,7 @@ def missing_encryption_password_issue(config) # @return [Issue, nil] def unavailable_encryption_method_issue(config) method = config.method - return if !method || method.available? + return if !method || available_encryption_methods.include?(method) error( format( @@ -204,6 +204,17 @@ def unavailable_encryption_method_issue(config) ) end + # @see #unavailable_encryption_method_issue + # + # @return [Array] + def available_encryption_methods + tpm_fde = Y2Storage::EncryptionMethod::TPM_FDE + + methods = Y2Storage::EncryptionMethod.available + methods << tpm_fde if tpm_fde.possible? + methods + end + # @see #encryption_issues # # @param config [Configs::Drive, Configs::Partition, Configs::LogicalVolume] diff --git a/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb index 1956c458c..9f75003ee 100644 --- a/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb +++ b/service/lib/agama/storage/config_conversions/from_json_conversions/encryption.rb @@ -46,28 +46,39 @@ def conversions return luks1_conversions if luks1? return luks2_conversions if luks2? return pervasive_luks2_conversions if pervasive_luks2? + return tpm_fde_conversions if tpm_fde? swap_encryption_conversions end + # @return [Boolean] def luks1? return false unless encryption_json.is_a?(Hash) !encryption_json[:luks1].nil? end + # @return [Boolean] def luks2? return false unless encryption_json.is_a?(Hash) !encryption_json[:luks2].nil? end + # @return [Boolean] def pervasive_luks2? return false unless encryption_json.is_a?(Hash) !encryption_json[:pervasiveLuks2].nil? end + # @return [Boolean] + def tpm_fde? + return false unless encryption_json.is_a?(Hash) + + !encryption_json[:tpmFde].nil? + end + # @return [Hash] def luks1_conversions luks1_json = encryption_json[:luks1] @@ -104,6 +115,16 @@ def pervasive_luks2_conversions } end + # @return [Hash] + def tpm_fde_conversions + tpm_fde_json = encryption_json[:tpmFde] + + { + method: Y2Storage::EncryptionMethod::TPM_FDE, + password: convert_password(tpm_fde_json) + } + end + # @return [Hash] def swap_encryption_conversions return {} unless encryption_json.is_a?(String) diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions.rb b/service/lib/agama/storage/config_conversions/to_json_conversions.rb index 47a379ca0..b405e2684 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions.rb @@ -24,12 +24,10 @@ require "agama/storage/config_conversions/from_json_conversions/config" require "agama/storage/config_conversions/from_json_conversions/drive" require "agama/storage/config_conversions/from_json_conversions/encryption" +require "agama/storage/config_conversions/from_json_conversions/encryption_properties" require "agama/storage/config_conversions/from_json_conversions/filesystem" require "agama/storage/config_conversions/from_json_conversions/logical_volume" -require "agama/storage/config_conversions/from_json_conversions/luks1" -require "agama/storage/config_conversions/from_json_conversions/luks2" require "agama/storage/config_conversions/from_json_conversions/partition" -require "agama/storage/config_conversions/from_json_conversions/pervasive_luks2" require "agama/storage/config_conversions/from_json_conversions/search" require "agama/storage/config_conversions/from_json_conversions/size" require "agama/storage/config_conversions/from_json_conversions/volume_group" diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb index f401a95f7..d12692a8f 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption.rb @@ -20,9 +20,7 @@ # find current contact information at www.suse.com. require "agama/storage/config_conversions/to_json_conversions/base" -require "agama/storage/config_conversions/to_json_conversions/luks1" -require "agama/storage/config_conversions/to_json_conversions/luks2" -require "agama/storage/config_conversions/to_json_conversions/pervasive_luks2" +require "agama/storage/config_conversions/to_json_conversions/encryption_properties" require "agama/storage/configs/encryption" module Agama @@ -51,29 +49,41 @@ def conversions method = config.method if method.is?(:luks1) - convert_luks1 + luks1_conversions elsif method.is?(:luks2) - convert_luks2 + luks2_conversions elsif method.is?(:pervasive_luks2) - convert_pervasive_luks2 + pervasive_luks2_conversions + elsif method.is?(:tpm_fde) + tpm_fde_conversions else {} end end # @return [Hash] - def convert_luks1 - { luks1: ToJSONConversions::Luks1.new(config).convert } + def luks1_conversions + { luks1: convert_encryption_properties } end # @return [Hash] - def convert_luks2 - { luks2: ToJSONConversions::Luks2.new(config).convert } + def luks2_conversions + { luks2: convert_encryption_properties } end # @return [Hash] - def convert_pervasive_luks2 - { pervasiveLuks2: ToJSONConversions::PervasiveLuks2.new(config).convert } + def pervasive_luks2_conversions + { pervasiveLuks2: convert_encryption_properties } + end + + # @return [Hash] + def tpm_fde_conversions + { tpmFde: convert_encryption_properties } + end + + # @return [Hash, nil] + def convert_encryption_properties + ToJSONConversions::EncryptionProperties.new(config).convert end # @return [String] diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/luks2.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb similarity index 55% rename from service/lib/agama/storage/config_conversions/to_json_conversions/luks2.rb rename to service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb index 84a9d600e..44ef0cfd2 100644 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/luks2.rb +++ b/service/lib/agama/storage/config_conversions/to_json_conversions/encryption_properties.rb @@ -26,8 +26,8 @@ module Agama module Storage module ConfigConversions module ToJSONConversions - # Luks2 conversion to JSON hash according to schema. - class Luks2 < Base + # Encryption properties conversion to JSON hash according to schema. + class EncryptionProperties < Base # @see Base def self.config_type Configs::Encryption @@ -37,6 +37,32 @@ def self.config_type # @see Base#conversions def conversions + method = config.method + + if method.is?(:luks1) + luks1_properties_conversions + elsif method.is?(:luks2) + luks2_properties_conversions + elsif method.is?(:pervasive_luks2) + pervasive_luks2_properties_conversions + elsif method.is?(:tpm_fde) + tpm_fde_properties_conversions + else + {} + end + end + + # @return [Hash] + def luks1_properties_conversions + { + password: config.password, + keySize: config.key_size, + cipher: config.cipher + } + end + + # @return [Hash] + def luks2_properties_conversions { password: config.password, keySize: config.key_size, @@ -45,6 +71,20 @@ def conversions pbkdFunction: config.pbkd_function&.to_s } end + + # @return [Hash] + def pervasive_luks2_properties_conversions + { + password: config.password + } + end + + # @return [Hash] + def tpm_fde_properties_conversions + { + password: config.password + } + end end end end diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/luks1.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/luks1.rb deleted file mode 100644 index 7cf77d1e4..000000000 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/luks1.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/to_json_conversions/base" -require "agama/storage/configs/encryption" - -module Agama - module Storage - module ConfigConversions - module ToJSONConversions - # Luks1 conversion to JSON hash according to schema. - class Luks1 < Base - # @see Base - def self.config_type - Configs::Encryption - end - - private - - # @see Base#conversions - def conversions - { - password: config.password, - keySize: config.key_size, - cipher: config.cipher - } - end - end - end - end - end -end diff --git a/service/lib/agama/storage/config_conversions/to_json_conversions/pervasive_luks2.rb b/service/lib/agama/storage/config_conversions/to_json_conversions/pervasive_luks2.rb deleted file mode 100644 index 63a129e96..000000000 --- a/service/lib/agama/storage/config_conversions/to_json_conversions/pervasive_luks2.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, contact SUSE LLC. -# -# To contact SUSE LLC about this file by physical or electronic mail, you may -# find current contact information at www.suse.com. - -require "agama/storage/config_conversions/to_json_conversions/base" -require "agama/storage/configs/encryption" - -module Agama - module Storage - module ConfigConversions - module ToJSONConversions - # Pervasive Luks2 conversion to JSON hash according to schema. - class PervasiveLuks2 < Base - # @see Base - def self.config_type - Configs::Encryption - end - - private - - # @see Base#conversions - def conversions - { - password: config.password - } - end - end - end - end - end -end diff --git a/service/lib/agama/storage/configs/btrfs.rb b/service/lib/agama/storage/configs/btrfs.rb index 21f7f65fe..9ad106e5f 100644 --- a/service/lib/agama/storage/configs/btrfs.rb +++ b/service/lib/agama/storage/configs/btrfs.rb @@ -39,12 +39,6 @@ class Btrfs # @return [String, nil] attr_accessor :default_subvolume - # Constructor - def initialize - @snapshots = false - @read_only = false - end - # @return [Boolean] def snapshots? !!snapshots diff --git a/service/lib/y2storage/proposal/agama_device_planner.rb b/service/lib/y2storage/proposal/agama_device_planner.rb index b245b29b8..1c48d3397 100644 --- a/service/lib/y2storage/proposal/agama_device_planner.rb +++ b/service/lib/y2storage/proposal/agama_device_planner.rb @@ -87,14 +87,14 @@ def grow?(device, config) config.size.max.unlimited? || config.size.max > device.size end - # @param planned [Planned::Disk, Planned::Partition] + # @param planned [Planned::Disk, Planned::Partition, Planned::LvmLv] # @param config [#encryption, #filesystem] def configure_block_device(planned, config) configure_encryption(planned, config.encryption) if config.encryption configure_filesystem(planned, config.filesystem) if config.filesystem end - # @param planned [Planned::Disk, Planned::Partition] + # @param planned [Planned::Disk, Planned::Partition, Planned::LvmLv] # @param config [Agama::Storage::Configs::Filesystem] def configure_filesystem(planned, config) planned.mount_point = config.path @@ -105,14 +105,14 @@ def configure_filesystem(planned, config) configure_filesystem_type(planned, config.type) if config.type end - # @param planned [Planned::Disk, Planned::Partition] + # @param planned [Planned::Disk, Planned::Partition, Planned::LvmLv] # @param config [Agama::Storage::Configs::FilesystemType] def configure_filesystem_type(planned, config) planned.filesystem_type = config.fs_type configure_btrfs(planned, config.btrfs) if config.btrfs end - # @param planned [Planned::Disk, Planned::Partition] + # @param planned [Planned::Disk, Planned::Partition, Planned::LvmLv] # @param config [Agama::Storage::Configs::Btrfs] def configure_btrfs(planned, config) # TODO: we need to discuss what to do with transactional systems and the read_only @@ -123,7 +123,7 @@ def configure_btrfs(planned, config) planned.subvolumes = config.subvolumes end - # @param planned [Planned::Disk, Planned::Partition] + # @param planned [Planned::Disk, Planned::Partition, Planned::LvmLv] # @param config [Agama::Storage::Configs::Encryption] def configure_encryption(planned, config) planned.encryption_password = config.password @@ -134,7 +134,7 @@ def configure_encryption(planned, config) planned.encryption_key_size = config.key_size end - # @param planned [Planned::Partition] + # @param planned [Planned::Partition, Planned::LvmLv] # @param config [Agama::Storage::Configs::Size] def configure_size(planned, config) planned.min_size = config.min diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 4246bdd1b..6adb956ef 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Oct 22 09:48:57 UTC 2024 - José Iván López González + +- Storage: extend schema to allow selecting TPM FDE as encryption + method (gh#agama-project/agama#1681). + ------------------------------------------------------------------- Wed Oct 16 15:09:31 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/test/agama/storage/config_checker_test.rb b/service/test/agama/storage/config_checker_test.rb index bf818958a..dc1f8f182 100644 --- a/service/test/agama/storage/config_checker_test.rb +++ b/service/test/agama/storage/config_checker_test.rb @@ -69,6 +69,31 @@ end end + context "if TPM FDE is not possible" do + let(:encryption) do + { + tpmFde: { + password: "12345" + } + } + end + + before do + allow_any_instance_of(Y2Storage::EncryptionMethod::TpmFde) + .to(receive(:possible?)) + .and_return(false) + end + + it "includes the expected issue" do + issues = subject.issues + expect(issues.size).to eq(1) + + issue = issues.first + expect(issue.error?).to eq(true) + expect(issue.description).to match("'TPM-Based Full Disk Encrytion' is not available") + end + end + context "with invalid method" do let(:encryption) { "protected_swap" } let(:filesystem) { { path: "/" } } diff --git a/service/test/agama/storage/config_conversions/from_json_test.rb b/service/test/agama/storage/config_conversions/from_json_test.rb index 87792bdb7..29af56465 100644 --- a/service/test/agama/storage/config_conversions/from_json_test.rb +++ b/service/test/agama/storage/config_conversions/from_json_test.rb @@ -201,6 +201,28 @@ expect(encryption.label).to be_nil end end + + context "if 'encryption' is 'tmpFde'" do + let(:encryption) do + { + tpmFde: { + password: "12345" + } + } + end + + it "sets #encryption to the expected value" do + config = config_proc.call(subject.convert) + encryption = config.encryption + expect(encryption).to be_a(Agama::Storage::Configs::Encryption) + expect(encryption.method).to eq(Y2Storage::EncryptionMethod::TPM_FDE) + expect(encryption.password).to eq("12345") + expect(encryption.key_size).to be_nil + expect(encryption.pbkd_function).to be_nil + expect(encryption.cipher).to be_nil + expect(encryption.label).to be_nil + end + end end shared_examples "with filesystem" do |config_proc| diff --git a/service/test/agama/storage/config_conversions/to_json_test.rb b/service/test/agama/storage/config_conversions/to_json_test.rb index 0b6102e0e..86b0b41a0 100644 --- a/service/test/agama/storage/config_conversions/to_json_test.rb +++ b/service/test/agama/storage/config_conversions/to_json_test.rb @@ -232,6 +232,29 @@ end end + context "if encryption method is TMP FDE" do + let(:encryption) do + { + tpmFde: { + password: "12345" + } + } + end + + it "generates the expected JSON" do + config_json = result_scope.call(subject.convert) + encryption_json = config_json[:encryption] + + expect(encryption_json).to eq( + { + tpmFde: { + password: "12345" + } + } + ) + end + end + context "if encryption method is protected swap" do let(:encryption) { "protected_swap" } diff --git a/service/test/agama/storage/config_solver_test.rb b/service/test/agama/storage/config_solver_test.rb index 4973011e5..0717922f0 100644 --- a/service/test/agama/storage/config_solver_test.rb +++ b/service/test/agama/storage/config_solver_test.rb @@ -46,7 +46,7 @@ "mount_path" => "/", "filesystem" => "btrfs", "size" => { "auto" => true, "min" => "5 GiB", "max" => "10 GiB" }, "btrfs" => { - "snapshots" => true, "default_subvolume" => "@", + "snapshots" => true, "default_subvolume" => "@", "read_only" => true, "subvolumes" => ["home", "opt", "root", "srv"] }, "outline" => { @@ -188,12 +188,42 @@ expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs) expect(filesystem.type.btrfs.snapshots?).to eq(true) - expect(filesystem.type.btrfs.read_only?).to eq(false) + expect(filesystem.type.btrfs.read_only?).to eq(true) expect(filesystem.type.btrfs.default_subvolume).to eq("@") expect(filesystem.type.btrfs.subvolumes).to all(be_a(Y2Storage::SubvolSpecification)) end end + context "if a config does not specify all the btrfs properties" do + let(:config_json) do + { + drives: [ + { + filesystem: { + path: "/", + type: { + btrfs: { + snapshots: false + } + } + } + } + ] + } + end + + it "completes the btrfs config according to the product info" do + subject.solve(config) + + drive = config.drives.first + btrfs = drive.filesystem.type.btrfs + expect(btrfs.snapshots?).to eq(false) + expect(btrfs.read_only?).to eq(true) + expect(btrfs.default_subvolume).to eq("@") + expect(btrfs.subvolumes).to all(be_a(Y2Storage::SubvolSpecification)) + end + end + partition_proc = proc { |c| c.drives.first.partitions.first } context "if a config does not specify size" do