diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb87df6..621274b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: ./gucc/tests/test-initcpio ./gucc/tests/test-pacmanconf ./gucc/tests/test-fstab_gen + ./gucc/tests/test-crypttab_gen shell: bash build-cmake_withoutdev: name: Build with CMake (DEVENV OFF) @@ -84,6 +85,7 @@ jobs: ./gucc/tests/test-initcpio ./gucc/tests/test-pacmanconf ./gucc/tests/test-fstab_gen + ./gucc/tests/test-crypttab_gen shell: bash build-meson_withoutdev: name: Build with Meson (DEVENV OFF) diff --git a/gucc/CMakeLists.txt b/gucc/CMakeLists.txt index 24cceb4..34015dd 100644 --- a/gucc/CMakeLists.txt +++ b/gucc/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(${PROJECT_NAME} SHARED src/user.cpp include/gucc/user.hpp src/locale.cpp include/gucc/locale.hpp src/fstab.cpp include/gucc/fstab.hpp + src/crypttab.cpp include/gucc/crypttab.hpp #src/chwd_profiles.cpp src/chwd_profiles.hpp #src/disk.cpp src/disk.hpp ) diff --git a/gucc/include/gucc/crypttab.hpp b/gucc/include/gucc/crypttab.hpp new file mode 100644 index 0000000..600107b --- /dev/null +++ b/gucc/include/gucc/crypttab.hpp @@ -0,0 +1,20 @@ +#ifndef CRYPTTAB_HPP +#define CRYPTTAB_HPP + +#include "gucc/partition.hpp" + +#include // for string +#include // for string_view +#include // for vector + +namespace gucc::fs { + +// Generate crypttab +auto generate_crypttab(const std::vector& partitions, std::string_view root_mountpoint, std::string_view crypttab_opts) noexcept -> bool; + +// Generate crypttab into string +auto generate_crypttab_content(const std::vector& partitions, std::string_view crypttab_opts) noexcept -> std::string; + +} // namespace gucc::fs + +#endif // CRYPTTAB_HPP diff --git a/gucc/meson.build b/gucc/meson.build index 454c821..38899cb 100644 --- a/gucc/meson.build +++ b/gucc/meson.build @@ -13,6 +13,7 @@ gucc_lib = library('gucc', 'src/user.cpp', 'src/locale.cpp', 'src/fstab.cpp', + 'src/crypttab.cpp', ], include_directories : [include_directories('include')], dependencies: deps diff --git a/gucc/src/crypttab.cpp b/gucc/src/crypttab.cpp new file mode 100644 index 0000000..97ae64c --- /dev/null +++ b/gucc/src/crypttab.cpp @@ -0,0 +1,118 @@ +#include "gucc/crypttab.hpp" +#include "gucc/partition.hpp" + +#include // for any_of, sort, unique_copy + +#include +#include + +#include +#include + +#include + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wuseless-cast" +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif + +#include +#include +#include + +#if defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +namespace fs = std::filesystem; +using namespace std::string_literals; +using namespace std::string_view_literals; + +namespace { + +// NOLINTNEXTLINE +static constexpr auto CRYPTTAB_HEADER = R"(# Configuration for encrypted block devices +# See crypttab(5) for details. + +# NOTE: Do not list your root (/) partition here, it must be set up +# beforehand by the initramfs (/etc/mkinitcpio.conf). + +# +)"sv; + +} // namespace + +namespace gucc::fs { + +auto gen_crypttab_entry(const Partition& partition, std::string_view crypttab_opts, bool is_root_encrypted, bool is_boot_encrypted) noexcept -> std::optional { + // skip if partition is not encypted + if (!partition.luks_mapper_name || !partition.luks_uuid) { + return std::nullopt; + } + // skip invalid usage + if (partition.luks_mapper_name->empty() || partition.luks_uuid->empty()) { + return std::nullopt; + } + + auto crypt_password = "/crypto_keyfile.bin"s; + auto crypt_options = fmt::format(FMT_COMPILE(" {}"), crypttab_opts); + + // 1. if root partition is not encrypted + // 2. if root partition is encrypted, but boot partition is not encrypted + if (!is_root_encrypted || (partition.mountpoint == "/"sv && !is_boot_encrypted)) { + crypt_password = "none"s; + crypt_options = ""s; + } + + const auto& device_str = fmt::format(FMT_COMPILE("UUID={}"), *partition.luks_uuid); + return std::make_optional(fmt::format(FMT_COMPILE("{:21} {:<45} {}{}\n"), *partition.luks_mapper_name, device_str, crypt_password, crypt_options)); +} + +auto generate_crypttab_content(const std::vector& partitions, std::string_view crypttab_opts) noexcept -> std::string { + std::string crypttab_content{CRYPTTAB_HEADER}; + + // sort by mountpoint & device + auto partitions_sorted{partitions}; + ranges::sort(partitions_sorted, {}, &Partition::mountpoint); + ranges::sort(partitions_sorted, {}, &Partition::device); + + // filter duplicates + std::vector partitions_filtered{}; + ranges::unique_copy( + partitions_sorted, std::back_inserter(partitions_filtered), + {}, + &Partition::device); + + const bool is_root_encrypted = ranges::any_of(partitions, [](auto&& part) { return part.mountpoint == "/"sv && part.luks_mapper_name; }); + const bool is_boot_encrypted = ranges::any_of(partitions, [](auto&& part) { return part.mountpoint == "/bool"sv && part.luks_mapper_name; }); + + for (auto&& partition : partitions_filtered) { + auto crypttab_entry = gen_crypttab_entry(partition, crypttab_opts, is_root_encrypted, is_boot_encrypted); + if (!crypttab_entry) { + continue; + } + crypttab_content += *crypttab_entry; + } + + return crypttab_content; +} + +auto generate_crypttab(const std::vector& partitions, std::string_view root_mountpoint, std::string_view crypttab_opts) noexcept -> bool { + const auto& crypttab_filepath = fmt::format(FMT_COMPILE("{}/etc/crypttab"), root_mountpoint); + + std::ofstream crypttab_file{crypttab_filepath, std::ios::out | std::ios::trunc}; + if (!crypttab_file.is_open()) { + spdlog::error("Failed to open crypttab for writing {}", crypttab_filepath); + return false; + } + crypttab_file << fs::generate_crypttab_content(partitions, crypttab_opts); + return true; +} + +} // namespace gucc::fs diff --git a/gucc/tests/meson.build b/gucc/tests/meson.build index 4114570..dd319ad 100644 --- a/gucc/tests/meson.build +++ b/gucc/tests/meson.build @@ -21,3 +21,11 @@ executable( link_with: [gucc_lib], include_directories: [include_directories('../include')], install: false) + +executable( + 'test-crypttab_gen', + files('unit-crypttab_gen.cpp'), + dependencies: deps, + link_with: [gucc_lib], + include_directories: [include_directories('../include')], + install: false) diff --git a/gucc/tests/unit-crypttab_gen.cpp b/gucc/tests/unit-crypttab_gen.cpp new file mode 100644 index 0000000..b82ff91 --- /dev/null +++ b/gucc/tests/unit-crypttab_gen.cpp @@ -0,0 +1,117 @@ +#include "gucc/crypttab.hpp" + +#include + +#include +#include +#include + +#include +#include + +using namespace std::string_literals; +using namespace std::string_view_literals; + +static constexpr auto CRYPTTAB_EMPTY_TEST = R"(# Configuration for encrypted block devices +# See crypttab(5) for details. + +# NOTE: Do not list your root (/) partition here, it must be set up +# beforehand by the initramfs (/etc/mkinitcpio.conf). + +# +)"sv; + +static constexpr auto CRYPTTAB_UNENCR_BOOT_TEST = R"(# Configuration for encrypted block devices +# See crypttab(5) for details. + +# NOTE: Do not list your root (/) partition here, it must be set up +# beforehand by the initramfs (/etc/mkinitcpio.conf). + +# +luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f UUID=6bdb3301-8efb-4b84-b0b7-4caeef26fd6f none +)"sv; + +int main() { + auto callback_sink = std::make_shared([](const spdlog::details::log_msg& msg) { + // noop + }); + spdlog::set_default_logger(std::make_shared("default", callback_sink)); + + const auto& uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s; + const auto& btrfs_mountopts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s; + const auto& xfs_mountopts = "defaults,lazytime,noatime,attr2,inode64,logbsize=256k,noquota"s; + + // btrfs with subvolumes + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .uuid_str = uuid_str, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = {}, .subvolume = "/@"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .uuid_str = uuid_str, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = {}, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .uuid_str = uuid_str, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = {}, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_EMPTY_TEST); + } + // basic xfs + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "xfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = xfs_mountopts}, + gucc::fs::Partition{.fstype = "fat16"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_EMPTY_TEST); + } + // luks xfs + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "xfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = xfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s}, + gucc::fs::Partition{.fstype = "linuxswap"s, .mountpoint = ""s, .uuid_str = ""s, .device = "/dev/nvme0n1p3"s, .mount_opts = "defaults,noatime"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_UNENCR_BOOT_TEST); + } + // zfs + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/home"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "zfs"s, .mountpoint = "/var/cache"s, .uuid_str = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .device = "/dev/nvme0n1p1"s, .mount_opts = "defaults,noatime,compress=zstd,space_cache=v2,commit=120"s}, + gucc::fs::Partition{.fstype = "vfat"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_EMPTY_TEST); + } + // luks btrfs with subvolumes + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@cache"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_UNENCR_BOOT_TEST); + } + // luks btrfs with subvolumes {shuffled} + { + const std::vector partitions{ + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/home"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@home"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@"s}, + gucc::fs::Partition{.fstype = "fat32"s, .mountpoint = "/boot"s, .uuid_str = "8EFB-4B84"s, .device = "/dev/nvme0n1p2"s, .mount_opts = "defaults,noatime"s}, + gucc::fs::Partition{.fstype = "btrfs"s, .mountpoint = "/var/cache"s, .device = "/dev/nvme0n1p1"s, .mount_opts = btrfs_mountopts, .luks_mapper_name = "luks-6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .luks_uuid = "6bdb3301-8efb-4b84-b0b7-4caeef26fd6f"s, .subvolume = "/@cache"s}, + }; + const auto& crypttab_content = gucc::fs::generate_crypttab_content(partitions, "luks"sv); + assert(crypttab_content == CRYPTTAB_UNENCR_BOOT_TEST); + } + + /* + + +{'claimed': False, 'device': '', 'features': {}, 'fs': 'unknown', 'fsName': 'unknown', 'mountPoint': '', 'partattrs': 0, 'partlabel': '', 'parttype': '', 'partuuid': '', 'uuid': ''} None +{'claimed': True, 'device': '/dev/sda1', 'features': {}, 'fs': 'fat32', 'fsName': 'fat32', 'mountPoint': '/boot', 'partattrs': 0, 'partlabel': '', 'parttype': '', 'partuuid': '57F0553F-8C12-46DC-BDA7-868728368EEF', 'uuid': '86F8-2CD4'} None +{'claimed': True, 'device': '/dev/sda2', 'features': {}, 'fs': 'btrfs', 'fsName': 'luks2', 'luksMapperName': 'luks-6363636e-bc94-46fa-8ede-86cb57295161', 'luksPassphrase': '123456789', 'luksUuid': '6363636e-bc94-46fa-8ede-86cb57295161', 'mountPoint': '/', 'partattrs': 0, 'partlabel': 'root', 'parttype': '', 'partuuid': '2B6DA8D5-9A95-4917-B736-8F204E393488', 'uuid': '6363636e-bc94-46fa-8ede-86cb57295161'} {'name': 'luks-6363636e-bc94-46fa-8ede-86cb57295161', 'device': 'UUID=6363636e-bc94-46fa-8ede-86cb57295161', 'password': 'none', 'options': ''} + + + */ +}