├── .gitignore ├── .rspec ├── .rubocop.yml ├── .solargraph.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── update-loader └── vscode_rspec ├── crypt_reboot.gemspec ├── exe └── cryptreboot ├── lib ├── crypt_reboot.rb └── crypt_reboot │ ├── boot_config.rb │ ├── cli.rb │ ├── cli │ ├── exiter.rb │ ├── happy_exiter.rb │ ├── params │ │ ├── definition.rb │ │ ├── flattener.rb │ │ ├── help_generator.rb │ │ └── parser.rb │ ├── params_parsing_executor.rb │ └── sad_exiter.rb │ ├── concatenator.rb │ ├── config.rb │ ├── crypt_tab │ ├── deserializer.rb │ ├── entry.rb │ ├── entry_deserializer.rb │ ├── entry_serializer.rb │ ├── keyfile_locator.rb │ ├── luks_to_plain_converter.rb │ ├── serializer.rb │ └── zfs_keystore_entries_generator.rb │ ├── elastic_memory_locker.rb │ ├── files_generator.rb │ ├── files_writer.rb │ ├── gziper.rb │ ├── initramfs │ ├── archiver.rb │ ├── decompressor.rb │ ├── decompressor │ │ ├── intolerant_decompressor.rb │ │ └── tolerant_decompressor.rb │ ├── extractor.rb │ └── patcher.rb │ ├── initramfs_patch_squeezer.rb │ ├── instantiable_config.rb │ ├── kexec │ └── loader.rb │ ├── kexec_patching_loader.rb │ ├── lazy_config.rb │ ├── luks │ ├── checker.rb │ ├── data.rb │ ├── data_fetcher.rb │ ├── dumper.rb │ ├── dumper │ │ ├── luks_v1_parser.rb │ │ └── luks_v2_parser.rb │ ├── key_fetcher.rb │ └── version_detector.rb │ ├── luks_crypt_tab_patcher.rb │ ├── passphrase_asker.rb │ ├── patched_initramfs_generator.rb │ ├── rebooter.rb │ ├── runner.rb │ ├── runner │ ├── binary.rb │ ├── boolean.rb │ ├── generic.rb │ ├── lines.rb │ ├── no_result.rb │ └── text.rb │ ├── safe_temp │ ├── directory.rb │ ├── file_name.rb │ └── mounter.rb │ ├── single_assign_restricted_map.rb │ ├── version.rb │ └── zfs_keystore_patcher.rb └── spec ├── crypt_reboot ├── cli │ ├── exiter_spec.rb │ ├── params │ │ ├── flattener_spec.rb │ │ ├── help_generator_spec.rb │ │ └── parser_spec.rb │ └── params_parsing_executor_spec.rb ├── concatenator_spec.rb ├── config_spec.rb ├── crypt_tab │ ├── deserializer_spec.rb │ ├── entry_deserializer_spec.rb │ ├── entry_serializer_spec.rb │ ├── entry_spec.rb │ ├── keyfile_locator_spec.rb │ ├── luks_to_plain_converter_spec.rb │ ├── serializer_spec.rb │ └── zfs_keystore_entries_generator_spec.rb ├── elastic_memory_locker_spec.rb ├── files_generator_spec.rb ├── files_writer_spec.rb ├── gziper_spec.rb ├── initramfs │ ├── archiver_spec.rb │ ├── decompressor │ │ ├── intolerant_decompressor_spec.rb │ │ └── tolerant_decompressor_spec.rb │ ├── decompressor_spec.rb │ ├── extractor_spec.rb │ └── patcher_spec.rb ├── initramfs_patch_squeezer_spec.rb ├── instantiable_config_spec.rb ├── kexec │ └── loader_spec.rb ├── kexec_patching_loader_spec.rb ├── lazy_config_spec.rb ├── luks │ ├── checker_spec.rb │ ├── data_fetcher_spec.rb │ ├── data_spec.rb │ ├── dumper │ │ ├── luks_v1_parser_spec.rb │ │ └── luks_v2_parser_spec.rb │ ├── key_fetcher_spec.rb │ └── version_detector_spec.rb ├── luks_crypt_tab_patcher_spec.rb ├── patched_initramfs_generator_spec.rb ├── rebooter_spec.rb ├── runner │ ├── binary_spec.rb │ ├── boolean_spec.rb │ ├── lines_spec.rb │ ├── no_result_spec.rb │ └── text_spec.rb ├── safe_temp │ ├── directory_spec.rb │ └── mounter_spec.rb ├── single_assign_restricted_map_spec.rb └── zfs_keystore_patcher_spec.rb ├── crypt_reboot_spec.rb ├── fixtures ├── archiver │ └── files │ │ └── file1.txt ├── concatenator │ ├── f1.txt │ └── f2.txt ├── dummy_initramfs ├── extracted_initramfs │ ├── luks_crypt_tab │ │ └── cryptroot │ │ │ └── crypttab │ └── zfs_keystore │ │ └── scripts │ │ └── zfs ├── liberal_cat ├── luks_dump │ ├── v1.txt │ └── v2.txt ├── luks_headers │ ├── invalid.bin │ ├── v1.bin │ └── v2.bin └── zfs_dev │ └── zvol │ ├── dummy │ └── dummy │ └── rpool │ ├── dummy │ └── keystore └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | .rspec_status 11 | Gemfile.lock 12 | .ruby-version 13 | /lib/basic_loader.rb 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | Style/FrozenStringLiteralComment: 6 | Enabled: true 7 | SafeAutoCorrect: true 8 | AllCops: 9 | NewCops: enable 10 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - "**/*.rb" 4 | exclude: 5 | - spec/**/* 6 | - test/**/* 7 | - vendor/**/* 8 | - ".bundle/**/*" 9 | require: [] 10 | domains: [] 11 | reporters: 12 | - rubocop 13 | - require_not_found 14 | formatter: 15 | rubocop: 16 | cops: safe 17 | except: [] 18 | only: [] 19 | extra_args: [] 20 | require_paths: [] 21 | plugins: [] 22 | max_files: 5000 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "rdbg", 9 | "name": "Debug specs", 10 | "request": "launch", 11 | "command": "rspec", 12 | "script": "spec", 13 | "args": [], 14 | "askParameters": false 15 | }, 16 | { 17 | "type": "rdbg", 18 | "name": "Debug console", 19 | "request": "launch", 20 | "script": "${workspaceFolder}/bin/console", 21 | "args": [], 22 | "askParameters": false, 23 | "useTerminal": true 24 | }, 25 | { 26 | "type": "rdbg", 27 | "name": "Debug current file", 28 | "request": "launch", 29 | "script": "${file}", 30 | "args": [], 31 | "askParameters": false 32 | }, 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.3.1] - 2024-10-04 2 | 3 | - Add a warning about usage of --debug flag 4 | 5 | ## [0.3.0] - 2024-09-27 6 | 7 | - Refactor ZFS keystore encryption support 8 | 9 | ## [0.3.0.beta.1] - 2024-09-26 10 | 11 | - Add preliminary support for LUKS-keystore-based ZFS encryption implemented by Ubuntu 12 | 13 | ## [0.2.1] - 2023-11-12 14 | 15 | - Use new MemoryLocker without a need for FFI compilation step 16 | 17 | ## [0.2.0] - 2023-07-29 18 | 19 | - Make memory locking optional with `--insecure-memory` command line option 20 | - Remove FFI gem dependency 21 | 22 | ## [0.1.2] - 2023-07-22 23 | 24 | - Lock memory to prevent secrets leaking to swap 25 | - Use `ramfs` instead of `tmpfs` for the same reason 26 | 27 | ## [0.1.1] - 2023-07-13 28 | 29 | - Standardize passphrase prompt 30 | 31 | ## [0.1.0] - 2023-07-09 32 | 33 | - Initial release 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | 6 | gem 'rake', '~> 13.0' 7 | gem 'rspec', '~> 3.0' 8 | gem 'rubocop', '~> 1.21' 9 | gem 'rubocop-rake', '~> 0.6.0' 10 | gem 'rubocop-rspec', '~> 2.20' 11 | gem 'solargraph', '~> 0.49' 12 | gem 'zeitwerk', '~> 2.6' 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Paweł Pokrywka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptreboot 2 | 3 | [![Gem Version](https://badge.fury.io/rb/crypt_reboot.svg)](https://badge.fury.io/rb/crypt_reboot) 4 | 5 | Convenient reboot for Linux systems with encrypted root partition. 6 | 7 | > Just type `cryptreboot` instead of `reboot`. 8 | 9 | It asks for a passphrase and reboots the system afterward, automatically 10 | unlocking the drive on startup using 11 | [in-memory initramfs patching and kexec](https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk). 12 | Without explicit consent, no secrets are stored on disk, even temporarily. 13 | 14 | Useful when unlocking the drive at startup is difficult, such as on headless 15 | and remote systems. 16 | 17 | By default, it uses the current kernel command line, `/boot/vmlinuz` as kernel 18 | and `/boot/initrd.img` as initramfs. 19 | 20 | Will work properly when using standard passphrase-based disk unlocking. 21 | Fancy methods such as using an external USB with a passphrase file will fail. 22 | 23 | ## Supported disk encryption methods 24 | 25 | ### LUKS crypttab 26 | LUKS-based disk-encryption configured with `/etc/crypttab` file. 27 | 28 | ### ZFS keystore 29 | Native ZFS encryption with LUKS-encrypted keystore volume. 30 | 31 | ## Compatible Linux distributions 32 | 33 | Currently, cryptreboot depends on `initramfs-tools` package which is available in 34 | Debian-based distributions. Therefore one should expect, this tool to work on 35 | Debian, Ubuntu, Linux Mint, Pop!_OS, etc. 36 | 37 | On the other hand, do not expect it to work on other distributions now. 38 | But support for them may come in upcoming versions. 39 | 40 | Following distributions were tested by the author on the AMD64 machine: 41 | 42 | - LUKS crypttab disk encryption method 43 | - DappNode 0.2.75 is based on Debian 12, see below 44 | - Debian 12 needs [symlinks for kernel and initramfs](#no-symlinks-to-most-recent-kernel-and-initramfs) 45 | - Pop!_OS 22.04 LTS 46 | - Ubuntu 24.04 LTS 47 | - Ubuntu 23.04 48 | - Ubuntu 22.04 LTS 49 | - Ubuntu 20.04 LTS needs tiny adjustments to system settings, 50 | specifically [changing compression](#lz4-initramfs-compression) and 51 | [fixing systemd kexec support](#staged-kernel-not-being-executed-by-systemd), but still 52 | [sometimes](#unable-to-kexec-on-reboot-using-old-systemd) reboot experience may be suboptimal 53 | - ~~Ubuntu 18.04 LTS~~ is not supported (initramfs uses *pre-crypttab* format) 54 | 55 | - ZFS keystore disk encryption method 56 | - Ubuntu 24.04 LTS 57 | - Ubuntu 22.04 LTS 58 | 59 | If you have successfully run cryptreboot on another distribution, 60 | please contact me and I will update the list. 61 | 62 | ## Requirements 63 | 64 | You need to ensure those are installed: 65 | - `ruby` >= 2.7 66 | - `kexec-tools` 67 | - `initramfs-tools` (other initramfs generators, such as `dracut` are 68 | not supported yet) 69 | 70 | If you use recent, mainstream Linux distribution, other requirements are 71 | probably already met: 72 | - `kexec` support in the kernel 73 | - `ramfs` filesystem support in kernel 74 | - `cryptsetup` (if you use disk encryption, it should be installed) 75 | - `systemd` or another way to guarantee staged kernel is executed on reboot 76 | - `strace` (not required if `--skip-lz4-check` flag is specified) 77 | 78 | If you use Debian-based distribution, use this command to install required packages: 79 | 80 | $ sudo apt install --no-install-recommends cryptsetup-initramfs kexec-tools ruby strace systemd 81 | 82 | When asked if kexec should handle reboots, answer `yes` (however the answer probably 83 | doesn't matter for cryptreboot to work). 84 | 85 | ## Installation 86 | 87 | Make sure the required software is installed, then install the gem system-wide by executing: 88 | 89 | $ sudo gem install crypt_reboot 90 | 91 | To upgrade run: 92 | 93 | $ sudo gem update crypt_reboot 94 | 95 | ## Usage 96 | 97 | Cryptreboot performs operations normally only available to the root user, 98 | so it is suggested to use sudo or a similar utility. 99 | 100 | To perform a reboot type: 101 | 102 | $ sudo cryptreboot 103 | 104 | To see the usage, run: 105 | 106 | $ cryptreboot --help 107 | 108 | ## Troubleshooting 109 | 110 | ### LZ4 initramfs compression 111 | 112 | If you get: 113 | 114 | > LZ4 compression is not allowed, change the compression algorithm in 115 | initramfs.conf and regenerate the initramfs image 116 | 117 | it means initramfs was compressed using the LZ4 algorithm, which seems to 118 | have issues with concatenating initramfs images. 119 | 120 | In case you are 100% sure LZ4 won't cause problems, you can use 121 | `--skip-lz4-check` command line flag. This will make the error message 122 | go away, but you risk automatic disk unlocking at startup to fail randomly. 123 | 124 | Instead, the recommended approach is to change the compression algorithm 125 | in `/etc/initramfs-tools/initramfs.conf` file. Look for `COMPRESS` and 126 | set it to some other value such as `gzip` (the safe choice), or `zstd` 127 | (the best compression, but your kernel and `initramfs-tools` need to support it). 128 | 129 | Here is a one-liner to change compression to `gzip`: 130 | 131 | $ sudo sed -iE 's/^\s*COMPRESS=.*$/COMPRESS=gzip/' /etc/initramfs-tools/initramfs.conf 132 | 133 | Then you need to regenerate all of your initramfs images: 134 | 135 | $ sudo update-initramfs -k all -u 136 | 137 | That's it. 138 | 139 | Resources related to the issue: 140 | - [Appending files to initramfs image - reliable? (StackExchange)](https://unix.stackexchange.com/a/737219) 141 | - [What is the correct frame format for Linux (Lz4 issue)](https://github.com/lz4/lz4/issues/956) 142 | - [Initramfs unpacking failed (Ubuntu bug report)](https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1835660) 143 | 144 | ### Staged kernel not being executed by systemd 145 | 146 | If rebooting with cryptreboot doesn't seem to differ from a standard 147 | reboot, it may suggest staged kernel is not being executed by the 148 | `systemd` at the end of the shutdown procedure. 149 | 150 | The solution I found is to execute `kexec -e` instead of 151 | `systemctl --force kexec` when the system is ready for a reboot. 152 | To do that `systemd-kexec.service` has to be modified. 153 | To make the change minimal, let's use `systemd drop-in` for that: 154 | 155 | $ sudo mkdir -p /etc/systemd/system/systemd-kexec.service.d/ 156 | $ echo -e "[Service]\nExecStart=\nExecStart=kexec -e" | sudo tee /etc/systemd/system/systemd-kexec.service.d/override.conf 157 | 158 | That should work. 159 | 160 | To cancel the change, remove the file: 161 | 162 | $ sudo rm /etc/systemd/system/systemd-kexec.service.d/override.conf 163 | 164 | ### No symlinks to the most recent kernel and initramfs 165 | 166 | By default, cryptreboot looks for kernel in `/boot/vmlinuz` and for initramfs 167 | in `/boot/initrd.img`. If those files are missing in your Linux distribution, 168 | cryptreboot will fail, unless you use `--kernel` and `--initramfs` command line 169 | options. 170 | 171 | $ sudo cryptreboot --kernel /boot/vmlinuz-`uname -r` --initramfs /boot/initrd.img-`uname -r` 172 | 173 | If you don't want to specify options every time you reboot, add symlinks to 174 | the currently running kernel and initramfs: 175 | 176 | $ cd /boot 177 | $ sudo ln -sf vmlinuz-`uname -r` vmlinuz 178 | $ sudo ln -sf initrd.img-`uname -r` initrd.img 179 | 180 | Unfortunately, you need to rerun it after each kernel upgrade, otherwise, 181 | cryptreboot is going to boot the old kernel. 182 | Upcoming versions of cryptreboot will offer better solutions. 183 | 184 | ### Problems with memory locking 185 | 186 | If you get: 187 | 188 | > Locking error: Failed to lock memory 189 | 190 | it means there was an error while locking memory to prevent a risk of sensitive data ending in a swap space. 191 | 192 | Make sure you have permission to lock memory. Root users have. 193 | If permissions are ok, then please report a bug describing your setup. 194 | 195 | The solution of last resort is to use `--insecure-memory` flag, which disables memory locking completely. 196 | 197 | ### Unable to kexec on reboot using old systemd 198 | 199 | Ubuntu 20.04 ships with `systemd` which may fall back to standard reboot instead of using `kexec`, because this utility 200 | is located on a filesystem being unmounted during the shutdown sequence. 201 | 202 | As a result, using cryptreboot would feel like using normal reboot. 203 | 204 | To tell if your system is affected, you have to check messages printed to the console after you run cryptreboot. 205 | This message happens just before reboot, so you will have just a few milliseconds to notice it on screen: 206 | 207 | > shutdown[1]: (sd-kexec) failed with exit status 1 208 | 209 | [There is a fix](https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1969365) waiting to be included in 210 | a stable release update to `systemd` since 2023-07-21. 211 | 212 | In the meantime, as a workaround, you can use `kexec` directly. **Warning: it will skip the standard shutdown procedure. Filesystems won't be unmounted, services won't be stopped, etc. It is like hitting `reset` button**. 213 | However, when you use a decent filesystem with journalling the risk of things going bad should not be high. 214 | 215 | Given the above warning, to reboot skipping the shutdown procedure, run: 216 | 217 | $ sudo cryptreboot -p 218 | $ sudo kexec -e # will skip proper shutdown sequence 219 | 220 | ## Development 221 | 222 | After checking out the repo, run `bundle install` to install 223 | dependencies. Then, run `rake spec` to run the tests. You can also 224 | run `bin/console` for an interactive prompt that will allow you 225 | to experiment. 226 | 227 | To build the gem, run `rake build`. To release a new version, update 228 | the version number in `version.rb`, and then run `rake release`, which 229 | will create a git tag for the version, push git commits and the created 230 | tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 231 | 232 | ## Contributing 233 | 234 | Bug reports and pull requests are welcome on GitHub at 235 | https://github.com/phantom-node/cryptreboot. 236 | This project is intended to be a safe, welcoming space for collaboration, 237 | and contributors are expected to adhere to the 238 | [code of conduct](https://github.com/phantom-node/cryptreboot/blob/master/CODE_OF_CONDUCT.md). 239 | 240 | ## Author 241 | 242 | My name is Paweł Pokrywka and I'm the author of cryptreboot. 243 | 244 | If you want to contact me or get to know me better, check out 245 | [my blog](https://www.pawelpokrywka.com). 246 | 247 | Thank you for your interest in this project :) 248 | 249 | ## License 250 | 251 | The software is available as open source under the terms of the 252 | [MIT License](https://opensource.org/licenses/MIT). 253 | 254 | ## Code of Conduct 255 | 256 | Everyone interacting in the Cryptreboot project's codebases, issue 257 | trackers, chat rooms, and mailing lists is expected to follow the 258 | [code of conduct](https://github.com/phantom-node/cryptreboot/blob/master/CODE_OF_CONDUCT.md). 259 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require 'rubocop/rake_task' 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %i[spec rubocop] 13 | 14 | Rake::Task.define_task :update_loader do 15 | system('bin/update-loader') || raise('Updating loader failed') 16 | end 17 | 18 | Rake::Task.define_task :remove_loader do 19 | File.unlink('lib/basic_loader.rb') 20 | end 21 | 22 | Rake::Task[:build].enhance [:update_loader] 23 | Rake::Task[:build].enhance do 24 | Rake::Task[:remove_loader].execute 25 | end 26 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'crypt_reboot' 6 | require 'irb' 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /bin/update-loader: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Update basic_loader file 5 | class BasicUpdater 6 | def call(loader_path) 7 | create loader_path 8 | 9 | loader_dir = File.dirname(loader_path) 10 | 11 | loader.on_load do |_cpath, _value, abspath| 12 | next if abspath !~ /.rb$/i # skip directories 13 | 14 | arg = relative_path_from(loader_dir, abspath).sub(/.rb$/i, '') 15 | append loader_path, %(require '#{arg}') 16 | end 17 | 18 | loader.eager_load 19 | end 20 | 21 | private 22 | 23 | def append(file, line) 24 | File.open(file, 'a') { |f| f.puts line } 25 | end 26 | 27 | def create(file) 28 | File.open(file, 'w') do |f| 29 | f.puts '# frozen_string_literal: true' 30 | f.puts 31 | f.puts '# File generated automatically, do not edit' 32 | f.puts 33 | f.puts "require 'crypt_reboot/version'" # not loaded automatically 34 | end 35 | end 36 | 37 | def relative_path_from(base_dir, target_path) 38 | target = Pathname.new(target_path) 39 | base = Pathname.new(base_dir) 40 | target.relative_path_from(base).to_s 41 | end 42 | 43 | attr_reader :loader 44 | 45 | def initialize(loader) 46 | @loader = loader 47 | end 48 | end 49 | 50 | require 'bundler/setup' 51 | require 'pathname' 52 | require 'fileutils' 53 | 54 | loader_path = File.join(__dir__, '..', 'lib', 'basic_loader.rb') 55 | FileUtils.rm_f(loader_path) 56 | 57 | # Has to be invoked after the loader file was deleted 58 | require 'crypt_reboot' 59 | 60 | BasicUpdater.new(CryptReboot::CODE_LOADER).call(loader_path) 61 | 62 | puts 'Loader updated' 63 | -------------------------------------------------------------------------------- /bin/vscode_rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # To force VS Code to use rake task for running tests use following as an "RSpec Command": 5 | # bin/vscode_rspec 6 | 7 | opts = ARGV.join(" ") 8 | system %{SPEC_OPTS="#{opts}" bundle exec rake spec} 9 | -------------------------------------------------------------------------------- /crypt_reboot.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/crypt_reboot/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'crypt_reboot' 7 | spec.version = CryptReboot::VERSION 8 | spec.authors = ['Paweł Pokrywka'] 9 | spec.email = ['pepawel@users.noreply.github.com'] 10 | 11 | spec.summary = 'Linux utility for automatic and secure unlocking of encrypted disks on reboot' 12 | spec.homepage = 'https://phantomno.de/cryptreboot' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = '>= 2.7.0' # default version shipped with Ubuntu 20.04.5 LTS 15 | 16 | spec.metadata['homepage_uri'] = spec.homepage 17 | spec.metadata['source_code_uri'] = 'https://github.com/phantom-node/cryptreboot' 18 | spec.metadata['changelog_uri'] = 'https://github.com/phantom-node/cryptreboot/blob/master/CHANGELOG.md' 19 | spec.metadata['rubygems_mfa_required'] = 'true' 20 | 21 | spec.files = ['CHANGELOG.md', 'LICENSE.txt', 'README.md', 'lib/basic_loader.rb'] 22 | spec.files += Dir.chdir(__dir__) do 23 | `git ls-files -z`.split("\x0").select do |f| 24 | f.match(%r{\A(?:lib|exe)/}) 25 | end 26 | end 27 | 28 | spec.bindir = 'exe' 29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | spec.add_dependency 'tty-command', '~> 0.10' 33 | spec.add_dependency 'tty-option', '~> 0.3' 34 | spec.add_dependency 'memory_locker', '~> 1.0.3' 35 | end 36 | -------------------------------------------------------------------------------- /exe/cryptreboot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'crypt_reboot' 6 | 7 | executor = CryptReboot::Cli::ParamsParsingExecutor.new 8 | result = executor.call(ARGV) 9 | exit result.call 10 | -------------------------------------------------------------------------------- /lib/crypt_reboot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'basic_loader' 5 | rescue LoadError => e 6 | raise if e.path != 'basic_loader' 7 | 8 | require 'zeitwerk' 9 | loader = Zeitwerk::Loader.for_gem 10 | loader.setup 11 | end 12 | 13 | # Main module 14 | module CryptReboot 15 | end 16 | 17 | CryptReboot.const_set(:CODE_LOADER, loader) 18 | -------------------------------------------------------------------------------- /lib/crypt_reboot/boot_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Data required for booting 5 | class BootConfig 6 | attr_reader :kernel, :initramfs, :cmdline 7 | 8 | def ==(other) 9 | kernel == other.kernel && initramfs == other.initramfs && cmdline == other.cmdline 10 | end 11 | 12 | def with_initramfs(new_initramfs) 13 | self.class.new(kernel: kernel, initramfs: new_initramfs, cmdline: cmdline) 14 | end 15 | 16 | private 17 | 18 | def initialize(kernel:, initramfs: nil, cmdline: nil) 19 | @kernel = kernel 20 | @initramfs = initramfs 21 | @cmdline = cmdline 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Command Line Interface 5 | module Cli 6 | PROGRAM_NAME = 'cryptreboot' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/exiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | # Print a message and return exit status 6 | class Exiter 7 | attr_reader :text 8 | 9 | def call 10 | stream.puts text.strip if text 11 | status 12 | end 13 | 14 | private 15 | 16 | attr_reader :status, :stream 17 | 18 | def initialize(text, status:, stream:) 19 | @text = text 20 | @status = status 21 | @stream = stream 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/happy_exiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | # Exit with a success message 6 | class HappyExiter < Exiter 7 | private 8 | 9 | def initialize(text) 10 | super(text, status: 0, stream: $stdout) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/params/definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tty-option' 4 | 5 | module CryptReboot 6 | module Cli 7 | module Params 8 | # Definition of options, flags, help and other CLI related things 9 | class Definition 10 | include TTY::Option 11 | 12 | # rubocop:disable Metrics/BlockLength 13 | usage do 14 | header 'Reboot for Linux systems with encrypted root partition.' 15 | 16 | program PROGRAM_NAME 17 | no_command 18 | 19 | desc 'It asks for a password and reboots the system afterward, automatically unlocking ' \ 20 | 'the drive on startup using in-memory initramfs patching and kexec. ' \ 21 | 'Without explicit consent, no secrets are stored on disk, even temporarily.', 22 | '', 23 | 'Useful when unlocking the drive at startup is difficult, such as on headless and remote systems.', 24 | '', 25 | "By default, it uses the current kernel command line, \"#{Config.kernel}\" as " \ 26 | "kernel and \"#{Config.initramfs}\" as initramfs.", 27 | '', 28 | 'It performs operations normally only available to the root user, so it is suggested to use ' \ 29 | 'sudo or a similar utility.' 30 | 31 | example 'Normal usage:', 32 | "$ sudo #{PROGRAM_NAME}" 33 | example 'Reboot into custom kernel:', 34 | "$ sudo #{PROGRAM_NAME} --kernel /boot/vmlinuz.old --initramfs /boot/initrd.old" 35 | example 'Specify custom kernel options:', 36 | "$ sudo #{PROGRAM_NAME} --cmdline \"root=UUID=d0...a2 ro nomodeset acpi=off\"" 37 | example 'Prepare to reboot and perform it manually later:', 38 | "$ sudo #{PROGRAM_NAME} --prepare-only", 39 | '$ sleep 3600 # do anything else in-between', 40 | '$ sudo reboot --no-wall --no-wtmp' 41 | 42 | footer 'To report a bug, get support or contribute, please visit the project page:', 43 | 'https://phantomno.de/cryptreboot', 44 | '', 45 | "Thank you for using #{PROGRAM_NAME}. Happy rebooting!" 46 | end 47 | # rubocop:enable Metrics/BlockLength 48 | 49 | option :kernel do 50 | long '--kernel path' 51 | desc 'Path to the kernel you want to reboot into' 52 | default Config.kernel 53 | end 54 | 55 | option :initramfs do 56 | long '--initramfs path' 57 | desc 'Path to the initramfs to be used by loaded kernel' 58 | default Config.initramfs 59 | end 60 | 61 | option :cmdline do 62 | long '--cmdline string' 63 | desc 'Command line for loaded kernel; current command line is used if not provided' 64 | end 65 | 66 | option :paths do 67 | arity :any 68 | long '--tool name:path' 69 | desc 'Path to given external tool specified by "name". By default, tools are searched in the PATH. ' \ 70 | 'If you want to specify paths for more than 1 tool, use this option multiple times. Tool names: ' \ 71 | 'cat, cpio, cryptsetup, grep, kexec, mount, reboot, strace, umount, unmkinitramfs' 72 | convert :map 73 | end 74 | 75 | # Flags 76 | 77 | flag :prepare_only do 78 | short '-p' 79 | long '--prepare-only' 80 | desc 'Load kernel and initramfs, but do not reboot' 81 | end 82 | 83 | flag :skip_lz4_check do 84 | long '--skip-lz4-check' 85 | desc 'Do not check if initramfs is compressed with LZ4 algorithm. ' \ 86 | 'If you use different compression and specify this flag, it will make ' \ 87 | 'initramfs extraction much faster. But if your initramfs uses ' \ 88 | 'LZ4 you risk you will need to manually unlock your disk on startup. ' \ 89 | 'See the README file to learn how to change the compression ' \ 90 | 'algorithm to a more robust one.' 91 | end 92 | 93 | flag :insecure_memory do 94 | short '-s' 95 | long '--insecure-memory' 96 | desc 'Do not lock memory. WARNING: there is a risk your secrets will leak to swap.' 97 | end 98 | 99 | flag :debug do 100 | short '-d' 101 | long '--debug' 102 | desc 'Print debug messages. WARNING: they may contain secrets, ' \ 103 | 'please exercise caution when sharing them.' 104 | end 105 | 106 | option :patch_save_path do 107 | long '--save-patch path' 108 | desc 'Save initramfs patch to file for debug purposes. ' \ 109 | 'WARNING: it contains encryption keys, you are responsible for ' \ 110 | 'their safe disposal, which may be difficult after the file comes ' \ 111 | 'into contact with the disk. Deleting the file alone may not be enough.' 112 | end 113 | 114 | flag :version do 115 | short '-v' 116 | long '--version' 117 | desc 'Print version and exit' 118 | end 119 | 120 | flag :help do 121 | short '-h' 122 | long '--help' 123 | desc 'Print this usage and exit' 124 | end 125 | end 126 | 127 | # This class contains code from external source, 128 | # do not expose it anywhere outside of the module 129 | private_constant :Definition 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/params/flattener.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | # Replace given key in params with new keys obtained from its contents with suffixes added 7 | class Flattener 8 | def call(params) 9 | paths = params.fetch(key, {}).transform_keys { |k| :"#{k}#{suffix}" } 10 | params.reject { |k, _| k == key }.merge(paths) 11 | end 12 | 13 | private 14 | 15 | attr_reader :key, :suffix 16 | 17 | def initialize(key:, suffix:) 18 | @key = key.to_sym 19 | @suffix = suffix 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/params/help_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | # Returns usage 7 | class HelpGenerator 8 | def call 9 | definition.help(order: ->(params) { params }) 10 | end 11 | 12 | private 13 | 14 | attr_reader :definition 15 | 16 | def initialize(definition: Definition.new) 17 | @definition = definition 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/params/parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | # Parse given ARGV and return params hash or raise exception with error summary 7 | class Parser 8 | ParseError = Class.new StandardError 9 | 10 | def call(raw_params) 11 | params = definition.parse(raw_params).params 12 | raise ParseError, params.errors.summary unless params.valid? 13 | 14 | flattener.call params.to_h 15 | end 16 | 17 | private 18 | 19 | attr_reader :definition, :flattener 20 | 21 | def initialize(definition: Definition.new, 22 | flattener: Flattener.new(key: 'paths', suffix: '_path')) 23 | @definition = definition 24 | @flattener = flattener 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/params_parsing_executor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | # Interprets parameters, executes everything and returns callable object 6 | class ParamsParsingExecutor 7 | def call(raw_params) 8 | params = parser.call(raw_params) 9 | handle_action_params!(params) or configure_and_exec(params) 10 | rescue StandardError, Interrupt => e 11 | raise if debug? 12 | 13 | sad_exiter_class.new(error_message(e)) 14 | end 15 | 16 | private 17 | 18 | def debug? 19 | debug_checker.call 20 | end 21 | 22 | def configure_and_exec(params) 23 | config_updater.call(**params) 24 | locker.call 25 | loader.call 26 | rebooter 27 | end 28 | 29 | def handle_action_params!(params) 30 | return happy_exiter_class.new(help_generator.call) if params[:help] 31 | return happy_exiter_class.new(version_string) if params[:version] 32 | 33 | params.reject! { |param_name, _| %i[help version].include? param_name } 34 | 35 | false 36 | end 37 | 38 | def exception_name(exception) 39 | name = exception.class.name.split('::').last 40 | name.gsub(/([a-z\d])([A-Z])/, '\1 \2').capitalize 41 | end 42 | 43 | def error_message(exception) 44 | "#{exception_name(exception)}: #{exception.message}" 45 | end 46 | 47 | attr_reader :parser, :config_updater, :loader, :help_generator, 48 | :version_string, :debug_checker, :rebooter, 49 | :happy_exiter_class, :sad_exiter_class, :locker 50 | 51 | # rubocop:disable Metrics/ParameterLists 52 | def initialize(parser: Params::Parser.new, 53 | config_updater: Config.method(:update!), 54 | loader: KexecPatchingLoader.new, 55 | help_generator: Params::HelpGenerator.new, 56 | version_string: "#{PROGRAM_NAME} #{VERSION}", 57 | debug_checker: LazyConfig.debug, 58 | rebooter: Rebooter.new, 59 | happy_exiter_class: HappyExiter, 60 | sad_exiter_class: SadExiter, 61 | locker: ElasticMemoryLocker.new) 62 | @parser = parser 63 | @config_updater = config_updater 64 | @loader = loader 65 | @help_generator = help_generator 66 | @version_string = version_string 67 | @debug_checker = debug_checker 68 | @rebooter = rebooter 69 | @happy_exiter_class = happy_exiter_class 70 | @sad_exiter_class = sad_exiter_class 71 | @locker = locker 72 | end 73 | # rubocop:enable Metrics/ParameterLists 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/crypt_reboot/cli/sad_exiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | # Exit with error message 6 | class SadExiter < Exiter 7 | private 8 | 9 | def initialize(text) 10 | super(text, status: 1, stream: $stderr) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/crypt_reboot/concatenator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Concatenate files into one 5 | class Concatenator 6 | def call(*files, to:) 7 | runner.call(tool, *files, output_file: to) 8 | end 9 | 10 | private 11 | 12 | def tool 13 | lazy_tool.call 14 | end 15 | 16 | attr_reader :lazy_tool, :runner 17 | 18 | def initialize(lazy_tool: LazyConfig.cat_path, 19 | runner: Runner::NoResult.new) 20 | @lazy_tool = lazy_tool 21 | @runner = runner 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/crypt_reboot/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | 5 | module CryptReboot 6 | # Global configuration singleton 7 | class Config < InstantiableConfig 8 | include Singleton 9 | 10 | class << self 11 | def method_missing(method_name, *args, **kwargs, &block) 12 | instance.respond_to?(method_name) ? instance.send(method_name, *args, **kwargs, &block) : super 13 | end 14 | 15 | def respond_to_missing?(name, *_, **_) 16 | instance.respond_to?(name) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/deserializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Load crypttab file and return array with deserialized entries 6 | class Deserializer 7 | def call(filename = nil, content: File.read(filename)) 8 | split_to_important_lines(content).map do |line| 9 | entry_deserializer.call line 10 | end 11 | end 12 | 13 | private 14 | 15 | def split_to_important_lines(content) 16 | content.split(/\n+|\r+/) 17 | .reject(&:empty?) 18 | .reject { |line| line.start_with? '#' } 19 | end 20 | 21 | attr_reader :entry_deserializer 22 | 23 | def initialize(entry_deserializer: EntryDeserializer.new) 24 | @entry_deserializer = entry_deserializer 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Value-object describing entry in crypttab file 6 | class Entry 7 | attr_reader :target, :source, :key_file, :options, :flags 8 | 9 | def ==(other) 10 | target == other.target && source == other.source && key_file == other.key_file && 11 | options == other.options && flags.sort == other.flags.sort 12 | end 13 | 14 | def headevice(header_prefix: nil) 15 | if header_prefix && header_path 16 | File.join(header_prefix, header_path) 17 | elsif header_path 18 | header_path 19 | else 20 | source 21 | end 22 | end 23 | 24 | private 25 | 26 | def header_path 27 | options[:header] 28 | end 29 | 30 | def initialize(target:, source:, key_file:, options:, flags:) 31 | @target = target 32 | @source = source 33 | @key_file = key_file 34 | @options = options 35 | @flags = flags 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/entry_deserializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Deserialize crypttab line into value object 6 | class EntryDeserializer 7 | InvalidFormat = Class.new StandardError 8 | 9 | def call(line) 10 | target, source, key_file, raw_floptions = columns = line.split 11 | raise InvalidFormat if columns.size < 3 12 | 13 | floptions = raw_floptions.to_s.split(',') 14 | flags = extract_flags(floptions) 15 | options = extract_options(floptions) 16 | entry_class.new( 17 | target: target, source: source, key_file: key_file, 18 | options: options, flags: flags 19 | ) 20 | end 21 | 22 | private 23 | 24 | def extract_flags(floptions) 25 | floptions.reject do |floption| 26 | floption.include?('=') 27 | end.map(&:to_sym) 28 | end 29 | 30 | def extract_options(floptions) 31 | options = floptions.select do |floption| 32 | floption.include?('=') 33 | end 34 | options.to_h do |option| 35 | parse_option(option) 36 | end 37 | end 38 | 39 | def parse_option(option) 40 | name, value = option.split('=') 41 | [name.to_sym, value.to_i.to_s == value ? value.to_i : value] 42 | end 43 | 44 | attr_reader :entry_class 45 | 46 | def initialize(entry_class: Entry) 47 | @entry_class = entry_class 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/entry_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Serialize crypttab entry into one line of text 6 | class EntrySerializer 7 | def call(entry) 8 | floptions = (entry.flags + serialize_options(entry.options)).join(',') 9 | [entry.target, entry.source, entry.key_file, floptions].join(' ') 10 | end 11 | 12 | private 13 | 14 | def serialize_options(options) 15 | options.map do |option, value| 16 | [option, value].join('=') 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/keyfile_locator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Return path of keyfile for given target 6 | class KeyfileLocator 7 | def call(target) 8 | File.join(base_dir, target + extension) 9 | end 10 | 11 | private 12 | 13 | attr_reader :base_dir, :extension 14 | 15 | def initialize(base_dir: '/cryptreboot', extension: '.key') 16 | @base_dir = base_dir 17 | @extension = extension 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/luks_to_plain_converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Convert given crypttab entry from LUKS to plain mode 6 | class LuksToPlainConverter 7 | def call(entry, data, keyfile) 8 | entry_class.new( 9 | target: entry.target, 10 | source: entry.source, 11 | key_file: keyfile, 12 | options: convert_options(entry.options, data), 13 | flags: entry.flags - [:luks] + [:plain] 14 | ) 15 | end 16 | 17 | private 18 | 19 | # According to cryptsetup manual, offset is specified in 512-byte sectors. 20 | # Therefore sector size of the actual device shouldn't be used. 21 | OFFSET_SECTOR_SIZE = 512 22 | private_constant :OFFSET_SECTOR_SIZE 23 | 24 | def convert_options(options, data) 25 | options = options.reject do |option, _| 26 | %i[keyfile-size keyslot key-slot header keyscript].include? option 27 | end 28 | options[:'sector-size'] ||= data.sector_size # allow user to set it explicitly 29 | options.merge({ cipher: data.cipher, size: data.key_bits, offset: data.offset / OFFSET_SECTOR_SIZE }) 30 | end 31 | 32 | attr_reader :entry_class 33 | 34 | def initialize(entry_class: Entry) 35 | @entry_class = entry_class 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Serialize entries and return crypttab file content as a string 6 | class Serializer 7 | def call(entries) 8 | body = serialize(entries).join("\n") 9 | "#{header}\n#{body}\n" 10 | end 11 | 12 | private 13 | 14 | def serialize(entries) 15 | entries.map do |entry| 16 | entry_serializer.call(entry) 17 | end 18 | end 19 | 20 | attr_reader :entry_serializer, :header 21 | 22 | def initialize(entry_serializer: EntrySerializer.new, 23 | header: '# This file has been patched by cryptreboot') 24 | @entry_serializer = entry_serializer 25 | @header = header 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/crypt_reboot/crypt_tab/zfs_keystore_entries_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | # Get a list of keystore zvols from a running system and return entries array 6 | class ZfsKeystoreEntriesGenerator 7 | def call 8 | glob = File.join(zvol_dir, '**/*') 9 | Dir.glob(glob) 10 | .select { |path| path =~ %r{/keystore$} && exist?(path) } 11 | .map { |path| generate_entry(path) } 12 | end 13 | 14 | private 15 | 16 | def exist?(path) 17 | File.exist? File.realpath(path) 18 | end 19 | 20 | def generate_entry(path) 21 | pool = File.basename File.dirname(path) 22 | # Target name is the same as produced by /scripts/zfs script within initramfs 23 | entry_builder.call target: "keystore-#{pool}", source: path 24 | end 25 | 26 | attr_reader :zvol_dir, :entry_builder 27 | 28 | def initialize(zvol_dir: '/dev/zvol', 29 | entry_builder: lambda { |target:, source:| 30 | # Flags and options are the same as produced by /scripts/zfs script within initramfs 31 | Entry.new target: target, 32 | source: source, 33 | key_file: 'none', 34 | options: {}, 35 | flags: %i[luks discard] 36 | }) 37 | @zvol_dir = zvol_dir 38 | @entry_builder = entry_builder 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/crypt_reboot/elastic_memory_locker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'memory_locker' unless defined? MemoryLocker # MemoryLocker is mocked in tests 4 | 5 | module CryptReboot 6 | # Try to lock memory if configuration allows it 7 | class ElasticMemoryLocker 8 | LockingError = Class.new StandardError 9 | 10 | def call 11 | return if skip_locking? 12 | 13 | locker.call 14 | nil 15 | rescue locking_error => e 16 | raise LockingError, 'Failed to lock memory', cause: e 17 | end 18 | 19 | private 20 | 21 | def skip_locking? 22 | insecure_memory_checker.call 23 | end 24 | 25 | attr_reader :insecure_memory_checker, :locker, :locking_error 26 | 27 | def initialize(insecure_memory_checker: LazyConfig.insecure_memory, 28 | locker: MemoryLocker, 29 | locking_error: MemoryLocker::Error) 30 | @insecure_memory_checker = insecure_memory_checker 31 | @locker = locker 32 | @locking_error = locking_error 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/crypt_reboot/files_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Generate a hash with file names as keys and file contents as values 5 | class FilesGenerator 6 | def call(entries, base_dir:, crypttab_path:) 7 | files = {} 8 | modified_entries = entries.map do |entry| 9 | next entry unless luks?(entry, base_dir) 10 | 11 | data = fetch_data(entry, base_dir) 12 | keyfile = keyfile_locator.call(entry.target) 13 | files[keyfile] = data.key 14 | entry_converter.call(entry, data, keyfile) 15 | end 16 | files.merge(crypttab_path => serializer.call(modified_entries)) 17 | end 18 | 19 | private 20 | 21 | def luks?(entry, base_dir) 22 | headevice = entry.headevice(header_prefix: base_dir) 23 | luks_checker.call(headevice) 24 | end 25 | 26 | def fetch_data(entry, base_dir) 27 | headevice = entry.headevice(header_prefix: base_dir) 28 | luks_data_fetcher.call(headevice, entry.target) 29 | end 30 | 31 | attr_reader :keyfile_locator, :entry_converter, :serializer, 32 | :luks_data_fetcher, :luks_checker 33 | 34 | def initialize(keyfile_locator: CryptTab::KeyfileLocator.new, 35 | entry_converter: CryptTab::LuksToPlainConverter.new, 36 | serializer: CryptTab::Serializer.new, 37 | luks_data_fetcher: Luks::DataFetcher.new, 38 | luks_checker: Luks::Checker.new) 39 | @keyfile_locator = keyfile_locator 40 | @entry_converter = entry_converter 41 | @serializer = serializer 42 | @luks_data_fetcher = luks_data_fetcher 43 | @luks_checker = luks_checker 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/crypt_reboot/files_writer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | module CryptReboot 6 | # Writes files from hash to specified directory 7 | class FilesWriter 8 | def call(files, target_dir) 9 | files.each do |relative_path, content| 10 | path = File.join(target_dir, relative_path) 11 | dir = File.dirname(path) 12 | FileUtils.mkdir_p(dir) unless File.directory?(dir) 13 | File.binwrite(path, content) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/crypt_reboot/gziper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'zlib' 4 | 5 | module CryptReboot 6 | # Gzip data and save it to file 7 | class Gziper 8 | def call(archive_path, data) 9 | writer.call(archive_path) do |gz| 10 | gz.write data 11 | end 12 | end 13 | 14 | private 15 | 16 | attr_reader :writer 17 | 18 | def initialize(writer: Zlib::GzipWriter.method(:open)) 19 | @writer = writer 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/archiver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'find' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | # Create compressed CPIO archive from files in a given directory 8 | class Archiver 9 | def call(dir, archive) 10 | Dir.chdir(dir) do 11 | uncompressed = runner.call(cpio, '-oH', 'newc', '--reproducible', input: finder.call) 12 | gziper.call(archive, uncompressed) 13 | end 14 | end 15 | 16 | private 17 | 18 | def cpio 19 | lazy_cpio.call 20 | end 21 | 22 | attr_reader :runner, :finder, :lazy_cpio, :gziper 23 | 24 | def initialize(runner: Runner::Binary.new, 25 | finder: -> { Find.find('.').to_a.join("\n") }, 26 | lazy_cpio: LazyConfig.cpio_path, 27 | gziper: Gziper.new) 28 | @runner = runner 29 | @finder = finder 30 | @lazy_cpio = lazy_cpio 31 | @gziper = gziper 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/decompressor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | # Instantiates appropriate decompressor 6 | class Decompressor 7 | def call(skip_lz4_check: Config.skip_lz4_check) 8 | skip_lz4_check ? TolerantDecompressor.new : IntolerantDecompressor.new 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/decompressor/intolerant_decompressor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shellwords' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | # Extract initramfs in strace to check if compression is supported 8 | class Decompressor 9 | class IntolerantDecompressor 10 | Lz4NotAllowed = Class.new StandardError 11 | 12 | def call(filename, dir) 13 | command_line = prepare_command_line(filename, dir) 14 | lines = runner.call(command_line) 15 | raise_exception if intolerable_tool_used?(lines) 16 | end 17 | 18 | private 19 | 20 | def prepare_command_line(filename, dir) 21 | options = '-f --trace=execve -z -qq --signal=\!all' 22 | args = "#{filename.shellescape} #{dir.shellescape}" 23 | strace_command_line = "#{strace.shellescape} #{options} #{unmkinitramfs.shellescape} #{args}" 24 | grep_command_line = "#{grep.shellescape} --line-buffered lz4" 25 | # guarantee at least 1 line of grep output, otherwise grep will return non-zero status 26 | grep_fixer = 'echo lz4 \"-t\"' 27 | "(#{grep_fixer}; #{strace_command_line}) 2>&1 | #{grep_command_line}" 28 | end 29 | 30 | def intolerable_tool_used?(lines) 31 | !!lines.find { |line| line !~ /"-t"/ && line !~ /"--test"/ } 32 | end 33 | 34 | def raise_exception 35 | raise Lz4NotAllowed, 'LZ4 compression is not allowed, change the compression ' \ 36 | 'algorithm in initramfs.conf and regenerate the initramfs image' 37 | end 38 | 39 | def unmkinitramfs 40 | lazy_unmkinitramfs.call 41 | end 42 | 43 | def strace 44 | lazy_strace.call 45 | end 46 | 47 | def grep 48 | lazy_grep.call 49 | end 50 | 51 | attr_reader :lazy_unmkinitramfs, :lazy_strace, :lazy_grep, :runner 52 | 53 | def initialize(lazy_unmkinitramfs: LazyConfig.unmkinitramfs_path, 54 | lazy_strace: LazyConfig.strace_path, 55 | lazy_grep: LazyConfig.grep_path, 56 | runner: Runner::Lines.new) 57 | @lazy_unmkinitramfs = lazy_unmkinitramfs 58 | @lazy_strace = lazy_strace 59 | @lazy_grep = lazy_grep 60 | @runner = runner 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/decompressor/tolerant_decompressor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | class Decompressor 6 | # Simply extract initramfs 7 | class TolerantDecompressor 8 | def call(filename, dir) 9 | runner.call(unmkinitramfs, filename, dir) 10 | end 11 | 12 | private 13 | 14 | attr_reader :lazy_unmkinitramfs, :runner 15 | 16 | def unmkinitramfs 17 | lazy_unmkinitramfs.call 18 | end 19 | 20 | def initialize(lazy_unmkinitramfs: LazyConfig.unmkinitramfs_path, 21 | runner: Runner::NoResult.new) 22 | @lazy_unmkinitramfs = lazy_unmkinitramfs 23 | @runner = runner 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/extractor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | # Create temporary directory, extract initramfs there and yield, cleaning afterwards 8 | class Extractor 9 | def call(filename) 10 | tmp_maker.call do |dir| 11 | logger.call message 12 | decompressor.call(filename, dir) 13 | yield File.join(dir, subdir) 14 | end 15 | end 16 | 17 | private 18 | 19 | def decompressor 20 | decompressor_factory.call 21 | end 22 | 23 | attr_reader :tmp_maker, :decompressor_factory, :message, :logger, :subdir 24 | 25 | def initialize(tmp_maker: Dir.method(:mktmpdir), 26 | decompressor_factory: Decompressor.new, 27 | message: 'Extracting initramfs... To speed things up, future versions will employ cache.', 28 | logger: ->(msg) { warn msg }, 29 | subdir: 'main') 30 | @tmp_maker = tmp_maker 31 | @decompressor_factory = decompressor_factory 32 | @message = message 33 | @logger = logger 34 | @subdir = subdir 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs/patcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | # Yield path to initramfs patched with files_spec. 8 | # Patched initramfs will be removed afterwards if user doesn't want to save it 9 | class Patcher 10 | def call(initramfs_path, files_spec) 11 | temp_provider.call do |base_dir| 12 | files_dir, patch_path, patched_path = prefix('files', 'patch', 'result', with: base_dir) 13 | writer.call(files_spec, files_dir) 14 | archiver.call(files_dir, patch_path) 15 | saver.call(patch_path) 16 | concatenator.call(initramfs_path, patch_path, to: patched_path) 17 | yield patched_path 18 | end 19 | end 20 | 21 | private 22 | 23 | def prefix(*file_names, with:) 24 | file_names.map do |file_name| 25 | File.join(with, file_name) 26 | end 27 | end 28 | 29 | attr_reader :temp_provider, :writer, :archiver, :concatenator, :saver 30 | 31 | def initialize(temp_provider: SafeTemp::Directory.new, 32 | writer: FilesWriter.new, 33 | archiver: Archiver.new, 34 | concatenator: Concatenator.new, 35 | saver: lambda { |file| 36 | dir = Config.patch_save_path 37 | FileUtils.cp(file, dir) if dir 38 | }) 39 | @temp_provider = temp_provider 40 | @writer = writer 41 | @archiver = archiver 42 | @concatenator = concatenator 43 | @saver = saver 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/crypt_reboot/initramfs_patch_squeezer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Transform initramfs image into a patch (files hash) 5 | class InitramfsPatchSqueezer 6 | def call(initramfs_path) 7 | extractor.call(initramfs_path) do |tmp_dir| 8 | patchers.inject({}) do |files, patcher| 9 | files.merge patcher.call(tmp_dir) 10 | end 11 | end 12 | end 13 | 14 | private 15 | 16 | attr_reader :extractor, :patchers 17 | 18 | def initialize(extractor: Initramfs::Extractor.new, 19 | patchers: [ 20 | LuksCryptTabPatcher.new, 21 | ZfsKeystorePatcher.new 22 | ]) 23 | @extractor = extractor 24 | @patchers = patchers 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/crypt_reboot/instantiable_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Configuration object 5 | class InstantiableConfig 6 | UnrecognizedSetting = Class.new StandardError 7 | 8 | attr_reader :initramfs, :cmdline, :kernel, :patch_save_path, :cat_path, :cpio_path, 9 | :unmkinitramfs_path, :kexec_path, :cryptsetup_path, :reboot_path, 10 | :mount_path, :umount_path, :strace_path, :grep_path, 11 | :debug, :prepare_only, :skip_lz4_check, :insecure_memory 12 | 13 | def update!(**settings) 14 | settings.each do |name, value| 15 | set!(name, value) 16 | end 17 | end 18 | 19 | private 20 | 21 | def set!(name, value) 22 | raise UnrecognizedSetting, "Unrecognized setting `#{name}`" unless instance_variable_defined?(:"@#{name}") 23 | 24 | instance_variable_set(:"@#{name}", value) 25 | end 26 | 27 | # rubocop:disable Metrics/MethodLength 28 | # rubocop:disable Metrics/AbcSize 29 | def initialize 30 | # Options 31 | @initramfs = '/boot/initrd.img' 32 | @cmdline = nil 33 | @kernel = '/boot/vmlinuz' 34 | @patch_save_path = nil 35 | @cat_path = 'cat' 36 | @cpio_path = 'cpio' 37 | @unmkinitramfs_path = 'unmkinitramfs' 38 | @kexec_path = 'kexec' 39 | @cryptsetup_path = 'cryptsetup' 40 | @reboot_path = 'reboot' 41 | @mount_path = 'mount' 42 | @umount_path = 'umount' 43 | @strace_path = 'strace' 44 | @grep_path = 'grep' 45 | 46 | # Flags 47 | @debug = false 48 | @prepare_only = false 49 | @skip_lz4_check = false 50 | @insecure_memory = false 51 | end 52 | # rubocop:enable Metrics/AbcSize 53 | # rubocop:enable Metrics/MethodLength 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/crypt_reboot/kexec/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Kexec 5 | # Load new kernel and initramfs into memory, making then ready for later execution 6 | class Loader 7 | def call(boot_config) 8 | args = [tool, '-al', boot_config.kernel] 9 | args += ['--initrd', boot_config.initramfs] if boot_config.initramfs 10 | args += boot_config.cmdline ? ['--append', boot_config.cmdline] : ['--reuse-cmdline'] 11 | 12 | runner.call(*args) 13 | end 14 | 15 | private 16 | 17 | def tool 18 | lazy_tool.call 19 | end 20 | 21 | attr_reader :lazy_tool, :runner 22 | 23 | def initialize(lazy_tool: LazyConfig.kexec_path, 24 | runner: Runner::NoResult.new) 25 | @lazy_tool = lazy_tool 26 | @runner = runner 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/crypt_reboot/kexec_patching_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Patch initramfs and load it along with kernel using kexec, 5 | # so it is ready to be executed. 6 | class KexecPatchingLoader 7 | def call(boot_config = BootConfig.new( 8 | kernel: Config.kernel, 9 | initramfs: Config.initramfs, 10 | cmdline: Config.cmdline 11 | )) 12 | generator.call(boot_config.initramfs) do |patched_initramfs| 13 | patched_boot_config = boot_config.with_initramfs(patched_initramfs) 14 | loader.call(patched_boot_config) 15 | end 16 | end 17 | 18 | private 19 | 20 | attr_reader :generator, :loader 21 | 22 | def initialize(generator: PatchedInitramfsGenerator.new, 23 | loader: Kexec::Loader.new) 24 | @generator = generator 25 | @loader = loader 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/crypt_reboot/lazy_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Return getter lambdas instead of configuration settings directly 5 | class LazyConfig 6 | class << self 7 | def method_missing(method_name, *args, **kwargs, &block) 8 | return super unless instance.respond_to?(method_name) 9 | 10 | -> { instance.send(method_name, *args, **kwargs, &block) } 11 | end 12 | 13 | def respond_to_missing?(name, *_, **_) 14 | instance.respond_to?(name) 15 | end 16 | 17 | def instance 18 | Config.instance 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Return true in case given device is LUKS (of given version is provided), false otherwise 6 | class Checker 7 | def call(headevice, version = :any) 8 | args = version == :any ? [] : ['--type', version] 9 | runner.call(binary, 'isLuks', 'none', '--header', headevice, *args) 10 | end 11 | 12 | private 13 | 14 | def binary 15 | lazy_binary.call 16 | end 17 | 18 | attr_reader :lazy_binary, :runner 19 | 20 | def initialize(lazy_binary: LazyConfig.cryptsetup_path, 21 | runner: Runner::Boolean.new) 22 | @lazy_binary = lazy_binary 23 | @runner = runner 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Value-object with encryption parameters 6 | class Data 7 | attr_reader :cipher, :offset, :sector_size, :key 8 | 9 | def ==(other) 10 | cipher == other.cipher && offset == other.offset && 11 | sector_size == other.sector_size && key == other.key 12 | end 13 | 14 | def key_bits 15 | key.bytesize * 8 16 | end 17 | 18 | def with_key(new_key) 19 | self.class.new( 20 | cipher: cipher, 21 | offset: offset, 22 | sector_size: sector_size, 23 | key: new_key 24 | ) 25 | end 26 | 27 | private 28 | 29 | def initialize(cipher:, offset:, sector_size:, key: '') 30 | @cipher = cipher 31 | @offset = offset 32 | @sector_size = sector_size 33 | @key = key 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/data_fetcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Fetch LUKS data including key (user will be asked for passphrase) 6 | class DataFetcher 7 | def call(headevice, target) 8 | version = detector.call(headevice) 9 | data = dumper.call(headevice, version) 10 | pass = asker.call("Please unlock disk #{target}: ") 11 | key = key_fetcher.call(headevice, pass) 12 | data.with_key(key) 13 | end 14 | 15 | private 16 | 17 | attr_reader :detector, :dumper, :asker, :key_fetcher 18 | 19 | def initialize(detector: VersionDetector.new, 20 | dumper: Dumper.new, 21 | asker: PassphraseAsker.new, 22 | key_fetcher: KeyFetcher.new) 23 | @detector = detector 24 | @dumper = dumper 25 | @asker = asker 26 | @key_fetcher = key_fetcher 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/dumper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Depending on LUKS version, delegates parsing to different parser 6 | class Dumper 7 | def call(headevice, version) 8 | dump = runner.call(binary, 'luksDump', 'none', '--header', headevice) 9 | parser = parsers.fetch(version) 10 | parser.call(dump) 11 | end 12 | 13 | private 14 | 15 | def binary 16 | lazy_binary.call 17 | end 18 | 19 | attr_reader :lazy_binary, :runner, :parsers 20 | 21 | def initialize(lazy_binary: LazyConfig.cryptsetup_path, 22 | runner: Runner::Lines.new, 23 | parsers: { 'LUKS2' => LuksV2Parser.new, 'LUKS1' => LuksV1Parser.new }) 24 | @lazy_binary = lazy_binary 25 | @runner = runner 26 | @parsers = parsers 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/dumper/luks_v1_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | class Dumper 6 | # Parse LUKS1 7 | class LuksV1Parser 8 | ParsingError = Class.new StandardError 9 | 10 | def call(lines) 11 | data = parse_lines(lines) 12 | instantiate(data.to_h) 13 | end 14 | 15 | private 16 | 17 | SECTOR_SIZE = 512 # LUKS1 support only this sector size 18 | private_constant :SECTOR_SIZE 19 | 20 | def instantiate(raw) 21 | data_class.new( 22 | cipher: [raw.fetch(:cipher_name), raw.fetch(:cipher_mode)].join('-'), 23 | offset: raw.fetch(:offset), 24 | sector_size: SECTOR_SIZE 25 | ) 26 | rescue KeyError => e 27 | raise ParsingError, 'Parsing failed because of missing data', cause: e 28 | end 29 | 30 | def parse_lines(lines) 31 | map_generator.call.tap do |result| 32 | lines.each do |line| 33 | update_result!(result, line: line) 34 | end 35 | end 36 | end 37 | 38 | def update_result!(result, line:) 39 | case line 40 | when /^Cipher name:\s+([\w-]+)$/ 41 | result[:cipher_name] = Regexp.last_match(1) 42 | when /^Cipher mode:\s+([\w-]+)$/ 43 | result[:cipher_mode] = Regexp.last_match(1) 44 | when /^Payload offset:\s+(\d+)$/ 45 | # LUKS1 provides offset in sectors 46 | result[:offset] = Regexp.last_match(1).to_i * SECTOR_SIZE 47 | end 48 | rescue duplicate_exception => e 49 | raise ParsingError, "Parsing failed on: `#{line}`", cause: e 50 | end 51 | 52 | attr_reader :data_class, :map_generator, :duplicate_exception 53 | 54 | def initialize(data_class: Data, 55 | map_generator: -> { SingleAssignRestrictedMap.new }, 56 | duplicate_exception: SingleAssignRestrictedMap::AlreadyAssigned) 57 | @data_class = data_class 58 | @map_generator = map_generator 59 | @duplicate_exception = duplicate_exception 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/dumper/luks_v2_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | class Dumper 6 | # Parse LUKS2 7 | class LuksV2Parser 8 | ParsingError = Class.new StandardError 9 | 10 | def call(lines) 11 | data = parse_lines(lines) 12 | instantiate(data.to_h) 13 | end 14 | 15 | private 16 | 17 | def instantiate(args) 18 | data_class.new(**args) 19 | rescue ArgumentError => e 20 | raise ParsingError, 'Parsing failed because of missing data', cause: e 21 | end 22 | 23 | def parse_lines(lines) 24 | map_generator.call.tap do |result| 25 | section_found = false 26 | lines.each do |line| 27 | if section_found 28 | break if line =~ /^\w/ 29 | 30 | update_result!(result, line: line) 31 | end 32 | line =~ /^Data segments:/ && section_found = true 33 | end 34 | end 35 | end 36 | 37 | def update_result!(result, line:) 38 | case line 39 | when /offset:\s+(\d+) \[bytes\]/ 40 | result[:offset] = Regexp.last_match(1).to_i 41 | when /cipher:\s+([\w-]+)$/ 42 | result[:cipher] = Regexp.last_match(1) 43 | when /sector:\s+(\d+) \[bytes\]/ 44 | result[:sector_size] = Regexp.last_match(1).to_i 45 | end 46 | rescue duplicate_exception => e 47 | raise ParsingError, "Parsing failed on: `#{line}`", cause: e 48 | end 49 | 50 | attr_reader :data_class, :map_generator, :duplicate_exception 51 | 52 | def initialize(data_class: Data, 53 | map_generator: -> { SingleAssignRestrictedMap.new }, 54 | duplicate_exception: SingleAssignRestrictedMap::AlreadyAssigned) 55 | @data_class = data_class 56 | @map_generator = map_generator 57 | @duplicate_exception = duplicate_exception 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/key_fetcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Fetch LUKS key 6 | class KeyFetcher 7 | InvalidPassphrase = Class.new StandardError 8 | 9 | def call(headevice, passphrase) 10 | temp_provider.call do |key_file| 11 | luks_dump(headevice, key_file, passphrase) 12 | file_reader.call(key_file) 13 | end 14 | end 15 | 16 | private 17 | 18 | def luks_dump(headevice, master_key_file, passphrase) 19 | runner.call( 20 | binary, 'luksDump', 'none', '--header', headevice, 21 | '--dump-master-key', '--master-key-file', master_key_file, 22 | '--key-file', '-', 23 | input: passphrase 24 | ) 25 | rescue run_exception 26 | # For simplicity sake let's assume it's invalid passphrase. 27 | # Other errors such as invalid device/header were handled by previous validation. 28 | raise InvalidPassphrase 29 | end 30 | 31 | def binary 32 | lazy_binary.call 33 | end 34 | 35 | attr_reader :lazy_binary, :runner, :run_exception, :file_reader, :temp_provider 36 | 37 | def initialize(lazy_binary: LazyConfig.cryptsetup_path, 38 | runner: Runner::Lines.new, 39 | run_exception: Runner::ExitError, 40 | file_reader: File.method(:read), 41 | temp_provider: SafeTemp::FileName.new) 42 | @lazy_binary = lazy_binary 43 | @runner = runner 44 | @run_exception = run_exception 45 | @file_reader = file_reader 46 | @temp_provider = temp_provider 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks/version_detector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | # Return LUKS version or raise the exception if given file doesn't represent a valid LUKS device 6 | class VersionDetector 7 | Error = Class.new StandardError 8 | NotLuks = Class.new Error 9 | UnsupportedVersion = Class.new Error 10 | 11 | def call(headevice) 12 | version = supported_versions.find do |tested_version| 13 | checker.call(headevice, tested_version) 14 | end 15 | return version if version 16 | raise UnsupportedVersion if checker.call(headevice) 17 | 18 | raise NotLuks 19 | end 20 | 21 | private 22 | 23 | attr_reader :checker, :supported_versions 24 | 25 | def initialize(checker: Checker.new, 26 | supported_versions: %w[LUKS2 LUKS1]) 27 | @checker = checker 28 | @supported_versions = supported_versions 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/crypt_reboot/luks_crypt_tab_patcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Generate patch (files hash) from files in a directory containing uncompressed initramfs 5 | class LuksCryptTabPatcher 6 | def call(dir) 7 | full_crypttab_path = File.join(dir, crypttab_path) 8 | return {} unless File.exist?(full_crypttab_path) 9 | 10 | crypttab_entries = crypttab_deserializer.call(full_crypttab_path) 11 | files_generator.call(crypttab_entries, base_dir: dir, crypttab_path: crypttab_path) 12 | end 13 | 14 | private 15 | 16 | attr_reader :crypttab_path, :crypttab_deserializer, :files_generator 17 | 18 | def initialize(crypttab_path: '/cryptroot/crypttab', 19 | crypttab_deserializer: CryptTab::Deserializer.new, 20 | files_generator: FilesGenerator.new) 21 | @crypttab_path = crypttab_path 22 | @crypttab_deserializer = crypttab_deserializer 23 | @files_generator = files_generator 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/crypt_reboot/passphrase_asker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'io/console' 4 | 5 | module CryptReboot 6 | # Ask user for passphrase and return it 7 | class PassphraseAsker 8 | def call(prompt) 9 | print prompt 10 | $stdin.noecho(&:gets).chomp 11 | ensure 12 | puts 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/crypt_reboot/patched_initramfs_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Yield path to patched version of provided initramfs 5 | class PatchedInitramfsGenerator 6 | def call(initramfs_path, &block) 7 | patch = squeezer.call(initramfs_path) 8 | patcher.call(initramfs_path, patch, &block) 9 | end 10 | 11 | private 12 | 13 | attr_reader :squeezer, :patcher 14 | 15 | def initialize(squeezer: InitramfsPatchSqueezer.new, 16 | patcher: Initramfs::Patcher.new) 17 | @squeezer = squeezer 18 | @patcher = patcher 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/crypt_reboot/rebooter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Perform the reboot or exit (doesn't return) 5 | class Rebooter 6 | def call(act = !Config.prepare_only) 7 | act ? runner.call : exiter.call 8 | end 9 | 10 | private 11 | 12 | attr_reader :runner, :exiter 13 | 14 | def initialize(runner: -> { Process.exec Config.reboot_path }, 15 | exiter: -> { exit 0 }) 16 | @runner = runner 17 | @exiter = exiter 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | ExitError = Class.new StandardError 6 | CommandNotFound = Class.new StandardError 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/binary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | # Return stdout as string 6 | class Binary < Generic 7 | def call(*args, **opts) 8 | super(*args, **opts.merge(binary: true)).out 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/boolean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | # Return true or false, depending if command succeeded or failed 6 | class Boolean < Generic 7 | def call(...) 8 | super(...).success? 9 | end 10 | 11 | private 12 | 13 | def initialize(**opts) 14 | super(**opts.merge(run_method: :run!)) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/generic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tty-command' 4 | 5 | module CryptReboot 6 | module Runner 7 | # Run an external process. Abstract class, use descendants. 8 | class Generic 9 | private 10 | 11 | def call(*args, input: nil, output_file: nil, binary: false) 12 | options = build_options(input, output_file, binary) 13 | cmd.send(run_method, *args, **options) 14 | rescue exceptions[:exit] => e 15 | raise ExitError, cause: e 16 | rescue exceptions[:not_found] => e 17 | raise CommandNotFound, cause: e 18 | end 19 | 20 | def build_options(input, output_file, binary) 21 | {}.tap do |options| 22 | options[:input] = input if input 23 | options[:out] = output_file if output_file 24 | options[:binmode] = true if binary 25 | end 26 | end 27 | 28 | def cmd 29 | lazy_cmd.call 30 | end 31 | 32 | attr_reader :lazy_cmd, :run_method, :exceptions 33 | 34 | def initialize(lazy_cmd: -> { TTY::Command.new(uuid: false, printer: Config.debug ? :pretty : :null) }, 35 | run_method: :run, 36 | exceptions: { 37 | exit: TTY::Command::ExitError, 38 | not_found: Errno::ENOENT 39 | }) 40 | @lazy_cmd = lazy_cmd 41 | @run_method = run_method 42 | @exceptions = exceptions 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/lines.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | # Return standard output in form of lines. 6 | # Newline characters are removed. 7 | class Lines < Generic 8 | def call(...) 9 | super(...).to_a 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/no_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | # Return nil 6 | class NoResult < Generic 7 | def call(...) 8 | super(...) 9 | nil 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/crypt_reboot/runner/text.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | # Return stdout as string 6 | class Text < Generic 7 | def call(...) 8 | super(...).out 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/crypt_reboot/safe_temp/directory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module SafeTemp 7 | # Create temporary directory, mounts ramfs and yields tmp dir location. 8 | # Make sure to cleanup afterwards. 9 | class Directory 10 | def call 11 | tmp_maker.call do |dir| 12 | mounter.call(dir) do 13 | yield dir 14 | end 15 | end 16 | end 17 | 18 | private 19 | 20 | attr_reader :mounter, :tmp_maker 21 | 22 | def initialize(mounter: Mounter.new, tmp_maker: Dir.method(:mktmpdir)) 23 | @mounter = mounter 24 | @tmp_maker = tmp_maker 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/crypt_reboot/safe_temp/file_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module SafeTemp 5 | # Yield non-existing temporary file name located in a safe dir. 6 | # Afterwards the directory containing this file is deleted. 7 | class FileName 8 | def call(name = 'file') 9 | dir_provider.call do |dir| 10 | yield File.join(dir, name) 11 | end 12 | end 13 | 14 | private 15 | 16 | attr_reader :dir_provider 17 | 18 | def initialize(dir_provider: Directory.new) 19 | @dir_provider = dir_provider 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/crypt_reboot/safe_temp/mounter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module SafeTemp 5 | # Mount ramfs at the given mount point, yield and unmount. 6 | # We don't want the contents of directory to be swapped, 7 | # therefore ramfs is used instead of tmpfs. 8 | class Mounter 9 | def call(dir, &block) 10 | mounter.call(dir) 11 | run(dir, &block) 12 | end 13 | 14 | private 15 | 16 | def run(dir, &block) 17 | block.call 18 | ensure 19 | umounter.call(dir) 20 | end 21 | 22 | attr_reader :runner, :mounter, :umounter 23 | 24 | def initialize(runner: Runner::NoResult.new, 25 | mounter: lambda { |dir| 26 | runner.call(Config.mount_path, '-t', 'ramfs', '-o', 'mode=700', 'none', dir) 27 | }, 28 | umounter: ->(dir) { runner.call(Config.umount_path, dir) }) 29 | @runner = runner 30 | @mounter = mounter 31 | @umounter = umounter 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/crypt_reboot/single_assign_restricted_map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Hash-like class allowing to assign value only once to a list of allowed keys 5 | class SingleAssignRestrictedMap 6 | AlreadyAssigned = Class.new StandardError 7 | 8 | def []=(field, value) 9 | raise AlreadyAssigned, "Value already assigned for `#{field}` field" if data.key? field 10 | 11 | data[field] = value 12 | end 13 | 14 | def to_h 15 | data 16 | end 17 | 18 | private 19 | 20 | attr_reader :data 21 | 22 | def initialize 23 | @data = {} 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/crypt_reboot/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | VERSION = '0.3.1' 5 | end 6 | -------------------------------------------------------------------------------- /lib/crypt_reboot/zfs_keystore_patcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | # Generate patch (files hash) from files in a directory containing uncompressed initramfs 5 | class ZfsKeystorePatcher 6 | def call(dir) 7 | crypttab_entries = entries_generator.call 8 | return {} if crypttab_entries.empty? 9 | 10 | files_generator 11 | .call(crypttab_entries, base_dir: dir, crypttab_path: tmp_crypttab_path) 12 | .merge(script_patch_files(dir)) 13 | end 14 | 15 | private 16 | 17 | def script_patch_files(dir) 18 | path = File.join(dir, script_path) 19 | content = File.read(path) 20 | { script_path => patch_script(content) } 21 | rescue Errno::ENOENT 22 | {} 23 | end 24 | 25 | def patch_script(script) 26 | comment = '# Following line has been added by cryptreboot' 27 | patch = "cp #{tmp_crypttab_path} #{crypttab_path}" 28 | script.sub(/^(\s*)(\${CRYPTROOT})\s*$/, "\n\\1#{comment}\n\\1#{patch}\n\n\\1\\2") 29 | end 30 | 31 | attr_reader :crypttab_path, :tmp_crypttab_path, :script_path, 32 | :entries_generator, :files_generator 33 | 34 | def initialize(crypttab_path: '/cryptroot/crypttab', 35 | tmp_crypttab_path: '/cryptreboot/zfs_crypttab', 36 | script_path: '/scripts/zfs', 37 | entries_generator: CryptTab::ZfsKeystoreEntriesGenerator.new, 38 | files_generator: FilesGenerator.new) 39 | @crypttab_path = crypttab_path 40 | @tmp_crypttab_path = tmp_crypttab_path 41 | @script_path = script_path 42 | @entries_generator = entries_generator 43 | @files_generator = files_generator 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/crypt_reboot/cli/exiter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stringio' 4 | 5 | module CryptReboot 6 | module Cli 7 | RSpec.describe Exiter do 8 | it 'returns status' do 9 | result = described_class.new(nil, status: 123, stream: $stdout).call 10 | expect(result).to eq(123) 11 | end 12 | 13 | it 'prints message' do 14 | io = StringIO.new 15 | described_class.new('message', status: double, stream: io).call 16 | expect(io.string).to eq("message\n") 17 | end 18 | 19 | it 'does not print a message if nil' do 20 | io = StringIO.new 21 | described_class.new(nil, status: double, stream: io).call 22 | expect(io.string).to be_empty 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/crypt_reboot/cli/params/flattener_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | RSpec.describe Flattener do 7 | subject(:flattener) do 8 | described_class.new(key: 'paths', suffix: '_path') 9 | end 10 | 11 | it 'flattens params' do 12 | result = flattener.call({ param1: 1, paths: { tool1: 'a', tool2: 'b' } }) 13 | expect(result).to eq({ param1: 1, tool1_path: 'a', tool2_path: 'b' }) 14 | end 15 | 16 | it 'passes through behave empty params' do 17 | result = flattener.call({}) 18 | expect(result).to eq({}) 19 | end 20 | 21 | it 'passes through behave params without key' do 22 | result = flattener.call({ param1: 1 }) 23 | expect(result).to eq({ param1: 1 }) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/crypt_reboot/cli/params/help_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | RSpec.describe HelpGenerator do 7 | it 'returns usage instructions' do 8 | usage = described_class.new.call 9 | expect(usage).to include('Usage:', '--help', '--version', 'initramfs', 'cryptsetup') 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/crypt_reboot/cli/params/parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | module Params 6 | RSpec.describe Parser do 7 | subject(:parser) do 8 | described_class.new 9 | end 10 | 11 | it 'parses valid params' do 12 | params = parser.call(['--kernel', 'kernel1', '--initramfs', 'initramfs1', '--cmdline', 'a b c']) 13 | expect(params).to include({ kernel: 'kernel1', initramfs: 'initramfs1', cmdline: 'a b c' }) 14 | end 15 | 16 | it 'parses flattened params' do 17 | params = parser.call(['--tool', 'cat:/usr/bin/cat', '--tool', 'cpio:/usr/bin/cpio']) 18 | expect(params).to include({ cat_path: '/usr/bin/cat', cpio_path: '/usr/bin/cpio' }) 19 | end 20 | 21 | it 'gets defaults' do 22 | params = parser.call([]) 23 | expect(params).to include({ kernel: '/boot/vmlinuz' }) 24 | end 25 | 26 | it 'fails on invalid params' do 27 | expect do 28 | parser.call(['--something']) 29 | end.to raise_error(Parser::ParseError) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/crypt_reboot/cli/params_parsing_executor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Cli 5 | RSpec.describe ParamsParsingExecutor do 6 | subject(:executor) do 7 | described_class.new( 8 | loader: loader, 9 | locker: locker, 10 | debug_checker: -> { config.debug }, 11 | config_updater: config.method(:update!) 12 | ) 13 | end 14 | 15 | let(:config) { InstantiableConfig.new } 16 | let(:locker) { spy } 17 | 18 | context 'when no real work will be done' do 19 | let(:loader) do 20 | -> { raise ZeroDivisionError, 'Loading failed' } 21 | end 22 | 23 | it 'locks memory before doing real work' do 24 | executor.call([]) 25 | expect(locker).to have_received(:call) 26 | end 27 | 28 | it 'does not lock memory if version number requested' do 29 | executor.call(['-v']) 30 | expect(locker).not_to have_received(:call) 31 | end 32 | 33 | it 'returns help' do 34 | result = executor.call(['--help']) 35 | expect(result).to be_a(HappyExiter).and \ 36 | have_attributes(text: a_string_including('Usage:', '--help', '--version', 'initramfs', 'cryptsetup')) 37 | end 38 | 39 | it 'returns version' do 40 | result = executor.call(['--version']) 41 | expect(result).to be_a(HappyExiter).and \ 42 | have_attributes(text: a_string_including(VERSION)) 43 | end 44 | 45 | it 'returns error if invalid flag specified' do 46 | result = executor.call(['--invalid-flag']) 47 | expect(result).to be_a(SadExiter).and \ 48 | have_attributes(text: a_string_including('invalid option')) 49 | end 50 | 51 | it 'returns error if processing failed' do 52 | result = executor.call([]) 53 | expect(result).to be_a(SadExiter).and have_attributes(text: 'Zero division error: Loading failed') 54 | end 55 | 56 | it 'raises error if processing failed and debug flag present' do 57 | expect do 58 | executor.call(['--debug']) 59 | end.to raise_error(ZeroDivisionError) 60 | end 61 | 62 | it 'does not raise errors caused by processing params if debug flag present' do 63 | result = executor.call(['--debug', '--invalid-flag']) 64 | expect(result).to be_a(SadExiter).and \ 65 | have_attributes(text: a_string_including('invalid option')) 66 | end 67 | end 68 | 69 | context 'when real work will be done' do 70 | let(:loader) { -> {} } 71 | 72 | it 'executes and returns rebooter' do 73 | result = executor.call([]) 74 | expect(result).to be_an_instance_of(Rebooter) 75 | end 76 | 77 | it 'updates config when option is specified' do 78 | executor.call(['--prepare-only']) 79 | expect(config.prepare_only).to be(true) 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/crypt_reboot/concatenator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | RSpec.describe Concatenator do 7 | subject(:cat) { described_class.new } 8 | 9 | it 'concatenates files' do 10 | Dir.mktmpdir do |dir| 11 | result_path = File.join(dir, 'result.txt') 12 | cat.call('spec/fixtures/concatenator/f1.txt', 'spec/fixtures/concatenator/f2.txt', to: result_path) 13 | expect(File.read(result_path)).to eq("f1\nf2\n") 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/crypt_reboot/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe Config do 5 | subject(:config) { described_class } 6 | 7 | def patch(value) 8 | "#{value}-patched" 9 | end 10 | 11 | it 'retrieves a setting' do 12 | old_value = config.cat_path 13 | config.update!(cat_path: patch(old_value)) 14 | result = config.cat_path 15 | expect(result).to eq(patch(old_value)) 16 | config.update!(cat_path: old_value) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/deserializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe Deserializer do 6 | subject(:deserializer) do 7 | described_class.new(entry_deserializer: entry_deserializer) 8 | end 9 | 10 | let :entry_deserializer do 11 | ->(line) { line.split[1] } 12 | end 13 | 14 | let :crypttab_content do 15 | "# comment1 \n" \ 16 | "cryptdata UUID=93115b73-4ddb-4a84-8ff4-3d0ef612e895 none luks\n" \ 17 | "# comment 2\n" \ 18 | 'cryptswap UUID=5e14f95d-6549-4ecf-8d67-6db87d029a51 /dev/urandom ' \ 19 | 'swap,plain,offset=1024,cipher=aes-xts-plain64,size=512' 20 | end 21 | 22 | let :expected_result do 23 | [ 24 | 'UUID=93115b73-4ddb-4a84-8ff4-3d0ef612e895', 25 | 'UUID=5e14f95d-6549-4ecf-8d67-6db87d029a51' 26 | ] 27 | end 28 | 29 | it 'deserializes crypttab file' do 30 | result = deserializer.call(content: crypttab_content) 31 | expect(result).to eq(expected_result) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/entry_deserializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe EntryDeserializer do 6 | subject(:deserializer) { described_class.new } 7 | 8 | context 'with complex example' do 9 | let :crypttab_line do 10 | 'cryptswap UUID=946d9327-0de2-4d7b-adba-28a079131b4c ' \ 11 | '/dev/urandom swap,plain,offset=1024,cipher=aes-xts-plain64,size=512' 12 | end 13 | 14 | let :expected_result do 15 | Entry.new( 16 | target: 'cryptswap', 17 | source: 'UUID=946d9327-0de2-4d7b-adba-28a079131b4c', 18 | key_file: '/dev/urandom', 19 | options: { 20 | offset: 1024, 21 | cipher: 'aes-xts-plain64', 22 | size: 512 23 | }, 24 | flags: %i[swap plain] 25 | ) 26 | end 27 | 28 | it 'deserializes' do 29 | result = deserializer.call(crypttab_line) 30 | expect(result).to eq(expected_result) 31 | end 32 | end 33 | 34 | context 'with minimal example without options' do 35 | let :crypttab_line do 36 | 'croot /dev/sda1 none' 37 | end 38 | 39 | let :expected_result do 40 | Entry.new( 41 | target: 'croot', 42 | source: '/dev/sda1', 43 | key_file: 'none', 44 | options: {}, 45 | flags: [] 46 | ) 47 | end 48 | 49 | it 'deserializes' do 50 | result = deserializer.call(crypttab_line) 51 | expect(result).to eq(expected_result) 52 | end 53 | end 54 | 55 | context 'with invalid crypttab line' do 56 | let :crypttab_line do 57 | 'croot /dev/sda1' 58 | end 59 | 60 | it 'fails with exception' do 61 | expect do 62 | deserializer.call(crypttab_line) 63 | end.to raise_error(EntryDeserializer::InvalidFormat) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/entry_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe EntrySerializer do 6 | subject(:serializer) { described_class.new } 7 | 8 | let :entry do 9 | Entry.new( 10 | target: 'cryptswap', 11 | source: 'UUID=946d9327-0de2-4d7b-adba-28a079131b4c', 12 | key_file: '/dev/urandom', 13 | options: { 14 | 'offset' => '1024', 15 | 'cipher' => 'aes-xts-plain64', 16 | 'size' => '512' 17 | }, 18 | flags: %w[swap plain] 19 | ) 20 | end 21 | 22 | let :expected_result do 23 | 'cryptswap UUID=946d9327-0de2-4d7b-adba-28a079131b4c ' \ 24 | '/dev/urandom swap,plain,offset=1024,cipher=aes-xts-plain64,size=512' 25 | end 26 | 27 | it 'deserializes complex example' do 28 | result = serializer.call(entry) 29 | expect(result).to eq(expected_result) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/entry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe Entry do 6 | let :header do 7 | described_class.new( 8 | target: 'target1', 9 | source: 'source1', 10 | key_file: 'key_file1', 11 | options: { header: '/my/header.bin' }, 12 | flags: [] 13 | ) 14 | end 15 | 16 | let :device do 17 | described_class.new( 18 | target: 'target1', 19 | source: '/dev/my_device', 20 | key_file: 'key_file1', 21 | options: {}, 22 | flags: [] 23 | ) 24 | end 25 | 26 | it 'returns header file path' do 27 | expect(header.headevice).to eq('/my/header.bin') 28 | end 29 | 30 | it 'returns device path' do 31 | expect(device.headevice).to eq('/dev/my_device') 32 | end 33 | 34 | context 'with directory prefix' do 35 | it 'returns prefixed header file path' do 36 | expect(header.headevice(header_prefix: '/prefix')).to eq('/prefix/my/header.bin') 37 | end 38 | 39 | it 'returns device path ignoring prefix' do 40 | expect(device.headevice(header_prefix: '/prefix')).to eq('/dev/my_device') 41 | end 42 | end 43 | 44 | it 'does not equal if entries are different' do 45 | expect(header).not_to eq(device) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/keyfile_locator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe KeyfileLocator do 6 | subject(:locator) do 7 | described_class.new 8 | end 9 | 10 | it 'returns key location' do 11 | path = locator.call('target') 12 | expect(path).to eq('/cryptreboot/target.key') 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/luks_to_plain_converter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe LuksToPlainConverter do 6 | subject(:converter) do 7 | described_class.new 8 | end 9 | 10 | let :luks_entry do 11 | Entry.new( 12 | target: 'target', 13 | source: 'source', 14 | key_file: 'my_keyfile', 15 | options: { 16 | 'keyfile-size': 1, 17 | keyslot: 2, 18 | 'key-slot': 3, 19 | header: 'a', 20 | keyscript: 'b', 21 | option1: 'c', 22 | option2: 'd', 23 | cipher: 'ignored_in_luks' 24 | }.merge(luks_options), 25 | flags: %i[luks flag1 flag2] 26 | ) 27 | end 28 | 29 | let :plain_entry do 30 | Entry.new( 31 | target: 'target', 32 | source: 'source', 33 | key_file: '/my/secret.key', 34 | options: { 35 | option1: 'c', 36 | option2: 'd', 37 | cipher: 'caesar-ecb', 38 | size: 256, 39 | offset: 100 40 | }.merge(plain_options), 41 | flags: %i[flag1 flag2 plain] 42 | ) 43 | end 44 | 45 | let :data do 46 | Luks::Data.new( 47 | cipher: 'caesar-ecb', 48 | offset: 51_200, 49 | sector_size: 4096, 50 | key: "\x0" * 32 51 | ) 52 | end 53 | 54 | let(:luks_options) { {} } 55 | let(:plain_options) { {} } 56 | 57 | context 'with user-supplied sector size' do 58 | let(:luks_options) { { 'sector-size': 512 } } 59 | let(:plain_options) { { 'sector-size': 512 } } 60 | 61 | it 'converts entry to plain' do 62 | new_entry = converter.call(luks_entry, data, '/my/secret.key') 63 | expect(new_entry).to eq(plain_entry) 64 | end 65 | end 66 | 67 | context 'with detected sector size' do 68 | let(:plain_options) { { 'sector-size': 4096 } } 69 | 70 | it 'converts entry to plain' do 71 | new_entry = converter.call(luks_entry, data, '/my/secret.key') 72 | expect(new_entry).to eq(plain_entry) 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/serializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe Serializer do 6 | subject(:serializer) do 7 | described_class.new(entry_serializer: entry_serializer, header: '# Comment') 8 | end 9 | 10 | let :entry_serializer do 11 | ->(entry) { "line:#{entry}" } 12 | end 13 | 14 | let :entries do 15 | [1, 2, 3] 16 | end 17 | 18 | let :expected_crypttab do 19 | "# Comment\n" \ 20 | "line:1\n" \ 21 | "line:2\n" \ 22 | "line:3\n" 23 | end 24 | 25 | it 'serializes entries' do 26 | crypttab = serializer.call(entries) 27 | expect(crypttab).to eq(expected_crypttab) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/crypt_reboot/crypt_tab/zfs_keystore_entries_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module CryptTab 5 | RSpec.describe ZfsKeystoreEntriesGenerator do 6 | subject(:generator) do 7 | described_class.new(zvol_dir: 'spec/fixtures/zfs_dev/zvol') 8 | end 9 | 10 | let :expected_result do 11 | [ 12 | Entry.new( 13 | target: 'keystore-rpool', 14 | source: 'spec/fixtures/zfs_dev/zvol/rpool/keystore', 15 | key_file: 'none', 16 | options: {}, 17 | flags: %i[luks discard] 18 | ) 19 | ] 20 | end 21 | 22 | it do 23 | result = generator.call 24 | expect(result).to eq(expected_result) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/crypt_reboot/elastic_memory_locker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe ElasticMemoryLocker do 5 | subject :locker do 6 | described_class.new(insecure_memory_checker: -> { insecure_memory }, 7 | locker: real_locker, 8 | locking_error: locking_error) 9 | end 10 | 11 | let(:insecure_memory) { false } 12 | let(:real_locker) { spy } 13 | let(:locking_error) { LocalJumpError } 14 | 15 | context 'when locking is disabled' do 16 | let(:insecure_memory) { true } 17 | 18 | it 'does not call real locker' do 19 | locker.call 20 | expect(real_locker).not_to have_received(:call) 21 | end 22 | end 23 | 24 | context 'when locking is enabled' do 25 | it 'calls real locker' do 26 | locker.call 27 | expect(real_locker).to have_received(:call) 28 | end 29 | end 30 | 31 | context 'when memory_locker does not work' do 32 | let :real_locker do 33 | -> { raise locking_error } 34 | end 35 | 36 | it 'raises exception' do 37 | expect do 38 | locker.call 39 | end.to raise_error( 40 | an_instance_of(described_class::LockingError).and(having_attributes(cause: locking_error)) 41 | ) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/crypt_reboot/files_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe FilesGenerator do 5 | subject(:generator) do 6 | described_class.new( 7 | luks_checker: checker, 8 | luks_data_fetcher: fetcher, 9 | serializer: serializer 10 | ) 11 | end 12 | 13 | let :checker do 14 | ->(headevice) { headevice != '/dev/non_luks_source' } 15 | end 16 | 17 | let :fetcher do 18 | lambda { |headevice, _target| 19 | keys = { 20 | '/dev/source1' => 'key1', 21 | '/dev/source2' => 'key2', 22 | '/dev/luks_source' => 'luks_key', 23 | '/my/base/dir/my/header.luks' => 'header_key' 24 | } 25 | 26 | Luks::Data.new(cipher: 'caesar-ecb', offset: 123, 27 | sector_size: 4096, key: keys[headevice]) 28 | } 29 | end 30 | 31 | let :serializer do 32 | ->(entries) { entries.map(&:target).join("\n") } 33 | end 34 | 35 | def create_entry(target, source, **options) 36 | CryptTab::Entry.new( 37 | target: target, source: source, key_file: 'my_keyfile', 38 | options: options, flags: [] 39 | ) 40 | end 41 | 42 | context 'with empty crypttab' do 43 | let :crypttab do 44 | [] 45 | end 46 | 47 | let :expected_files do 48 | { '/cryptroot/crypttab' => '' } 49 | end 50 | 51 | it 'generates files hash' do 52 | files = generator.call(crypttab, base_dir: '/ignored', crypttab_path: '/cryptroot/crypttab') 53 | expect(files).to eq(expected_files) 54 | end 55 | end 56 | 57 | context 'with crypttab containing two LUKS devices' do 58 | let :crypttab do 59 | [ 60 | create_entry('target1', '/dev/source1'), 61 | create_entry('target2', '/dev/source2') 62 | ] 63 | end 64 | 65 | let :expected_files do 66 | { 67 | '/cryptreboot/target1.key' => 'key1', 68 | '/cryptreboot/target2.key' => 'key2', 69 | '/cryptroot/crypttab' => "target1\ntarget2" 70 | } 71 | end 72 | 73 | it 'generates files hash' do 74 | files = generator.call(crypttab, base_dir: '/ignored', crypttab_path: '/cryptroot/crypttab') 75 | expect(files).to eq(expected_files) 76 | end 77 | end 78 | 79 | context 'with crypttab containing mix of LUKS and non-LUKS devices' do 80 | let :crypttab do 81 | [ 82 | create_entry('luks', '/dev/luks_source'), 83 | create_entry('non_luks', '/dev/non_luks_source') 84 | ] 85 | end 86 | 87 | let :expected_files do 88 | { 89 | '/cryptreboot/luks.key' => 'luks_key', 90 | '/cryptroot/crypttab' => "luks\nnon_luks" 91 | } 92 | end 93 | 94 | it 'generates files hash' do 95 | files = generator.call(crypttab, base_dir: '/ignored', crypttab_path: '/cryptroot/crypttab') 96 | expect(files).to eq(expected_files) 97 | end 98 | end 99 | 100 | context 'with crypttab containing LUKS entry with detached header' do 101 | let :crypttab do 102 | [ 103 | create_entry('luks', '/dev/luks_source', header: '/my/header.luks') 104 | ] 105 | end 106 | 107 | let :expected_files do 108 | { 109 | '/cryptreboot/luks.key' => 'header_key', 110 | '/cryptroot/crypttab' => 'luks' 111 | } 112 | end 113 | 114 | it 'generates files hash' do 115 | files = generator.call(crypttab, base_dir: '/my/base/dir', crypttab_path: '/cryptroot/crypttab') 116 | expect(files).to eq(expected_files) 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/crypt_reboot/files_writer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | RSpec.describe FilesWriter do 7 | subject(:writer) { described_class.new } 8 | 9 | let :files_spec do 10 | { 11 | 'file1' => 'contents1', 12 | 'dir1/dir2/file2' => 'contents2', 13 | 'dir1/file3' => 'contents3', 14 | 'file4' => 'contents4' 15 | } 16 | end 17 | 18 | let :expected_files do 19 | [ 20 | '.', 21 | './file4', 22 | './file1', 23 | './dir1', 24 | './dir1/dir2', 25 | './dir1/dir2/file2', 26 | './dir1/file3' 27 | ] 28 | end 29 | 30 | it 'creates directory structure and files' do 31 | Dir.mktmpdir do |dir| 32 | writer.call(files_spec, dir) 33 | expect(`cd #{dir}; find`.split("\n")).to match_array(expected_files) 34 | end 35 | end 36 | 37 | it 'creates a file with content' do 38 | Dir.mktmpdir do |dir| 39 | writer.call({ 'file.txt' => 'my content' }, dir) 40 | content = File.read(File.join(dir, 'file.txt')) 41 | expect(content).to eq('my content') 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/crypt_reboot/gziper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tempfile' 4 | 5 | module CryptReboot 6 | RSpec.describe Gziper do 7 | subject(:gziper) { described_class.new } 8 | 9 | it 'creates correctly formatted gzip file' do 10 | Tempfile.open do |file| 11 | gziper.call(file.path, 'This will be compressed') 12 | result = system("gzip -t #{file.path}") 13 | expect(result).to be(true) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/archiver_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | RSpec.describe Archiver do 8 | subject(:archiver) { described_class.new(gziper: File.method(:binwrite)) } 9 | 10 | let(:files_dir) { 'spec/fixtures/archiver/files' } 11 | 12 | def tmp_file 13 | Dir.mktmpdir do |base_dir| 14 | file = File.join(base_dir, 'file') 15 | yield file 16 | end 17 | end 18 | 19 | it 'produces archive with valid header' do 20 | tmp_file do |archive_path| 21 | archiver.call(files_dir, archive_path) 22 | archive = File.read(archive_path) 23 | expect(archive).to start_with('070701') 24 | end 25 | end 26 | 27 | it 'produces archive containing file' do 28 | tmp_file do |archive_path| 29 | archiver.call(files_dir, archive_path) 30 | archive = File.read(archive_path) 31 | expect(archive).to match(/file1.txt.+testfile1/) 32 | end 33 | end 34 | 35 | it 'produces archive with valid trailer' do 36 | tmp_file do |archive_path| 37 | archiver.call(files_dir, archive_path) 38 | archive = File.read(archive_path) 39 | expect(archive).to match(/TRAILER!!!/) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/decompressor/intolerant_decompressor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | class Decompressor 6 | RSpec.describe IntolerantDecompressor do 7 | subject(:decompressor) do 8 | described_class.new(runner: runner) 9 | end 10 | 11 | context 'when ignoring result' do 12 | let(:memo) { spy } 13 | 14 | let :runner do 15 | lambda { |arg| 16 | memo.call(arg) 17 | [] 18 | } 19 | end 20 | 21 | it 'executes correct command line' do 22 | decompressor.call('/boot/initrd.img', '/my/dir') 23 | expect(memo).to have_received(:call) 24 | .with('(echo lz4 \"-t\"; strace -f --trace=execve -z -qq --signal=\!all ' \ 25 | 'unmkinitramfs /boot/initrd.img /my/dir) ' \ 26 | '2>&1 | grep --line-buffered lz4') 27 | end 28 | 29 | it 'handles shell escaping' do 30 | decompressor.call(%(it's my "file" name), '/my|"nice/dir"') 31 | expect(memo).to have_received(:call) 32 | .with('(echo lz4 \"-t\"; strace -f --trace=execve -z -qq --signal=\!all ' \ 33 | "unmkinitramfs it\\'s\\ my\\ \\\"file\\\"\\ name /my\\|\\\"nice/dir\\\") " \ 34 | '2>&1 | grep --line-buffered lz4') 35 | end 36 | end 37 | 38 | context 'when ignoring command line being executed' do 39 | let :runner do 40 | ->(_) { lines } 41 | end 42 | 43 | context 'with command telling lz4 was used for test and real work' do 44 | let(:lines) do 45 | [ 46 | 'execve("/usr/bin/lz4cat", ["lz4cat", "-t"]', 47 | 'execve("/usr/bin/lz4cat", ["lz4cat", "file"]' 48 | ] 49 | end 50 | 51 | it 'raises exception' do 52 | expect do 53 | decompressor.call('file', 'dir') 54 | end.to raise_error(IntolerantDecompressor::Lz4NotAllowed) 55 | end 56 | end 57 | 58 | context 'with command telling lz4 was used only for test (1)' do 59 | let(:lines) do 60 | [ 61 | 'execve("/usr/bin/lz4cat", ["lz4cat", "-t"]' 62 | ] 63 | end 64 | 65 | it 'does not raise exception' do 66 | expect do 67 | decompressor.call('file', 'dir') 68 | end.not_to raise_error 69 | end 70 | end 71 | 72 | context 'with command telling lz4 was used only for test (2)' do 73 | let(:lines) do 74 | [ 75 | 'execve("/usr/bin/lz4cat", ["lz4cat", "--test"]' 76 | ] 77 | end 78 | 79 | it 'does not raise exception' do 80 | expect do 81 | decompressor.call('file', 'dir') 82 | end.not_to raise_error 83 | end 84 | end 85 | 86 | context 'with command telling lz4 was not used at all' do 87 | let(:lines) { [] } 88 | 89 | it 'does not raise exception' do 90 | expect do 91 | decompressor.call('file', 'dir') 92 | end.not_to raise_error 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/decompressor/tolerant_decompressor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | class Decompressor 6 | RSpec.describe TolerantDecompressor do 7 | subject(:decompressor) do 8 | described_class.new(runner: runner) 9 | end 10 | 11 | let(:runner) { spy } 12 | 13 | it 'decompress' do 14 | decompressor.call('/boot/initrd.img', '/my/dir') 15 | expect(runner).to have_received(:call).with('unmkinitramfs', '/boot/initrd.img', '/my/dir') 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/decompressor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | RSpec.describe Decompressor do 6 | subject(:factory) do 7 | described_class.new 8 | end 9 | 10 | it 'returns tolerant compressor' do 11 | decompressor = factory.call(skip_lz4_check: true) 12 | expect(decompressor).to be_instance_of(Decompressor::TolerantDecompressor) 13 | end 14 | 15 | it 'returns intolerant compressor' do 16 | decompressor = factory.call(skip_lz4_check: false) 17 | expect(decompressor).to be_instance_of(Decompressor::IntolerantDecompressor) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/extractor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Initramfs 5 | RSpec.describe Extractor do 6 | subject(:extractor) do 7 | described_class.new(decompressor_factory: -> { fake_decompressor }, 8 | logger: logger, 9 | message: 'extracting') 10 | end 11 | 12 | let(:logger) { spy } 13 | 14 | let :fake_decompressor do 15 | lambda { |_initramfs, dir| 16 | dir = File.join(dir, 'main') 17 | Dir.mkdir(dir) 18 | File.open(test_file_path(dir), 'w') { 0 } 19 | } 20 | end 21 | 22 | def test_file_path(dir) 23 | File.join(dir, 'test_file.txt') 24 | end 25 | 26 | it 'extracts' do 27 | extractor.call('dummy_initramfs') do |dir| 28 | expect(File).to exist(test_file_path(dir)) 29 | end 30 | end 31 | 32 | it 'displays message' do 33 | extractor.call('dummy_initramfs') {} 34 | expect(logger).to have_received(:call).with('extracting') 35 | end 36 | 37 | it 'cleans up' do 38 | tmp_dir = nil 39 | extractor.call('dummy_initramfs') do |dir| 40 | tmp_dir = dir 41 | end 42 | expect(File).not_to exist(tmp_dir) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs/patcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module Initramfs 7 | RSpec.describe Patcher do 8 | subject(:patcher) do 9 | described_class.new( 10 | temp_provider: Dir.method(:mktmpdir), 11 | archiver: ->(dir, file) { `cd #{dir}; find > #{file}` } 12 | ) 13 | end 14 | 15 | let(:initramfs_path) { 'spec/fixtures/dummy_initramfs' } 16 | 17 | let(:files_spec) do 18 | { 'file1' => 'contents1', 'dir1/file2' => 'contents2' } 19 | end 20 | 21 | let(:expected_initramfs) do 22 | "not real\n.\n./file1\n./dir1\n./dir1/file2\n" 23 | end 24 | 25 | it 'yields a block' do 26 | expect do |block| 27 | patcher.call(initramfs_path, files_spec, &block) 28 | end.to yield_control 29 | end 30 | 31 | it 'patches initramfs' do 32 | patched = nil 33 | patcher.call(initramfs_path, files_spec) { |path| patched = File.read(path) } 34 | expect(patched).to eq(expected_initramfs) 35 | end 36 | 37 | it 'cleans up' do 38 | patched_path = nil 39 | patcher.call(initramfs_path, files_spec) { |path| patched_path = path } 40 | expect(File).not_to exist(patched_path) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/crypt_reboot/initramfs_patch_squeezer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe InitramfsPatchSqueezer do 5 | subject(:squeezer) do 6 | described_class.new( 7 | extractor: ->(_, &b) { b.call('dir_with_unpacked_initramfs') }, 8 | patchers: [ 9 | ->(_) { { 'file1' => 'content1' } }, 10 | ->(_) { { 'file2' => 'content2' } } 11 | ] 12 | ) 13 | end 14 | 15 | let :expected_patch do 16 | { 17 | 'file1' => 'content1', 18 | 'file2' => 'content2' 19 | } 20 | end 21 | 22 | it do 23 | patch = squeezer.call(double) 24 | expect(patch).to eq(expected_patch) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/crypt_reboot/instantiable_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe InstantiableConfig do 5 | subject(:config) { described_class.new } 6 | 7 | it 'provides default value' do 8 | expect(config.cat_path).to eq('cat') 9 | end 10 | 11 | it 'allows to be updated' do 12 | config.update!(cat_path: 'dog') 13 | expect(config.cat_path).to eq('dog') 14 | end 15 | 16 | it 'does not allow to update unknown settings' do 17 | expect do 18 | config.update!(dog_path: 'something') 19 | end.to raise_error(InstantiableConfig::UnrecognizedSetting) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/crypt_reboot/kexec/loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Kexec 5 | RSpec.describe Loader do 6 | subject(:loader) do 7 | described_class.new(lazy_tool: -> { 'kexec' }, runner: runner) 8 | end 9 | 10 | let(:runner) { spy } 11 | 12 | let(:boot_config_obj) { BootConfig.new(**boot_config) } 13 | 14 | context 'with kernel only' do 15 | context 'with custom command line' do 16 | let :boot_config do 17 | { kernel: 'kernel', cmdline: 'cmdline' } 18 | end 19 | 20 | it 'executes kexec with correct arguments' do 21 | loader.call(boot_config_obj) 22 | expect(runner).to have_received(:call).with('kexec', '-al', 'kernel', '--append', 'cmdline') 23 | end 24 | end 25 | 26 | context 'without custom command line' do 27 | let :boot_config do 28 | { kernel: 'kernel' } 29 | end 30 | 31 | it 'executes kexec with correct arguments' do 32 | loader.call(boot_config_obj) 33 | expect(runner).to have_received(:call).with('kexec', '-al', 'kernel', '--reuse-cmdline') 34 | end 35 | end 36 | end 37 | 38 | context 'with kernel and initramfs' do 39 | context 'with custom command line' do 40 | let :boot_config do 41 | { kernel: 'kernel', initramfs: 'initramfs', cmdline: 'cmdline' } 42 | end 43 | 44 | it 'executes kexec with correct arguments' do 45 | loader.call(boot_config_obj) 46 | expect(runner).to have_received(:call).with('kexec', '-al', 'kernel', '--initrd', 47 | 'initramfs', '--append', 'cmdline') 48 | end 49 | end 50 | 51 | context 'without custom command line' do 52 | let :boot_config do 53 | { kernel: 'kernel', initramfs: 'initramfs' } 54 | end 55 | 56 | it 'executes kexec with correct arguments' do 57 | loader.call(boot_config_obj) 58 | expect(runner).to have_received(:call).with('kexec', '-al', 'kernel', '--initrd', 59 | 'initramfs', '--reuse-cmdline') 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/crypt_reboot/kexec_patching_loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe KexecPatchingLoader do 5 | subject(:loader) do 6 | described_class.new( 7 | generator: ->(image, &block) { block.call "patched-#{image}" }, 8 | loader: kexec_loader 9 | ) 10 | end 11 | 12 | let(:kexec_loader) { spy } 13 | 14 | it 'loads patched initramfs' do 15 | boot_config = BootConfig.new(kernel: 'kernel', cmdline: 'cmdline', initramfs: 'initramfs') 16 | loader.call(boot_config) 17 | new_boot_config = boot_config.with_initramfs('patched-initramfs') 18 | expect(kexec_loader).to have_received(:call).with(new_boot_config) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/crypt_reboot/lazy_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe LazyConfig do 5 | subject(:config) { described_class } 6 | 7 | def patch(value) 8 | "#{value}-patched" 9 | end 10 | 11 | it 'retrieves getter for a setting' do 12 | old_value = config.cpio_path.call 13 | getter = config.cpio_path 14 | config.update!(cpio_path: patch(old_value)).call 15 | expect(getter.call).to eq(patch(old_value)) 16 | config.update!(cpio_path: old_value).call 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/checker_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | RSpec.describe Checker do 6 | subject(:checker) { described_class.new } 7 | 8 | context 'when looking for LUKS2' do 9 | it 'recognizes LUKS2' do 10 | result = checker.call('spec/fixtures/luks_headers/v2.bin', 'LUKS2') 11 | expect(result).to be(true) 12 | end 13 | 14 | it 'returns false if given LUKS1' do 15 | result = checker.call('spec/fixtures/luks_headers/v1.bin', 'LUKS2') 16 | expect(result).to be(false) 17 | end 18 | 19 | it 'returns false on invalid header' do 20 | result = checker.call('spec/fixtures/luks_headers/invalid.bin', 'LUKS2') 21 | expect(result).to be(false) 22 | end 23 | end 24 | 25 | context 'when looking for LUKS1' do 26 | it 'recognizes LUKS1' do 27 | result = checker.call('spec/fixtures/luks_headers/v1.bin', 'LUKS1') 28 | expect(result).to be(true) 29 | end 30 | 31 | it 'returns false if given LUKS2' do 32 | result = checker.call('spec/fixtures/luks_headers/v2.bin', 'LUKS1') 33 | expect(result).to be(false) 34 | end 35 | 36 | it 'returns false on invalid header' do 37 | result = checker.call('spec/fixtures/luks_headers/invalid.bin', 'LUKS1') 38 | expect(result).to be(false) 39 | end 40 | end 41 | 42 | it 'returns false on invalid header when no LUKS version provided' do 43 | result = checker.call('spec/fixtures/luks_headers/invalid.bin') 44 | expect(result).to be(false) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/data_fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | RSpec.describe DataFetcher do 6 | subject(:fetcher) do 7 | described_class.new( 8 | asker: ->(_) { 'qwe' }, 9 | key_fetcher: ->(_, _) { expected_data.key } 10 | ) 11 | end 12 | 13 | context 'with LUKS2 header' do 14 | let :expected_data do 15 | Data.new( 16 | cipher: 'aes-xts-plain64', 17 | offset: 16_777_216, 18 | sector_size: 512, 19 | key: 'secret' 20 | ) 21 | end 22 | 23 | it 'fetches data' do 24 | data = fetcher.call('spec/fixtures/luks_headers/v2.bin', 'v2') 25 | expect(data).to eq(expected_data) 26 | end 27 | end 28 | 29 | context 'with LUKS1 header' do 30 | let :expected_data do 31 | Data.new( 32 | cipher: 'aes-xts-plain64', 33 | offset: 0, 34 | sector_size: 512, 35 | key: 'secret' 36 | ) 37 | end 38 | 39 | it 'fetches data' do 40 | data = fetcher.call('spec/fixtures/luks_headers/v1.bin', 'v1') 41 | expect(data).to eq(expected_data) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/data_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | RSpec.describe Data do 6 | def data(args) 7 | described_class.new(**args) 8 | end 9 | 10 | let :default_params do 11 | { cipher: 'caesar-ecb', offset: 123, sector_size: 4096 } 12 | end 13 | 14 | let :with_key do 15 | default_params.merge( 16 | { key: "\x06\x04\x57\xc4\x52\x2f\x7d\xdd\x8c\x2a\x59\x42\x64\xbd\x89\xa3" } 17 | ) 18 | end 19 | 20 | it 'may be constructed without key' do 21 | expect(data(default_params).key).to be_empty 22 | end 23 | 24 | it 'may be constructed with key' do 25 | expect(data(with_key).key).not_to be_empty 26 | end 27 | 28 | it 'returns true if comparing two identical objects' do 29 | one = data(default_params) 30 | two = data(default_params) 31 | expect(one == two).to be(true) 32 | end 33 | 34 | it 'returns false if comparing two different objects' do 35 | one = data(default_params) 36 | two = data(default_params.merge(cipher: 'aes-cbc')) 37 | expect(one == two).to be(false) 38 | end 39 | 40 | it 'adds key' do 41 | new_data = data(default_params).with_key('secret') 42 | expected_data = data(default_params.merge({ key: 'secret' })) 43 | expect(new_data).to eq(expected_data) 44 | end 45 | 46 | it 'calculates key bits correctly' do 47 | expect(data(with_key).key_bits).to eq(128) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/dumper/luks_v1_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | class Dumper 6 | RSpec.describe LuksV1Parser do 7 | subject(:parser) do 8 | described_class.new 9 | end 10 | 11 | let :real_lines do 12 | File.readlines('spec/fixtures/luks_dump/v1.txt').map(&:chomp) 13 | end 14 | 15 | let :expected_data do 16 | Data.new( 17 | cipher: 'aes-xts-plain64', 18 | offset: 2_097_152, 19 | sector_size: 512 20 | ) 21 | end 22 | 23 | it 'fetches data' do 24 | data = parser.call(real_lines) 25 | expect(data).to eq(expected_data) 26 | end 27 | 28 | it 'fails on empty data' do 29 | expect do 30 | parser.call([]) 31 | end.to raise_error(LuksV1Parser::ParsingError) 32 | end 33 | 34 | it 'fails on incomplete data' do 35 | expect do 36 | parser.call(['Cipher name: ceasar', 'Cipher mode: xts-plain64']) 37 | end.to raise_error(LuksV1Parser::ParsingError) 38 | end 39 | 40 | it 'fails on duplicate data' do 41 | expect do 42 | parser.call(['Cipher name: ceasar', 'Cipher mode: 1', 'Payload offset: 1', 'Cipher mode: 2']) 43 | end.to raise_error(LuksV1Parser::ParsingError) 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/dumper/luks_v2_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | class Dumper 6 | RSpec.describe LuksV2Parser do 7 | subject(:parser) do 8 | described_class.new 9 | end 10 | 11 | let :real_lines do 12 | File.readlines('spec/fixtures/luks_dump/v2.txt').map(&:chomp) 13 | end 14 | 15 | let :expected_data do 16 | Data.new( 17 | cipher: 'aes-xts-plain64', 18 | offset: 16_777_216, 19 | sector_size: 512 20 | ) 21 | end 22 | 23 | it 'fetches data' do 24 | data = parser.call(real_lines) 25 | expect(data).to eq(expected_data) 26 | end 27 | 28 | it 'fails on empty data' do 29 | expect do 30 | parser.call([]) 31 | end.to raise_error(LuksV2Parser::ParsingError) 32 | end 33 | 34 | it 'fails on incomplete data' do 35 | expect do 36 | parser.call(['Data segments:', "\tcipher: ceasar"]) 37 | end.to raise_error(LuksV2Parser::ParsingError) 38 | end 39 | 40 | it 'fails on duplicate data' do 41 | expect do 42 | parser.call(['Data segments:', "\tcipher: ceasar", "\toffset: 1 [bytes]", 43 | "\tsector: 1 [bytes]", "\tcipher: ceasar"]) 44 | end.to raise_error(LuksV2Parser::ParsingError) 45 | end 46 | 47 | it 'fails on data moved to another section' do 48 | expect do 49 | parser.call(['Data segments:', "\tcipher: ceasar", "\toffset: 1 [bytes]", 50 | 'Data segments:', "\tsector: 1 [bytes]"]) 51 | end.to raise_error(LuksV2Parser::ParsingError) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/key_fetcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module Luks 7 | RSpec.describe KeyFetcher do 8 | subject(:fetcher) do 9 | described_class.new(temp_provider: temp_provider) 10 | end 11 | 12 | let :temp_provider do 13 | SafeTemp::FileName.new(dir_provider: Dir.method(:mktmpdir)) 14 | end 15 | 16 | context 'with valid passphrase' do 17 | let(:passphrase) { 'qwe' } 18 | 19 | context 'with LUKS2 header' do 20 | let(:header_file) { 'spec/fixtures/luks_headers/v2.bin' } 21 | 22 | let :expected_key do 23 | "\xeb\xd3\xe2\x08\xe4\x5f\xeb\x4d\x81\xd2\xd8\xc2\x79\xba\x6a\x9f" \ 24 | "\xb0\xf8\xf2\xc4\x98\x9f\xa6\xed\xfb\x95\x52\x22\xe7\x96\x0f\x6d" \ 25 | "\x25\x18\xba\x3a\x36\xed\x07\xa3\xf3\x9e\x93\x79\xb7\x47\xaf\x5d" \ 26 | "\x8f\x87\x30\x56\x21\x76\x8a\x74\x87\x23\x91\xaa\xe4\xa7\xfc\x0b" 27 | end 28 | 29 | it 'fetches key' do 30 | key = fetcher.call(header_file, passphrase) 31 | expect(key).to eq(expected_key) 32 | end 33 | end 34 | 35 | context 'with LUKS1 header' do 36 | let(:header_file) { 'spec/fixtures/luks_headers/v1.bin' } 37 | 38 | let :expected_key do 39 | "\x41\x44\x91\xb7\xef\x52\x84\x4f\xc0\x2a\xf3\xbf\x97\xbb\x0b\x8b" \ 40 | "\x06\x04\x57\xc4\x52\x2f\x7d\xdd\x8c\x2a\x59\x42\x64\xbd\x89\xa3" \ 41 | "\x09\x1c\x6c\xa3\x57\x31\xe0\x59\x82\x6f\xe1\x25\x0b\x9c\xbf\x8c" \ 42 | "\x3c\x16\x68\xc6\xb7\x37\xcc\x0e\x6e\x79\xab\xf5\x47\x3d\x85\xed" 43 | end 44 | 45 | it 'fetches key' do 46 | key = fetcher.call(header_file, passphrase) 47 | expect(key).to eq(expected_key) 48 | end 49 | end 50 | end 51 | 52 | context 'with invalid passphrase' do 53 | let(:passphrase) { 'invalid' } 54 | 55 | context 'with LUKS2 header' do 56 | let(:header_file) { 'spec/fixtures/luks_headers/v2.bin' } 57 | 58 | it 'fails' do 59 | expect do 60 | fetcher.call(header_file, passphrase) 61 | end.to raise_error(KeyFetcher::InvalidPassphrase) 62 | end 63 | end 64 | 65 | context 'with LUKS1 header' do 66 | let(:header_file) { 'spec/fixtures/luks_headers/v1.bin' } 67 | 68 | it 'fails' do 69 | expect do 70 | fetcher.call(header_file, passphrase) 71 | end.to raise_error(KeyFetcher::InvalidPassphrase) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks/version_detector_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Luks 5 | RSpec.describe VersionDetector do 6 | subject(:detector) do 7 | described_class.new(checker: checker, supported_versions: supported_versions) 8 | end 9 | 10 | let :checker do 11 | ->(headevice, version = :any) { device_db.fetch(headevice).include?(version) } 12 | end 13 | 14 | let :device_db do 15 | { 16 | '/dev/v2' => ['LUKS2', :any], 17 | '/dev/v1' => ['LUKS1', :any], 18 | '/dev/any' => [:any], 19 | '/dev/inv' => ['invalid'] 20 | } 21 | end 22 | 23 | context 'with support for LUKS1 & LUKS2' do 24 | let(:supported_versions) { %w[LUKS2 LUKS1] } 25 | 26 | it 'recognizes LUKS2' do 27 | version = detector.call('/dev/v2') 28 | expect(version).to eq('LUKS2') 29 | end 30 | 31 | it 'recognizes LUKS1' do 32 | version = detector.call('/dev/v1') 33 | expect(version).to eq('LUKS1') 34 | end 35 | 36 | it 'fails on invalid header' do 37 | expect do 38 | detector.call('/dev/inv') 39 | end.to raise_error(VersionDetector::NotLuks) 40 | end 41 | end 42 | 43 | context 'with no support for LUKS2' do 44 | let(:supported_versions) { ['LUKS1'] } 45 | 46 | it 'fails on LUKS2' do 47 | expect do 48 | detector.call('/dev/v2') 49 | end.to raise_error(VersionDetector::UnsupportedVersion) 50 | end 51 | 52 | it 'still recognizes LUKS1' do 53 | version = detector.call('/dev/v1') 54 | expect(version).to eq('LUKS1') 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/crypt_reboot/luks_crypt_tab_patcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe LuksCryptTabPatcher do 5 | subject(:patcher) { described_class.new } 6 | 7 | let :expected_patch do 8 | { 9 | '/cryptroot/crypttab' => "# This file has been patched by cryptreboot\n" \ 10 | 'cryptswap /dev/null /dev/urandom ' \ 11 | "swap,plain,offset=1024,cipher=aes-xts-plain64,size=512\n" 12 | } 13 | end 14 | 15 | it do 16 | patch = patcher.call('spec/fixtures/extracted_initramfs/luks_crypt_tab') 17 | expect(patch).to eq(expected_patch) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/crypt_reboot/patched_initramfs_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe PatchedInitramfsGenerator do 5 | subject(:generator) do 6 | described_class.new( 7 | squeezer: ->(_) { { 'file' => 'content' } }, 8 | patcher: ->(image, patch, &block) { block.call("#{image} was patched with #{patch}") } 9 | ) 10 | end 11 | 12 | let(:expected_path) { 'my_initramfs_image was patched with {"file"=>"content"}' } 13 | 14 | it 'yields something' do 15 | expect do |block| 16 | generator.call('my_initramfs_image', &block) 17 | end.to yield_control 18 | end 19 | 20 | it 'yields patched initramfs' do 21 | generator.call('my_initramfs_image') do |path| 22 | expect(path).to eq(expected_path) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/crypt_reboot/rebooter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe Rebooter do 5 | subject(:rebooter) { described_class.new(runner: runner, exiter: exiter) } 6 | 7 | let(:runner) { spy } 8 | let(:exiter) { spy } 9 | 10 | it 'reboots' do 11 | rebooter.call(true) 12 | expect(runner).to have_received(:call) 13 | end 14 | 15 | it 'exits without reboot' do 16 | rebooter.call(false) 17 | expect(exiter).to have_received(:call) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/crypt_reboot/runner/binary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | RSpec.describe Binary do 6 | subject(:runner) do 7 | described_class.new 8 | end 9 | 10 | it 'returns string of zeros' do 11 | result = runner.call('dd', 'if=/dev/zero', 'bs=1c', 'count=8') 12 | expect(result).to eq("\0" * 8) 13 | end 14 | 15 | it 'returns line endings' do 16 | result = runner.call('echo', '-n', '\r\n\r\n\n\r') 17 | expect(result).to eq("\r\n\r\n\n\r") 18 | end 19 | 20 | it 'raises exception on error' do 21 | expect do 22 | runner.call('false') 23 | end.to raise_error(ExitError) 24 | end 25 | 26 | it 'raises exception on not found error' do 27 | expect do 28 | runner.call('dfkjgaksjdgfkajsghd') 29 | end.to raise_error(CommandNotFound) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/crypt_reboot/runner/boolean_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | RSpec.describe Boolean do 6 | subject(:runner) do 7 | described_class.new 8 | end 9 | 10 | it 'returns true on success' do 11 | result = runner.call('true') 12 | expect(result).to be(true) 13 | end 14 | 15 | it 'returns false on failure' do 16 | result = runner.call('false') 17 | expect(result).to be(false) 18 | end 19 | 20 | it 'raises exception on not found error' do 21 | expect do 22 | runner.call('dfkjgaksjdgfkajsghd') 23 | end.to raise_error(CommandNotFound) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/crypt_reboot/runner/lines_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | RSpec.describe Lines do 6 | subject(:runner) do 7 | described_class.new 8 | end 9 | 10 | it 'returns standard output as array of lines' do 11 | result = runner.call('echo "123\n456"') 12 | expect(result).to eq(%w[123 456]) 13 | end 14 | 15 | it 'raises exception on error' do 16 | expect do 17 | runner.call('false') 18 | end.to raise_error(ExitError) 19 | end 20 | 21 | it 'raises exception on not found error' do 22 | expect do 23 | runner.call('dfkjgaksjdgfkajsghd') 24 | end.to raise_error(CommandNotFound) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/crypt_reboot/runner/no_result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module Runner 7 | RSpec.describe NoResult do 8 | subject(:runner) do 9 | described_class.new 10 | end 11 | 12 | it 'ignores output' do 13 | result = runner.call('echo "123\n456"') 14 | expect(result).to be_nil 15 | end 16 | 17 | it 'executes command' do 18 | Dir.mktmpdir do |dir| 19 | tmp_file = File.join(dir, 'file.txt') 20 | runner.call('touch', tmp_file) 21 | expect(File).to exist(tmp_file) 22 | end 23 | end 24 | 25 | it 'raises exception on error' do 26 | expect do 27 | runner.call('false') 28 | end.to raise_error(ExitError) 29 | end 30 | 31 | it 'raises exception on not found error' do 32 | expect do 33 | runner.call('dfkjgaksjdgfkajsghd') 34 | end.to raise_error(CommandNotFound) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/crypt_reboot/runner/text_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module Runner 5 | RSpec.describe Text do 6 | subject(:runner) do 7 | described_class.new 8 | end 9 | 10 | it 'returns standard output as text' do 11 | result = runner.call('echo 123') 12 | expect(result).to eq("123\n") 13 | end 14 | 15 | it 'raises exception on error' do 16 | expect do 17 | runner.call('false') 18 | end.to raise_error(ExitError) 19 | end 20 | 21 | it 'raises exception on not found error' do 22 | expect do 23 | runner.call('dfkjgaksjdgfkajsghd') 24 | end.to raise_error(CommandNotFound) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/crypt_reboot/safe_temp/directory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | module SafeTemp 5 | RSpec.describe Directory do 6 | subject(:tempdir) do 7 | described_class.new(mounter: null_mounter) 8 | end 9 | 10 | let :null_mounter do 11 | ->(_, &block) { block.call } 12 | end 13 | 14 | def return_tempdir_ignoring_exceptions 15 | dir = nil 16 | begin 17 | tempdir.call do |new_dir| 18 | dir = new_dir 19 | yield 20 | end 21 | rescue StandardError 22 | # ignore 23 | end 24 | dir 25 | end 26 | 27 | it 'allows to create a file in temp dir' do 28 | tempdir.call do |dir| 29 | file_path = File.join(dir, 'test.txt') 30 | File.open(file_path, 'w') { 0 } 31 | expect(File).to exist(file_path) 32 | end 33 | end 34 | 35 | it 'removes the dir after block finishes' do 36 | dir = tempdir.call { |new_dir| new_dir } 37 | expect(File).not_to exist(dir) 38 | end 39 | 40 | it 'propagates exception' do 41 | expect do 42 | tempdir.call do 43 | raise StandardError 44 | end 45 | end.to raise_error(StandardError) 46 | end 47 | 48 | it 'ensures the dir is removed in case of exception' do 49 | dir = return_tempdir_ignoring_exceptions do 50 | raise StandardError 51 | end 52 | expect(File).not_to exist(dir) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/crypt_reboot/safe_temp/mounter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tmpdir' 4 | 5 | module CryptReboot 6 | module SafeTemp 7 | RSpec.describe Mounter do 8 | def ignore_exception 9 | yield 10 | rescue StandardError 11 | # ignore 12 | end 13 | 14 | it 'mounts, yields and unmounts' do 15 | ops = [] 16 | mounter = described_class.new(mounter: ->(dir) { ops += ['mount', dir] }, 17 | umounter: ->(dir) { ops += ['umount', dir] }) 18 | mounter.call('d1') { ops << 'yield' } 19 | expect(ops).to eq(%w[mount d1 yield umount d1]) 20 | end 21 | 22 | it 'mounts and unmounts, even in case of exception in block' do 23 | ops = [] 24 | mounter = described_class.new(mounter: ->(dir) { ops += ['mount', dir] }, 25 | umounter: ->(dir) { ops += ['umount', dir] }) 26 | ignore_exception { mounter.call('d1') { raise StandardError } } 27 | expect(ops).to eq(%w[mount d1 umount d1]) 28 | end 29 | 30 | it 'does not yield or unmount if mount failed' do 31 | ops = [] 32 | mounter = described_class.new(mounter: proc { raise StandardError }, 33 | umounter: ->(dir) { ops += ['umount', dir] }) 34 | ignore_exception { mounter.call('d1') { ops << 'yield' } } 35 | expect(ops).to eq(%w[]) 36 | end 37 | 38 | it 'mounts, yields and raises because of unmounting failure' do 39 | ops = [] 40 | mounter = described_class.new(mounter: ->(dir) { ops += ['mount', dir] }, 41 | umounter: proc { raise StandardError }) 42 | ignore_exception { mounter.call('d1') { ops << 'yield' } } 43 | expect(ops).to eq(%w[mount d1 yield]) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/crypt_reboot/single_assign_restricted_map_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe SingleAssignRestrictedMap do 5 | subject(:map) do 6 | described_class.new 7 | end 8 | 9 | it 'returns hash if all required fields were passed' do 10 | map[:field1] = 1 11 | map[:field2] = 2 12 | expect(map.to_h).to eq({ field1: 1, field2: 2 }) 13 | end 14 | 15 | it 'fails when trying to assign twice' do 16 | map[:field1] = 1 17 | expect do 18 | map[:field1] = 2 19 | end.to raise_error(SingleAssignRestrictedMap::AlreadyAssigned) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/crypt_reboot/zfs_keystore_patcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CryptReboot 4 | RSpec.describe ZfsKeystorePatcher do 5 | subject(:patcher) do 6 | described_class.new entries_generator: entries_generator, files_generator: files_generator 7 | end 8 | 9 | let :entries_generator do 10 | -> { [double] } 11 | end 12 | 13 | let :files_generator do 14 | ->(*_, **_) { { '/cryptreboot/zfs_crypttab' => '# Dummy file' } } 15 | end 16 | 17 | let :expected_patch do 18 | { 19 | '/cryptreboot/zfs_crypttab' => '# Dummy file', 20 | # rubocop:disable Layout/LineContinuationLeadingSpace 21 | '/scripts/zfs' => "# Simplified ZFS boot script\n" \ 22 | "CRYPTROOT=/scripts/local-top/cryptroot\n" \ 23 | "if [ true ]\n" \ 24 | "\n" \ 25 | " # Following line has been added by cryptreboot\n" \ 26 | " cp /cryptreboot/zfs_crypttab /cryptroot/crypttab\n" \ 27 | "\n" \ 28 | " ${CRYPTROOT}\n" \ 29 | "fi\n" 30 | # rubocop:enable Layout/LineContinuationLeadingSpace 31 | } 32 | end 33 | 34 | it do 35 | patch = patcher.call('spec/fixtures/extracted_initramfs/zfs_keystore') 36 | expect(patch).to eq(expected_patch) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/crypt_reboot_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe CryptReboot do 4 | it 'has a version number' do 5 | expect(CryptReboot::VERSION).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/archiver/files/file1.txt: -------------------------------------------------------------------------------- 1 | testfile1 2 | -------------------------------------------------------------------------------- /spec/fixtures/concatenator/f1.txt: -------------------------------------------------------------------------------- 1 | f1 2 | -------------------------------------------------------------------------------- /spec/fixtures/concatenator/f2.txt: -------------------------------------------------------------------------------- 1 | f2 2 | -------------------------------------------------------------------------------- /spec/fixtures/dummy_initramfs: -------------------------------------------------------------------------------- 1 | not real 2 | -------------------------------------------------------------------------------- /spec/fixtures/extracted_initramfs/luks_crypt_tab/cryptroot/crypttab: -------------------------------------------------------------------------------- 1 | # No LUKS volumes, because we don't want asking for passphrase 2 | cryptswap /dev/null /dev/urandom swap,plain,offset=1024,cipher=aes-xts-plain64,size=512 3 | -------------------------------------------------------------------------------- /spec/fixtures/extracted_initramfs/zfs_keystore/scripts/zfs: -------------------------------------------------------------------------------- 1 | # Simplified ZFS boot script 2 | CRYPTROOT=/scripts/local-top/cryptroot 3 | if [ true ] 4 | ${CRYPTROOT} 5 | fi 6 | -------------------------------------------------------------------------------- /spec/fixtures/liberal_cat: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cat 3 | -------------------------------------------------------------------------------- /spec/fixtures/luks_dump/v1.txt: -------------------------------------------------------------------------------- 1 | LUKS header information for spec/fixtures/luks_headers/v1.bin 2 | 3 | Version: 1 4 | Cipher name: aes 5 | Cipher mode: xts-plain64 6 | Hash spec: sha256 7 | Payload offset: 4096 8 | MK bits: 512 9 | MK digest: 63 b6 18 a1 14 ca 26 a0 a8 7b 5e ff 38 ef 32 6d af 22 21 93 10 | MK salt: 8b eb af 11 a8 89 81 45 3b c9 7a 9e b2 80 92 81 11 | 7c d9 26 a6 37 0a 53 48 8d 77 69 06 c7 ec 71 64 12 | MK iterations: 136961 13 | UUID: 07ecf68b-0c15-4544-b51a-df0f46c002f8 14 | 15 | Key Slot 0: DISABLED 16 | Key Slot 1: ENABLED 17 | Iterations: 1000 18 | Salt: 3d ec f3 07 67 bf f0 2c 1c 8a dc a0 ac 31 0e 25 19 | 42 a7 02 14 7a 75 0a 0a 40 73 80 04 d9 a0 bb 2d 20 | Key material offset: 512 21 | AF stripes: 4000 22 | Key Slot 2: DISABLED 23 | Key Slot 3: DISABLED 24 | Key Slot 4: DISABLED 25 | Key Slot 5: DISABLED 26 | Key Slot 6: DISABLED 27 | Key Slot 7: DISABLED 28 | -------------------------------------------------------------------------------- /spec/fixtures/luks_dump/v2.txt: -------------------------------------------------------------------------------- 1 | LUKS header information 2 | Version: 2 3 | Epoch: 9 4 | Metadata area: 16384 [bytes] 5 | Keyslots area: 16744448 [bytes] 6 | UUID: 4c54c4c6-e240-4b3a-8270-254cda2cda21 7 | Label: (no label) 8 | Subsystem: (no subsystem) 9 | Flags: (no flags) 10 | 11 | Data segments: 12 | 0: crypt 13 | offset: 16777216 [bytes] 14 | length: (whole device) 15 | cipher: aes-xts-plain64 16 | sector: 512 [bytes] 17 | 18 | Keyslots: 19 | 0: luks2 20 | Key: 512 bits 21 | Priority: normal 22 | Cipher: aes-xts-plain64 23 | Cipher key: 512 bits 24 | PBKDF: pbkdf2 25 | Hash: sha256 26 | Iterations: 1000 27 | Salt: 6e ab c2 5e 79 de 23 c3 33 e9 6a fc c9 c9 60 1c 28 | 4a 71 eb 4d 8f 68 d4 c8 5b 1d 71 77 e1 5b 5f 81 29 | AF stripes: 4000 30 | AF hash: sha256 31 | Area offset:290816 [bytes] 32 | Area length:258048 [bytes] 33 | Digest ID: 0 34 | Tokens: 35 | Digests: 36 | 0: pbkdf2 37 | Hash: sha256 38 | Iterations: 142006 39 | Salt: 90 96 24 71 fc b6 b8 fb 49 04 64 86 b5 12 85 b2 40 | b4 94 1a e9 61 fe 16 02 6f 1d 32 d1 51 42 6d a3 41 | Digest: fd a7 fc ec d7 2f b2 fa 45 62 d4 b4 ee 64 2f 2c 42 | 84 25 1b 38 8d da 5b 3e ce be d3 de 91 2c b3 63 43 | -------------------------------------------------------------------------------- /spec/fixtures/luks_headers/invalid.bin: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /spec/fixtures/luks_headers/v1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-node/cryptreboot/893c30cbccce2b28468d81a59443f8d6f55027d9/spec/fixtures/luks_headers/v1.bin -------------------------------------------------------------------------------- /spec/fixtures/luks_headers/v2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-node/cryptreboot/893c30cbccce2b28468d81a59443f8d6f55027d9/spec/fixtures/luks_headers/v2.bin -------------------------------------------------------------------------------- /spec/fixtures/zfs_dev/zvol/dummy/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-node/cryptreboot/893c30cbccce2b28468d81a59443f8d6f55027d9/spec/fixtures/zfs_dev/zvol/dummy/dummy -------------------------------------------------------------------------------- /spec/fixtures/zfs_dev/zvol/rpool/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-node/cryptreboot/893c30cbccce2b28468d81a59443f8d6f55027d9/spec/fixtures/zfs_dev/zvol/rpool/dummy -------------------------------------------------------------------------------- /spec/fixtures/zfs_dev/zvol/rpool/keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom-node/cryptreboot/893c30cbccce2b28468d81a59443f8d6f55027d9/spec/fixtures/zfs_dev/zvol/rpool/keystore -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'crypt_reboot' 5 | 6 | CryptReboot::Config.update!(debug: true) 7 | MemoryLocker = -> {} # memory locking is not required for testing 8 | 9 | RSpec.configure do |config| 10 | # Enable flags like --only-failures and --next-failure 11 | config.example_status_persistence_file_path = '.rspec_status' 12 | 13 | # Disable RSpec exposing methods globally on `Module` and `main` 14 | config.disable_monkey_patching! 15 | 16 | config.expect_with :rspec do |c| 17 | c.syntax = :expect 18 | end 19 | end 20 | --------------------------------------------------------------------------------