├── .cargo └── config.toml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Readme.md ├── Todo.md ├── boot-lib ├── Cargo.toml └── src │ ├── crypt.rs │ └── lib.rs ├── boot-rs ├── Cargo.toml └── src │ └── main.rs ├── boot-strap ├── Cargo.toml └── src │ ├── app.rs │ ├── initramfs.rs │ └── main.rs ├── boot.cfg.tst ├── build_boot.sh ├── build_init.sh ├── build_strap.sh ├── initramfs-lib ├── Cargo.toml └── src │ ├── error.rs │ ├── lib.rs │ └── print.rs ├── initramfs-rs ├── Cargo.toml └── src │ ├── app.rs │ └── main.rs ├── new-boot-src.sh └── rust-toolchain.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [profile.dev] 2 | panic = "abort" # No std requirement 3 | 4 | [profile.release] 5 | panic = "abort" # No std requirement 6 | 7 | [profile.lto] 8 | panic = "abort" # No std requirement 9 | codegen-units = 1 # Better optimization 10 | debug = false # Inherits from release so should actually do nothing but whatever 11 | inherits = "release" 12 | lto = true # link time optimization 13 | strip = true # smaller binary 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | *.iml -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aead" 7 | version = "0.5.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 10 | dependencies = [ 11 | "crypto-common", 12 | "generic-array", 13 | ] 14 | 15 | [[package]] 16 | name = "aes" 17 | version = "0.8.2" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" 20 | dependencies = [ 21 | "cfg-if", 22 | "cipher", 23 | "cpufeatures", 24 | ] 25 | 26 | [[package]] 27 | name = "aes-gcm" 28 | version = "0.10.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 31 | dependencies = [ 32 | "aead", 33 | "aes", 34 | "cipher", 35 | "ctr", 36 | "ghash", 37 | "subtle", 38 | ] 39 | 40 | [[package]] 41 | name = "argon2" 42 | version = "0.5.3" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" 45 | dependencies = [ 46 | "base64ct", 47 | "blake2", 48 | "cpufeatures", 49 | "password-hash", 50 | ] 51 | 52 | [[package]] 53 | name = "base64ct" 54 | version = "1.6.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 57 | 58 | [[package]] 59 | name = "bit_field" 60 | version = "0.10.2" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.3.3" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 69 | 70 | [[package]] 71 | name = "blake2" 72 | version = "0.10.6" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" 75 | dependencies = [ 76 | "digest", 77 | ] 78 | 79 | [[package]] 80 | name = "block-buffer" 81 | version = "0.10.3" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 84 | dependencies = [ 85 | "generic-array", 86 | ] 87 | 88 | [[package]] 89 | name = "boot-lib" 90 | version = "0.1.0" 91 | dependencies = [ 92 | "aes-gcm", 93 | "argon2", 94 | ] 95 | 96 | [[package]] 97 | name = "boot-rs" 98 | version = "0.1.0" 99 | dependencies = [ 100 | "boot-lib", 101 | "uefi", 102 | ] 103 | 104 | [[package]] 105 | name = "boot-strap" 106 | version = "0.1.0" 107 | dependencies = [ 108 | "boot-lib", 109 | "hex", 110 | "initramfs-lib", 111 | "rusl", 112 | "tiny-cli", 113 | "tiny-std", 114 | ] 115 | 116 | [[package]] 117 | name = "cfg-if" 118 | version = "1.0.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 121 | 122 | [[package]] 123 | name = "cipher" 124 | version = "0.4.3" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" 127 | dependencies = [ 128 | "crypto-common", 129 | "inout", 130 | ] 131 | 132 | [[package]] 133 | name = "cpufeatures" 134 | version = "0.2.12" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 137 | dependencies = [ 138 | "libc", 139 | ] 140 | 141 | [[package]] 142 | name = "crypto-common" 143 | version = "0.1.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 146 | dependencies = [ 147 | "generic-array", 148 | "typenum", 149 | ] 150 | 151 | [[package]] 152 | name = "ctr" 153 | version = "0.9.2" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 156 | dependencies = [ 157 | "cipher", 158 | ] 159 | 160 | [[package]] 161 | name = "digest" 162 | version = "0.10.6" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 165 | dependencies = [ 166 | "block-buffer", 167 | "crypto-common", 168 | "subtle", 169 | ] 170 | 171 | [[package]] 172 | name = "generic-array" 173 | version = "0.14.6" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 176 | dependencies = [ 177 | "typenum", 178 | "version_check", 179 | ] 180 | 181 | [[package]] 182 | name = "ghash" 183 | version = "0.5.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" 186 | dependencies = [ 187 | "opaque-debug", 188 | "polyval", 189 | ] 190 | 191 | [[package]] 192 | name = "hex" 193 | version = "0.4.3" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 196 | 197 | [[package]] 198 | name = "initramfs-lib" 199 | version = "0.1.0" 200 | dependencies = [ 201 | "rusl", 202 | "tiny-std", 203 | ] 204 | 205 | [[package]] 206 | name = "initramfs-rs" 207 | version = "0.1.0" 208 | dependencies = [ 209 | "initramfs-lib", 210 | "rusl", 211 | "tiny-std", 212 | ] 213 | 214 | [[package]] 215 | name = "inout" 216 | version = "0.1.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 219 | dependencies = [ 220 | "generic-array", 221 | ] 222 | 223 | [[package]] 224 | name = "libc" 225 | version = "0.2.155" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 228 | 229 | [[package]] 230 | name = "linux-rust-bindings" 231 | version = "0.1.3" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "109280d660e557dfa6474e3b1bbe04272ba3fa5c5e5ce73b394c7b224f082caa" 234 | 235 | [[package]] 236 | name = "log" 237 | version = "0.4.17" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 240 | dependencies = [ 241 | "cfg-if", 242 | ] 243 | 244 | [[package]] 245 | name = "opaque-debug" 246 | version = "0.3.0" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 249 | 250 | [[package]] 251 | name = "password-hash" 252 | version = "0.5.0" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" 255 | dependencies = [ 256 | "base64ct", 257 | "rand_core", 258 | "subtle", 259 | ] 260 | 261 | [[package]] 262 | name = "polyval" 263 | version = "0.6.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" 266 | dependencies = [ 267 | "cfg-if", 268 | "cpufeatures", 269 | "opaque-debug", 270 | "universal-hash", 271 | ] 272 | 273 | [[package]] 274 | name = "proc-macro2" 275 | version = "1.0.66" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 278 | dependencies = [ 279 | "unicode-ident", 280 | ] 281 | 282 | [[package]] 283 | name = "ptr_meta" 284 | version = "0.2.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "bcada80daa06c42ed5f48c9a043865edea5dc44cbf9ac009fda3b89526e28607" 287 | dependencies = [ 288 | "ptr_meta_derive", 289 | ] 290 | 291 | [[package]] 292 | name = "ptr_meta_derive" 293 | version = "0.2.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "bca9224df2e20e7c5548aeb5f110a0f3b77ef05f8585139b7148b59056168ed2" 296 | dependencies = [ 297 | "proc-macro2", 298 | "quote", 299 | "syn 1.0.109", 300 | ] 301 | 302 | [[package]] 303 | name = "quote" 304 | version = "1.0.32" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 307 | dependencies = [ 308 | "proc-macro2", 309 | ] 310 | 311 | [[package]] 312 | name = "rand_core" 313 | version = "0.6.4" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 316 | 317 | [[package]] 318 | name = "rusl" 319 | version = "0.3.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "37cef3b7f9e145a892eede62669c682351b12d2c5152d8428cab41d63c62f8aa" 322 | dependencies = [ 323 | "linux-rust-bindings", 324 | "sc", 325 | ] 326 | 327 | [[package]] 328 | name = "sc" 329 | version = "0.2.7" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "010e18bd3bfd1d45a7e666b236c78720df0d9a7698ebaa9c1c559961eb60a38b" 332 | 333 | [[package]] 334 | name = "subtle" 335 | version = "2.4.1" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 338 | 339 | [[package]] 340 | name = "syn" 341 | version = "1.0.109" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 344 | dependencies = [ 345 | "proc-macro2", 346 | "quote", 347 | "unicode-ident", 348 | ] 349 | 350 | [[package]] 351 | name = "syn" 352 | version = "2.0.28" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" 355 | dependencies = [ 356 | "proc-macro2", 357 | "quote", 358 | "unicode-ident", 359 | ] 360 | 361 | [[package]] 362 | name = "tiny-cli" 363 | version = "0.2.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "207482065611d87e72914213b2c543be93d28f152c67149ac5fc76ddfadef3f4" 366 | 367 | [[package]] 368 | name = "tiny-start" 369 | version = "0.1.3" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "de055a37ee430d2d0ded2ecbaf49cafc46316a9f64f144028fea4ec676b3807c" 372 | dependencies = [ 373 | "rusl", 374 | ] 375 | 376 | [[package]] 377 | name = "tiny-std" 378 | version = "0.2.4" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "7746a3b6b64a7dbd7e77ba3040dfce6ef3f693aedb0f08ab60b7026f8374cee7" 381 | dependencies = [ 382 | "rusl", 383 | "sc", 384 | "tiny-start", 385 | ] 386 | 387 | [[package]] 388 | name = "typenum" 389 | version = "1.16.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 392 | 393 | [[package]] 394 | name = "ucs2" 395 | version = "0.3.2" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "bad643914094137d475641b6bab89462505316ec2ce70907ad20102d28a79ab8" 398 | dependencies = [ 399 | "bit_field", 400 | ] 401 | 402 | [[package]] 403 | name = "uefi" 404 | version = "0.28.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "a9c0a56dc9fed2589aad6ddca11c2584968fc21f227b5d7083bb8961d26a69fa" 407 | dependencies = [ 408 | "bitflags", 409 | "cfg-if", 410 | "log", 411 | "ptr_meta", 412 | "ucs2", 413 | "uefi-macros", 414 | "uefi-raw", 415 | "uguid", 416 | ] 417 | 418 | [[package]] 419 | name = "uefi-macros" 420 | version = "0.13.0" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "26a7b1c2c808c3db854a54d5215e3f7e7aaf5dcfbce095598cba6af29895695d" 423 | dependencies = [ 424 | "proc-macro2", 425 | "quote", 426 | "syn 2.0.28", 427 | ] 428 | 429 | [[package]] 430 | name = "uefi-raw" 431 | version = "0.5.2" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "efa8716f52e8cab8bcedfd5052388a0f263b69fe5cc2561548dc6a530678333c" 434 | dependencies = [ 435 | "bitflags", 436 | "ptr_meta", 437 | "uguid", 438 | ] 439 | 440 | [[package]] 441 | name = "uguid" 442 | version = "2.1.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "1ef516f0806c5f61da6aa95125d0eb2d91cc95b2df426c06bde8be657282aee5" 445 | 446 | [[package]] 447 | name = "unicode-ident" 448 | version = "1.0.8" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 451 | 452 | [[package]] 453 | name = "universal-hash" 454 | version = "0.5.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 457 | dependencies = [ 458 | "crypto-common", 459 | "subtle", 460 | ] 461 | 462 | [[package]] 463 | name = "version_check" 464 | version = "0.9.4" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 467 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["boot-lib", "boot-rs", "boot-strap", "initramfs-lib", "initramfs-rs"] 4 | 5 | [workspace.dependencies] 6 | boot-lib = { path = "boot-lib" } 7 | initramfs-lib = { path = "initramfs-lib" } 8 | 9 | aes-gcm = { version = "0.10.3", default-features = false, features = ["alloc", "aes"] } 10 | argon2 = { version = "0.5.3", default-features = false, features = ["alloc"] } 11 | hex = { version = "0.4.3", default-features = false } 12 | rusl = { version = "0.3.0", default-features = false} 13 | tiny-cli = { version = "0.2.1" } 14 | tiny-std = { version = "0.2.4", default-features = false } 15 | uefi = { version = "0.28.0", features = ["alloc", "global_allocator", "panic_handler"] } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Boot-rs 2 | A collection of tools to create an ergonomic and secure 3 | encrypted boot-process. 4 | 5 | ## Considerations 6 | How do you make sure that when your kernel boots that it hasn't been 7 | compromised? 8 | 9 | An answer to that question could be to have it encrypted, if the bootloader 10 | can encrypt and then launch into it, then it hasn't been compromised. As 11 | long as the kernel that boots is actually your kernel, and not a replacement. 12 | 13 | That brings the question to whether the bootloader can be trusted, it can't. 14 | You can use secure-boot to make sure that it's indeed your bootloaded that runs. 15 | Another solution is to embed your disk-decryption keys in the initramfs of your 16 | encrypted kernel. 17 | This way you know it's indeed your kernel that is running, since it knows how 18 | to decrypt your disks. 19 | Something that's problematic about that is that a malicious bootloader could 20 | for example record your decryption key, use it to decrypt your kernel, 21 | get your disk decryption keys from the initramfs, launch a malicious kernel, 22 | and decrypt your disks with the stolen keys. So secure boot is likely 23 | the best way to go. 24 | 25 | ## Functionality 26 | This project's overall functionality. 27 | 28 | ### Boot-pre-os 29 | This project contains a bootloader which can decrypt the kernel after 30 | generating configuration using the cli `boot-strap` which is compiled into the bootloader. 31 | The bootloader can then be signed, again using `boot-strap`, if that certificate is added, and 32 | secure boot enabled. The very early boot process should be safe. 33 | 34 | ### Os-boot 35 | The bootloader decrypts the kernel and launches the image. 36 | The initramfs, which can be generated by `boot-strap`, then takes over 37 | and decrypts your disks using the keys saved in the initramfs. 38 | The initramfs is loaded into ram by the kernel, and removed after the early-os-boot process, 39 | when control is handed over to `/sbin/init`. After load and before removal, the keys reside in RAM and 40 | are vulnerable to low-level attacks. 41 | 42 | ## Setup 43 | There are a few steps required to set up `boot-rs`. 44 | 45 | ### Generate initramfs 46 | The initramfs can be generated by `boot-strap initramfs -i initramfs.cfg -d initramfs`. 47 | This will generate an initramfs directory with the appropriate files to unlock a cryptodisk. 48 | 49 | Test by running `cryptsetup luksOpen --key-file initramfs/crypt.key --test-passphrase `. 50 | 51 | To get `initramfs-rs` as the init executable, compile it by `./build_init -r` then copy it to the initramfs directory as `init`. 52 | Ex: `cp target/release/initramfs-rs /root/initramfs/init`. 53 | 54 | ### Build kernel 55 | Build the kernel as usual, but with modules built-in, and with the above generated initramfs built in. 56 | 57 | ### Encrypt kernel 58 | The kernel can then be encrypted using ex: 59 | `boot-strap boot -i /usr/src/linux/arch/x86/boot/bzImage -k /boot/EFI/gentoo/gentoo-6.1.19.enc -c boot.cfg 60 | -d "HD(1,GPT,f0054eea-adf8-4956-958f-12e353cac4c8,0x800,0x100000)" -p /EFI/gentoo/gentoo-6.1.19.enc`. 61 | This will move the encrypted kernel into `/boot/EFI/gentoo/gentoo-6.1.19.enc`. 62 | 63 | ### Build bootloader 64 | Encrypting the kernel generates a configuration file. 65 | Build `boot-rs` with that file included: 66 | 67 | `./build-boot --profile lto` 68 | 69 | ### Sign the bootloader 70 | Sign the bootloader using any preferred method, and/or copy it directly onto the boot disk ex: 71 | `cp target/x86_64-unknown-uefi/lto/boot-rs.efi /boot/EFI/boot-rs.efi`. 72 | 73 | ### Add a boot entry if one does not already exist 74 | `efibootmgr -c -L "Boot-rs" -l "\EFI\boot-rs.efi" -d /dev/sda1` 75 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | # More ergonomic 2 | Ideally we'd only require a single build pass to get everything in place, but since the initramfs is being 3 | compiled into the kernel that's not possible. 4 | It could at least be improved though. 5 | # Maybe make the header bit-rot resistant 6 | Having bit-rot ruin the kernel image isn't a complete disaster but pretty annoying, 7 | especially if you don't get to know why. -------------------------------------------------------------------------------- /boot-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boot-lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | aes-gcm = { workspace = true, default-features = false, features = ["alloc", "aes"] } 10 | argon2 = { workspace = true, default-features = false, features = ["alloc"] } -------------------------------------------------------------------------------- /boot-lib/src/crypt.rs: -------------------------------------------------------------------------------- 1 | use aes_gcm::aead::consts::U32; 2 | use aes_gcm::aead::generic_array::GenericArray; 3 | use aes_gcm::aead::{Aead, KeyInit, Payload}; 4 | use aes_gcm::{Aes256Gcm, Nonce}; 5 | use alloc::format; 6 | use alloc::string::{String, ToString}; 7 | use alloc::vec::Vec; 8 | use argon2::{Algorithm, Params, Version}; 9 | 10 | pub const REQUIRED_HASH_LENGTH: usize = 32; 11 | pub const REQUIRED_NONCE_LENGTH: usize = 12; 12 | pub const ARGON2_CFG_LENGTH: usize = 12; 13 | pub const METADATA_LENGTH: usize = REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH + ARGON2_CFG_LENGTH; 14 | pub const AES_GCM_TAG_LENGTH: usize = 16; 15 | pub const MAGIC: [u8; 16] = *b"DECRYPTED_KERNEL"; 16 | 17 | #[cfg_attr(test, derive(Eq, PartialEq, Debug))] 18 | pub struct Argon2Cfg { 19 | pub lanes: u32, 20 | pub mem_cost: u32, 21 | pub time_cost: u32, 22 | } 23 | 24 | impl Argon2Cfg { 25 | fn serialize(&self) -> [u8; ARGON2_CFG_LENGTH] { 26 | let mut serialized = [0u8; ARGON2_CFG_LENGTH]; 27 | serialized[0..4].copy_from_slice(&self.lanes.to_le_bytes()); 28 | serialized[4..8].copy_from_slice(&self.mem_cost.to_le_bytes()); 29 | serialized[8..12].copy_from_slice(&self.time_cost.to_le_bytes()); 30 | serialized 31 | } 32 | 33 | fn deserialize(bytes: &[u8; ARGON2_CFG_LENGTH]) -> Self { 34 | Self { 35 | lanes: u32::from_le_bytes(bytes[0..4].try_into().unwrap()), 36 | mem_cost: u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 37 | time_cost: u32::from_le_bytes(bytes[8..12].try_into().unwrap()), 38 | } 39 | } 40 | } 41 | 42 | pub const DEFAULT_CONFIG: Argon2Cfg = Argon2Cfg { 43 | lanes: 4, 44 | mem_cost: 65536, 45 | time_cost: 10, 46 | }; 47 | 48 | pub struct AesKey(pub [u8; REQUIRED_HASH_LENGTH]); 49 | 50 | #[cfg_attr(test, derive(Eq, PartialEq, Debug))] 51 | pub struct Argon2Salt(pub [u8; REQUIRED_HASH_LENGTH]); 52 | 53 | #[cfg_attr(test, derive(Eq, PartialEq, Debug))] 54 | pub struct AesGcmNonce(pub [u8; REQUIRED_NONCE_LENGTH]); 55 | 56 | /// Derive a key with the provided salt. 57 | /// # Errors 58 | /// Hashing failure. 59 | pub fn derive_key( 60 | pass: &[u8], 61 | salt: &Argon2Salt, 62 | argon2_cfg: &Argon2Cfg, 63 | ) -> Result { 64 | let mut key = [0u8; REQUIRED_HASH_LENGTH]; 65 | argon2::Argon2::new( 66 | Algorithm::Argon2i, 67 | Version::V0x13, 68 | Params::new( 69 | argon2_cfg.mem_cost, 70 | argon2_cfg.time_cost, 71 | argon2_cfg.lanes, 72 | Some(REQUIRED_HASH_LENGTH), 73 | ) 74 | .map_err(|e| format!("ERROR: Failed to instantiate argon2 parameters: {e}"))?, 75 | ) 76 | .hash_password_into(pass, &salt.0, &mut key) 77 | .map_err(|e| format!("ERROR: Failed to hash password: {e}"))?; 78 | Ok(AesKey(key)) 79 | } 80 | 81 | #[cfg_attr(test, derive(Eq, PartialEq, Debug))] 82 | pub struct EncryptionMetadata { 83 | nonce: AesGcmNonce, 84 | salt: Argon2Salt, 85 | argon2_cfg: Argon2Cfg, 86 | } 87 | 88 | impl EncryptionMetadata { 89 | #[must_use] 90 | pub fn new( 91 | nonce: [u8; REQUIRED_NONCE_LENGTH], 92 | salt: [u8; REQUIRED_HASH_LENGTH], 93 | argon2_cfg: Argon2Cfg, 94 | ) -> Self { 95 | Self { 96 | nonce: AesGcmNonce(nonce), 97 | salt: Argon2Salt(salt), 98 | argon2_cfg, 99 | } 100 | } 101 | 102 | #[must_use] 103 | pub fn serialize(&self) -> [u8; METADATA_LENGTH] { 104 | let mut metadata = [0u8; METADATA_LENGTH]; 105 | metadata[0..REQUIRED_NONCE_LENGTH].copy_from_slice(&self.nonce.0); 106 | metadata[REQUIRED_NONCE_LENGTH..REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH] 107 | .copy_from_slice(&self.salt.0); 108 | metadata[REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH 109 | ..REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH + ARGON2_CFG_LENGTH] 110 | .copy_from_slice(&self.argon2_cfg.serialize()); 111 | metadata 112 | } 113 | 114 | /// # Panics 115 | /// If the array bounds are manually written to be off 116 | #[must_use] 117 | pub fn deserialize(bytes: &[u8; METADATA_LENGTH]) -> Self { 118 | let nonce: [u8; REQUIRED_NONCE_LENGTH] = 119 | bytes[0..REQUIRED_NONCE_LENGTH].try_into().unwrap(); 120 | let salt: [u8; REQUIRED_HASH_LENGTH] = bytes 121 | [REQUIRED_NONCE_LENGTH..REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH] 122 | .try_into() 123 | .unwrap(); 124 | let argon2_cfg = Argon2Cfg::deserialize( 125 | &bytes[REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH 126 | ..REQUIRED_NONCE_LENGTH + REQUIRED_HASH_LENGTH + ARGON2_CFG_LENGTH] 127 | .try_into() 128 | .unwrap(), 129 | ); 130 | Self { 131 | nonce: AesGcmNonce(nonce), 132 | salt: Argon2Salt(salt), 133 | argon2_cfg, 134 | } 135 | } 136 | 137 | #[inline] 138 | #[must_use] 139 | pub fn nonce(&self) -> &AesGcmNonce { 140 | &self.nonce 141 | } 142 | 143 | #[inline] 144 | #[must_use] 145 | pub fn salt(&self) -> &Argon2Salt { 146 | &self.salt 147 | } 148 | 149 | #[inline] 150 | #[must_use] 151 | pub fn argon2_cfg(&self) -> &Argon2Cfg { 152 | &self.argon2_cfg 153 | } 154 | } 155 | 156 | /// Encrypts some input with AES-GCM. 157 | /// # Errors 158 | /// Invalid key or invalid cipher. 159 | pub fn encrypt(src: &[u8], key: &AesKey, metadata: &EncryptionMetadata) -> Result, String> { 160 | let key: &GenericArray = GenericArray::from_slice(&key.0); 161 | let nonce = Nonce::from_slice(&metadata.nonce.0); 162 | 163 | let mut enc = Vec::with_capacity(src.len() + AES_GCM_TAG_LENGTH + METADATA_LENGTH); 164 | enc.extend_from_slice(&metadata.serialize()); 165 | 166 | let output = Aes256Gcm::new(key) 167 | .encrypt(nonce, Payload::from(src)) 168 | .map_err(|e| format!("Encryption failed: {e}"))?; 169 | enc.extend(output); 170 | Ok(enc) 171 | } 172 | 173 | /// Same as `hash_and_decrypt_boot` arguments supplied differently. 174 | /// # Errors 175 | /// See `hash_and_decrypt_boot` 176 | /// # Panics 177 | /// If the manual array bounds are off 178 | pub fn hash_and_decrypt(src: &[u8], pass: &[u8]) -> Result, BootDecryptError> { 179 | let metadata_bytes: &[u8; METADATA_LENGTH] = src 180 | .get(..METADATA_LENGTH) 181 | .ok_or_else(|| { 182 | BootDecryptError::Other("Bad payload, length doesn't even fit metadata".to_string()) 183 | })? 184 | .try_into() 185 | .unwrap(); 186 | let metadata = EncryptionMetadata::deserialize(metadata_bytes); 187 | let key = derive_key(pass, &metadata.salt, &metadata.argon2_cfg).map_err(|e| { 188 | BootDecryptError::Other(format!( 189 | "ERROR: Failed to hash password with provided salt: {e}" 190 | )) 191 | })?; 192 | decrypt(src, &key, &metadata.nonce) 193 | } 194 | 195 | #[derive(Debug, Eq, PartialEq)] 196 | pub enum BootDecryptError { 197 | /// Failed to decrypt into expected content. 198 | /// It could also be because of tampering or corruption. 199 | InvalidContent, 200 | /// Some error during the process of data conversion, would likely not be caused by the wrong 201 | /// key. 202 | Other(String), 203 | } 204 | 205 | fn decrypt(src: &[u8], key: &AesKey, iv: &AesGcmNonce) -> Result, BootDecryptError> { 206 | let key: &GenericArray = GenericArray::from_slice(&key.0); 207 | let nonce = Nonce::from_slice(&iv.0); 208 | let decrypted = Aes256Gcm::new(key) 209 | .decrypt(nonce, &src[METADATA_LENGTH..]) 210 | .map_err(|_e| BootDecryptError::InvalidContent)?; 211 | Ok(decrypted) 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use crate::crypt::{ 217 | decrypt, derive_key, encrypt, hash_and_decrypt, Argon2Cfg, EncryptionMetadata, 218 | DEFAULT_CONFIG, REQUIRED_HASH_LENGTH, REQUIRED_NONCE_LENGTH, 219 | }; 220 | 221 | #[test] 222 | fn metadata_serialize_deserialize() { 223 | let enc = EncryptionMetadata::new( 224 | [5u8; REQUIRED_NONCE_LENGTH], 225 | [7u8; REQUIRED_HASH_LENGTH], 226 | DEFAULT_CONFIG, 227 | ); 228 | let ser = enc.serialize(); 229 | let de = EncryptionMetadata::deserialize(&ser); 230 | assert_eq!(enc, de); 231 | } 232 | 233 | /// Just something that doesn't force us to run tests with `--release` 234 | const LOW_COMPUTE_CFG: Argon2Cfg = Argon2Cfg { 235 | lanes: 1, 236 | mem_cost: 8, 237 | time_cost: 1, 238 | }; 239 | 240 | #[test] 241 | fn encrypt_decrypt() { 242 | let pass = [0, 1, 2, 3]; 243 | let nonce = [0u8; REQUIRED_NONCE_LENGTH]; 244 | let salt = [0u8; REQUIRED_HASH_LENGTH]; 245 | let metadata = EncryptionMetadata::new(nonce, salt, LOW_COMPUTE_CFG); 246 | let key = derive_key(&pass, &metadata.salt, &metadata.argon2_cfg).unwrap(); 247 | let data = b"My spooky data"; 248 | let mut encrypted = encrypt(data, &key, &metadata).unwrap(); 249 | assert_ne!(data.as_slice(), &encrypted); 250 | let decrypted = hash_and_decrypt(&mut encrypted, &pass).unwrap(); 251 | assert_eq!(data.as_slice(), decrypted); 252 | } 253 | 254 | #[test] 255 | fn encrypt_decrypt_bad_key() { 256 | let pass = [9u8, 5, 3, 4]; 257 | let iv = [0u8; REQUIRED_NONCE_LENGTH]; 258 | let salt = [0u8; REQUIRED_HASH_LENGTH]; 259 | let metadata = EncryptionMetadata::new(iv, salt, LOW_COMPUTE_CFG); 260 | let key = derive_key(&pass, &metadata.salt, &metadata.argon2_cfg).unwrap(); 261 | let data = b"My spooky data"; 262 | let mut encrypted = encrypt(data, &key, &metadata).unwrap(); 263 | assert_ne!(data.as_slice(), &encrypted); 264 | let decrypted = hash_and_decrypt(&mut encrypted, &pass).unwrap(); 265 | assert_eq!(data.as_slice(), decrypted); 266 | let bad_pass = [1u8, 2, 3, 4]; 267 | let mut encrypted = encrypt(data, &key, &metadata).unwrap(); 268 | assert_ne!(data.as_slice(), &encrypted); 269 | let derived_bad = derive_key(&bad_pass, &metadata.salt, &metadata.argon2_cfg).unwrap(); 270 | let decrypted = decrypt(&mut encrypted, &derived_bad, &metadata.nonce); 271 | assert!(decrypted.is_err()); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /boot-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), no_std)] 2 | #![warn(clippy::pedantic)] 3 | extern crate alloc; 4 | 5 | use alloc::format; 6 | use alloc::string::{String, ToString}; 7 | 8 | pub mod crypt; 9 | 10 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 11 | pub struct BootCfg<'a> { 12 | pub device: &'a str, 13 | pub encrypted_path_on_device: &'a str, 14 | } 15 | 16 | impl<'a> BootCfg<'a> { 17 | /// Tries to parse the boot cfg from a raw string 18 | /// # Errors 19 | /// Bad format 20 | pub fn parse_raw(cfg: &'a str) -> Result { 21 | let mut device: Option<&'a str> = None; 22 | let mut encrypted_path_on_device: Option<&'a str> = None; 23 | for line in cfg.lines() { 24 | if let Some((key, value)) = line.split_once('=') { 25 | let key = key.trim(); 26 | let value = value.trim(); 27 | match key { 28 | "device" => { 29 | device = Some(value); 30 | } 31 | "encrypted_path_on_device" => { 32 | encrypted_path_on_device = Some(value); 33 | } 34 | e => { 35 | return Err(format!("Got unrecognized configuration key {e}")); 36 | } 37 | } 38 | } else if line.trim().starts_with('#') || line.trim().is_empty() { 39 | continue; 40 | } else { 41 | return Err(format!("Found bad line in configuration {line}")); 42 | } 43 | } 44 | 45 | Ok(Self { 46 | device: device 47 | .ok_or_else(|| "ERROR: Missing configuration for `device`".to_string())?, 48 | encrypted_path_on_device: encrypted_path_on_device.ok_or_else(|| { 49 | "ERROR: Missing configuration for `encrypted_path_on_device`".to_string() 50 | })?, 51 | }) 52 | } 53 | 54 | #[must_use] 55 | pub fn serialize(&self) -> String { 56 | format!( 57 | "\ 58 | device={}\n\ 59 | encrypted_path_on_device={}\n\ 60 | ", 61 | self.device, self.encrypted_path_on_device, 62 | ) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn can_parse_example_cfg() { 72 | let bytes = include_str!("../../boot.cfg.tst"); 73 | let res = BootCfg::parse_raw(bytes).unwrap(); 74 | let ser = res.serialize(); 75 | let de = BootCfg::parse_raw(&ser).unwrap(); 76 | assert_eq!(res, de); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /boot-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boot-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | boot-lib = { workspace = true } 10 | uefi = { workspace = true, features = ["alloc", "global_allocator", "panic_handler"] } -------------------------------------------------------------------------------- /boot-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![no_std] 3 | #![warn(clippy::pedantic)] 4 | #![allow(clippy::let_underscore_untyped, clippy::used_underscore_binding)] 5 | 6 | extern crate alloc; 7 | 8 | use alloc::string::{String, ToString}; 9 | use alloc::vec::Vec; 10 | use alloc::{format, vec}; 11 | use boot_lib::crypt::BootDecryptError; 12 | use boot_lib::BootCfg; 13 | use core::fmt::Write; 14 | use uefi::prelude::*; 15 | use uefi::proto::console::text::Key; 16 | use uefi::proto::device_path::text::{AllowShortcuts, DevicePathToText, DisplayOnly}; 17 | use uefi::proto::device_path::DevicePath; 18 | use uefi::proto::media::file::{File, FileAttribute, FileInfo, FileMode}; 19 | use uefi::proto::media::fs::SimpleFileSystem; 20 | use uefi::proto::ProtocolPointer; 21 | use uefi::table::boot::{LoadImageSource, SearchType}; 22 | use uefi::table::runtime::Time; 23 | use uefi::CStr16; 24 | 25 | const CFG_RAW: &str = include_str!("../../boot.cfg"); 26 | 27 | #[entry] 28 | fn main(_handle: Handle, mut system_table: SystemTable) -> Status { 29 | uefi::helpers::init(&mut system_table).unwrap(); 30 | let _ = system_table 31 | .stdout() 32 | .write_str("[boot-rs]: Welcome to the encrypted boot checker!\n"); 33 | let e = match boot(&mut system_table) { 34 | Ok(()) => { 35 | // Should be unreachable if I understood the docs correctly, we should yield to 36 | // the kernel image and never come back. 37 | return Status::SUCCESS; 38 | } 39 | // Borrow checker, the above borrow doesn't allow us to use stdout. 40 | Err(e) => e, 41 | }; 42 | let _ = system_table 43 | .stdout() 44 | .write_fmt(format_args!("[boot-rs]: Failed to boot: {e}.\n")); 45 | await_enter(&mut system_table); 46 | Status::LOAD_ERROR 47 | } 48 | 49 | fn boot(system: &mut SystemTable) -> Result<(), String> { 50 | let boot_cfg = BootCfg::parse_raw(CFG_RAW) 51 | .map_err(|e| format!("ERROR: Failed to read configuration {e}"))?; 52 | // Casting into P which has access to GUID is neigh impossible, so we'll just turbofish 53 | // into a function. Gets around vtable shennanigans as well. 54 | let encrypted_kernel = read_kernel_data::(system, &boot_cfg)?; 55 | let _ = system.stdout().write_str("[boot-rs]: Read kernel file.\n"); 56 | let decrypted_kernel = decrypt_kernel(system, &encrypted_kernel)?; 57 | let _ = system 58 | .stdout() 59 | .write_str("[boot-rs]: Decrypted kernel file, loading kernel image.\n"); 60 | yield_to_kernel(system, decrypted_kernel) 61 | } 62 | 63 | fn read_kernel_data( 64 | system: &mut SystemTable, 65 | cfg: &BootCfg, 66 | ) -> Result, String> { 67 | let hb = system 68 | .boot_services() 69 | .locate_handle_buffer(SearchType::ByProtocol(&P::GUID)) 70 | .map_err(|e| { 71 | format!("ERROR: Failed to locate handle buffers for SimpleFileSystem: {e:?}") 72 | })?; 73 | let to_text_handle = system 74 | .boot_services() 75 | .get_handle_for_protocol::() 76 | .unwrap(); 77 | let to_text_path_protoc = system 78 | .boot_services() 79 | .open_protocol_exclusive::(to_text_handle) 80 | .unwrap(); 81 | for handle in hb.iter() { 82 | if let Ok(dev_protoc) = system 83 | .boot_services() 84 | .open_protocol_exclusive::(*handle) 85 | { 86 | let mut found_tgt = false; 87 | for node in dev_protoc.node_iter() { 88 | let text = to_text_path_protoc 89 | .convert_device_node_to_text( 90 | system.boot_services(), 91 | node, 92 | DisplayOnly(false), 93 | AllowShortcuts(true), 94 | ) 95 | .unwrap(); 96 | if text.to_string().to_lowercase() == cfg.device.to_lowercase() { 97 | found_tgt = true; 98 | } 99 | } 100 | if !found_tgt { 101 | continue; 102 | } 103 | for node in dev_protoc.node_iter() { 104 | let node_ptr = node.as_ffi_ptr(); 105 | // I don't know why there aren't conversion methods for this, maybe I'm doing something weird 106 | // Safety: No pointer conversion, no messing with the pointer 107 | let mut dev_ptr = unsafe { DevicePath::from_ffi_ptr(node_ptr) }; 108 | if let Ok(can_load_fs) = system 109 | .boot_services() 110 | .locate_device_path::(&mut dev_ptr) 111 | { 112 | let mut loaded = system.boot_services().open_protocol_exclusive::(can_load_fs) 113 | .map_err(|e| format!("ERROR: Failed to load SimpleFileSystem protocol on an handle checked to be able to load it: {e:?}"))?; 114 | if let Ok(mut vol) = loaded.open_volume() { 115 | let mut buf = vec![0u16; 256]; 116 | let file_name = CStr16::from_str_with_buf(cfg.encrypted_path_on_device, &mut buf) 117 | .map_err(|e| format!("ERROR: Failed to convert `encrypted_path_on_device` {} to a CStr16: {e:?}", cfg.encrypted_path_on_device))?; 118 | let file_handle = vol.open(file_name, FileMode::Read, FileAttribute::all()) 119 | .map_err(|e| format!("ERROR: Failed to open `encrypted_path_on_device` {} for reading: {e:?}", cfg.encrypted_path_on_device))?; 120 | let mut content = file_handle.into_regular_file() 121 | .ok_or_else(|| format!("ERROR: Failed to convert `encrypted_path_on_device` {} into a regular file.", cfg.encrypted_path_on_device))?; 122 | let info = content.get_boxed_info::() 123 | .map_err(|e| format!("ERROR: Failed to get file info of regular file at `encrypted_path_on_device` {}: {e:?}", cfg.encrypted_path_on_device))?; 124 | let file_size: usize = info.file_size().try_into().map_err(|e| { 125 | format!("ERROR: Regular file to big to fit in a usize {e}") 126 | })?; 127 | let mut buffer = vec![0u8; file_size]; 128 | let read_bytes = content.read(&mut buffer) 129 | .map_err(|e| format!("ERROR: Failed to read {file_size} bytes from regular file at `encrypted_path_on_device` into memory: {e:?}"))?; 130 | if read_bytes != file_size { 131 | return Err(format!("ERROR: Read unexpected of bytes from regular file at `encrypted_path_on_device`: expected {file_size}, got {read_bytes}.")); 132 | } 133 | return Ok(buffer); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | Err("ERROR: Failed to find kernel image an any device".to_string()) 140 | } 141 | 142 | fn decrypt_kernel(system: &mut SystemTable, buf: &[u8]) -> Result, String> { 143 | for i in 1..4 { 144 | let key = get_pass(system)?; 145 | let _ = system 146 | .stdout() 147 | .write_str("[boot-rs]: Got password, deriving key and attempting decrypt.\n"); 148 | let t0 = system.runtime_services().get_time(); 149 | match boot_lib::crypt::hash_and_decrypt(buf, key.as_bytes()) { 150 | Ok(tgt) => { 151 | let t1 = system.runtime_services().get_time(); 152 | let failed_time = if let (Ok(t0), Ok(t1)) = (t0, t1) { 153 | #[allow(clippy::cast_precision_loss)] 154 | if let Some(delta) = get_time_delta_millis(t0, t1) { 155 | let seconds = delta as f32 / 1000f32; 156 | let _ = system.stdout().write_fmt(format_args!( 157 | "[boot-rs]: Derived key and decrypted kernel in {seconds} seconds.\n" 158 | )); 159 | false 160 | } else { 161 | true 162 | } 163 | } else { 164 | true 165 | }; 166 | if failed_time { 167 | let _ = system.stdout().write_fmt(format_args!( 168 | "[boot-rs]: Derived key and decrypted kernel.\n" 169 | )); 170 | } 171 | return Ok(tgt); 172 | } 173 | Err(e) => match e { 174 | BootDecryptError::InvalidContent => { 175 | let _ = system.stdout().write_fmt(format_args!( 176 | "[boot-rs]: Failed to decrypt kernel, bad pass, attempt [{i}/3].\n" 177 | )); 178 | } 179 | BootDecryptError::Other(o) => { 180 | return Err(format!("ERROR: Failed to decrypt kernel image: {o}")); 181 | } 182 | }, 183 | } 184 | } 185 | Err("ERROR: Failed to decrypt kernel image, too many failed attempts".to_string()) 186 | } 187 | 188 | fn yield_to_kernel( 189 | system: &mut SystemTable, 190 | mut raw_kernel_image: Vec, 191 | ) -> Result<(), String> { 192 | let self_h = system.boot_services().image_handle(); 193 | // Data is copied, it's okay to drop the buffer after this code executes 194 | let loaded_kernel = system 195 | .boot_services() 196 | .load_image( 197 | self_h, 198 | LoadImageSource::FromBuffer { 199 | buffer: &mut raw_kernel_image, 200 | file_path: None, 201 | }, 202 | ) 203 | .map_err(|e| format!("ERROR: Failed to load kernel image: {e:?}"))?; 204 | system 205 | .boot_services() 206 | .start_image(loaded_kernel) 207 | .map_err(|e| format!("ERROR: Failed to yield execution to the kernel image: {e:?}")) 208 | } 209 | 210 | fn get_pass(system_table: &mut SystemTable) -> Result { 211 | let _ = system_table 212 | .stdout() 213 | .write_str("[boot-rs]: Enter kernel decryption key: \n"); 214 | let mut decr = String::new(); 215 | loop { 216 | // Safety: 217 | // Safe if event is not reused after close (we only oneshot it). 218 | 219 | let Some(key_ready_evt) = system_table.stdin().wait_for_key_event() else { 220 | continue; 221 | }; 222 | system_table.boot_services().wait_for_event(&mut [key_ready_evt]) 223 | .map_err(|e| format!("ERROR: Failed to wait for key event when listening to kernel decryption key: {e:?}"))?; 224 | let key = system_table 225 | .stdin() 226 | .read_key() 227 | .map_err(|e| { 228 | format!("ERROR: Failed to read key after waiting and receiving a key event: {e:?}") 229 | })? 230 | .ok_or_else(|| { 231 | "ERROR: Failed to read key (none supplied) after waiting and receiving a key event" 232 | .to_string() 233 | })?; 234 | match key { 235 | Key::Printable(key) => { 236 | let ch = char::from(key); 237 | // We're in Microsoft world Char16s and carriage returns. 238 | if ch == '\r' { 239 | break; 240 | } 241 | decr.push(ch); 242 | } 243 | Key::Special(_) => {} 244 | } 245 | } 246 | Ok(decr) 247 | } 248 | 249 | fn await_enter(system_table: &mut SystemTable) { 250 | let _ = system_table 251 | .stdout() 252 | .write_str("[boot-rs]: Press enter to exit.\n"); 253 | let stdin = system_table.stdin(); 254 | loop { 255 | if let Some(key) = stdin.read_key().unwrap() { 256 | match key { 257 | Key::Printable(ch) => { 258 | if char::from(ch) == '\r' { 259 | break; 260 | } 261 | } 262 | Key::Special(_) => {} 263 | } 264 | } 265 | } 266 | } 267 | 268 | fn get_time_delta_millis(t0: Time, t1: Time) -> Option { 269 | if t0.day() == t1.day() { 270 | let t1_millis = get_millis_of_day(t1); 271 | let t0_millis = get_millis_of_day(t0); 272 | if t0_millis > t1_millis { 273 | Some(t0_millis - t1_millis) 274 | } else { 275 | Some(t1_millis - t0_millis) 276 | } 277 | } else { 278 | // Don't really want to take in to account different days, let's keep it simple 279 | None 280 | } 281 | } 282 | 283 | fn get_millis_of_day(t: Time) -> u64 { 284 | (u64::from(t.second()) + u64::from(t.minute()) * 60 + u64::from(t.hour()) * 3600) * 1000 285 | + u64::from(t.nanosecond()) / 1_000_000 286 | } 287 | -------------------------------------------------------------------------------- /boot-strap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "boot-strap" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | boot-lib = { workspace = true } 10 | hex = { workspace = true, default-features = false, features = ["alloc"] } 11 | initramfs-lib = { workspace = true } 12 | tiny-cli = { workspace = true } 13 | tiny-std = { workspace = true, features = ["alloc", "executable", "global-allocator", "cli"]} 14 | rusl = { workspace = true, features = ["alloc"]} 15 | -------------------------------------------------------------------------------- /boot-strap/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::initramfs::{gen_cfg, generate_initramfs}; 2 | use alloc::format; 3 | use alloc::string::{String, ToString}; 4 | use boot_lib::crypt::{ 5 | derive_key, encrypt, hash_and_decrypt, Argon2Cfg, BootDecryptError, EncryptionMetadata, 6 | DEFAULT_CONFIG, REQUIRED_HASH_LENGTH, REQUIRED_NONCE_LENGTH, 7 | }; 8 | use boot_lib::BootCfg; 9 | use core::fmt::Write; 10 | use initramfs_lib::{print_error, print_ok}; 11 | use rusl::string::unix_str::UnixStr; 12 | use tiny_cli::{ArgParse, Subcommand}; 13 | use tiny_std::linux::get_pass::get_pass; 14 | use tiny_std::println; 15 | use tiny_std::time::SystemTime; 16 | use tiny_std::unix::random::system_random; 17 | 18 | /// Generate initramfs configuration 19 | #[derive(Debug, ArgParse)] 20 | #[cli(help_path = "boot-rs, initramfs, gen-cfg")] 21 | struct InitramfsGenCfgOpts { 22 | /// UUID for the home partition 23 | #[cli(short = "h", long = "home-uuid")] 24 | home_uuid: String, 25 | /// UUID for the root partition 26 | #[cli(short = "r", long = "root-uuid")] 27 | root_uuid: String, 28 | /// UUID for the swap partition 29 | #[cli(short = "s", long = "swap-uuid")] 30 | swap_uuid: String, 31 | /// Overwrite the current file if present. 32 | /// Probably not good, since we generate a cryptsetup key from the generated salt, 33 | /// if key that has already been registered, this salt shouldn't change 34 | #[cli(short = "o", long = "overwrite")] 35 | overwrite: bool, 36 | /// Destination file 37 | #[cli(short = "d", long = "destination-file")] 38 | destination_file: Option<&'static UnixStr>, 39 | } 40 | #[derive(Debug, ArgParse)] 41 | #[cli(help_path = "boot-rs, initramfs, gen-init")] 42 | struct InitramfsGenInitOpts { 43 | /// Configuration file containing uuids of cryptdevices 44 | #[cli(short = "i", long = "initramfs-cfg")] 45 | initramfs_cfg: &'static UnixStr, 46 | /// Directory where the initramfs should be created, will wipe a previous initramfs if it exists there. 47 | /// Ex: /root/initramfs 48 | #[cli(short = "d", long = "destination-file")] 49 | destination_directory: &'static UnixStr, 50 | 51 | /// Argon 2 mem cost. 52 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 53 | /// mem cost for that algorithm. 54 | /// If omitted, a default value will be used. 55 | #[cli(short = "m", long = "argon2-mem")] 56 | argon2_mem: Option, 57 | 58 | /// Argon 2 time cost. 59 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 60 | /// time cost for that algorithm. 61 | /// If omitted, a default value will be used. 62 | #[cli(short = "t", long = "argon2-time")] 63 | argon2_time: Option, 64 | /// Argon 2 parallel cost. 65 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 66 | /// parallel cost for that algorithm. 67 | /// If omitted, a default value will be used. 68 | #[cli(short = "l", long = "argon2-lanes")] 69 | argon2_lanes: Option, 70 | } 71 | 72 | #[derive(Debug, ArgParse)] 73 | #[cli(help_path = "boot-rs, initramfs")] 74 | struct InitramfsOptions { 75 | #[cli(subcommand)] 76 | subcommand: InitramfsAction, 77 | } 78 | 79 | #[derive(Debug, Subcommand)] 80 | enum InitramfsAction { 81 | GenCfg(InitramfsGenCfgOpts), 82 | GenInit(InitramfsGenInitOpts), 83 | } 84 | 85 | /// Encrypt kernel image 86 | #[derive(Debug, ArgParse)] 87 | #[cli(help_path = "boot-rs, boot")] 88 | struct BootOpts { 89 | /// The kernel image path. 90 | /// Where the kernel image to encrypt is. 91 | /// If compiling locally, is usually `src-dir/arch//boot/bzImage`. 92 | #[cli(short = "i", long = "kernel-image-path")] 93 | kernel_image_path: &'static UnixStr, 94 | 95 | /// Encrypted destination. 96 | /// Where to put the encrypted kernel image. 97 | #[cli(short = "e", long = "kernel-enc-path")] 98 | kernel_enc_path: &'static UnixStr, 99 | 100 | /// Configuration destination. 101 | /// Where to put the generated configuration file. 102 | #[cli(short = "c", long = "cfg-destination")] 103 | cfg_destination: &'static UnixStr, 104 | 105 | /// Efi boot device. 106 | /// The efi label of the boot device. 107 | /// Ex: `HD(1,GPT,f0054eea-adf8-4956-958f-12e353cac4c8,0x800,0x100000)` 108 | #[cli(short = "d", long = "efi-device")] 109 | efi_device: String, 110 | 111 | /// Efi boot device kernel path. 112 | /// The internal path on the EFI device, where you plan to put the kernel. 113 | /// Likely to be the same as `kernel_enc_path`. 114 | /// We're not using microsoft path-specs because we're not in microsoft world just yet. 115 | /// Ex: `/EFI/gentoo/gentoo-6.1.19.enc` 116 | #[cli(short = "p", long = "efi-path")] 117 | efi_path: &'static UnixStr, 118 | 119 | /// Argon 2 mem cost. 120 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 121 | /// mem cost for that algorithm. 122 | /// If omitted, a default value will be used. 123 | #[cli(short = "m", long = "argon2-mem")] 124 | argon2_mem: Option, 125 | /// Argon 2 time cost. 126 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 127 | /// time cost for that algorithm. 128 | /// If omitted, a default value will be used. 129 | #[cli(short = "t", long = "argon2-time")] 130 | argon2_time: Option, 131 | /// Argon 2 parallel cost. 132 | /// The password will be converted to a 32-byte AES key using argon2, this specifies a custom 133 | /// parallel cost for that algorithm. 134 | /// If omitted, a default value will be used. 135 | #[cli(short = "l", long = "argon2-lanes")] 136 | argon2_lanes: Option, 137 | } 138 | 139 | /// Generate boot config 140 | #[derive(Debug, ArgParse)] 141 | #[cli(help_path = "boot-rs")] 142 | struct Opts { 143 | #[cli(subcommand)] 144 | subcommand: Action, 145 | } 146 | 147 | #[derive(Debug, Subcommand)] 148 | enum Action { 149 | Initramfs(InitramfsOptions), 150 | Boot(BootOpts), 151 | } 152 | 153 | fn create_argon2opts_maybe_default( 154 | argon2_mem: Option, 155 | argon2_time: Option, 156 | argon2_lanes: Option, 157 | ) -> Argon2Cfg { 158 | let mut cfg = DEFAULT_CONFIG; 159 | if let Some(lanes) = argon2_lanes { 160 | cfg.lanes = lanes; 161 | } 162 | if let Some(mem) = argon2_mem { 163 | cfg.mem_cost = mem; 164 | } 165 | if let Some(time) = argon2_time { 166 | cfg.time_cost = time; 167 | } 168 | cfg 169 | } 170 | 171 | pub(crate) fn run() { 172 | let args = tiny_std::unix::cli::parse_cli_args::(); 173 | match args.subcommand { 174 | Action::Initramfs(initramfs) => match initramfs.subcommand { 175 | InitramfsAction::GenCfg(InitramfsGenCfgOpts { 176 | home_uuid, 177 | root_uuid, 178 | swap_uuid, 179 | overwrite, 180 | destination_file, 181 | }) => { 182 | gen_cfg(home_uuid, root_uuid, swap_uuid, destination_file, overwrite).unwrap(); 183 | } 184 | InitramfsAction::GenInit(InitramfsGenInitOpts { 185 | initramfs_cfg, 186 | destination_directory, 187 | argon2_mem, 188 | argon2_time, 189 | argon2_lanes, 190 | }) => { 191 | let argon2_cfg = 192 | create_argon2opts_maybe_default(argon2_mem, argon2_time, argon2_lanes); 193 | if let Err(e) = 194 | generate_initramfs(initramfs_cfg, &argon2_cfg, destination_directory) 195 | { 196 | print_error!("Failed to generate initramfs: {e}"); 197 | rusl::process::exit(1); 198 | } 199 | print_ok!("Successfully generated initramfs, don't forget to add initramfs-rs as `./init` to it."); 200 | } 201 | }, 202 | Action::Boot(boot_opts) => match generate(&boot_opts) { 203 | Ok(()) => {} 204 | Err(e) => { 205 | panic!("Failed to generate: {e}"); 206 | } 207 | }, 208 | } 209 | } 210 | 211 | fn generate(gen_opts: &BootOpts) -> Result<(), String> { 212 | let kernel_data = tiny_std::fs::read(gen_opts.kernel_image_path).map_err(|e| { 213 | format!( 214 | "Failed to read kernel image at supplied path {:?}: {e}", 215 | gen_opts.kernel_image_path 216 | ) 217 | })?; 218 | print_ok!("Read kernel image at {:?}", gen_opts.kernel_image_path); 219 | let efi_path = get_efi_path_string( 220 | gen_opts 221 | .efi_path 222 | .as_str() 223 | .map_err(|_e| "Failed to convert supplied EFI path to a utf8 str".to_string())?, 224 | ) 225 | .map_err(|e| format!("Failed to convert supplied efi path: {e}"))?; 226 | let (nonce, salt) = 227 | generate_nonce_and_salt().map_err(|e| format!("Failed to generate random vectors: {e}"))?; 228 | let pass = prompt_passwords().map_err(|e| format!("Failed to get password: {e}"))?; 229 | let argon2_cfg = create_argon2opts_maybe_default( 230 | gen_opts.argon2_mem, 231 | gen_opts.argon2_time, 232 | gen_opts.argon2_lanes, 233 | ); 234 | let metadata = EncryptionMetadata::new(nonce, salt, argon2_cfg); 235 | println!("[boot-rs]: Deriving encryption key."); 236 | let (key, derive_key_time) = 237 | timed(|| derive_key(pass.as_bytes(), metadata.salt(), metadata.argon2_cfg()))?; 238 | let key = key.map_err(|e| format!("Failed to derive a key from the password: {e}"))?; 239 | println!("[boot-rs]: Derived encryption key in {derive_key_time} seconds."); 240 | println!("[boot-rs]: Encrypting kernel image."); 241 | let (encrypted, encrypt_time) = timed(|| encrypt(&kernel_data, &key, &metadata))?; 242 | println!("[boot-rs]: Encrypted kernel image in {encrypt_time} seconds."); 243 | // Insanity check. 244 | let encrypted = encrypted?; 245 | if encrypted == kernel_data { 246 | return Err( 247 | "Encryption failed, output data same as input data (Sanity check triggered)." 248 | .to_string(), 249 | ); 250 | } 251 | println!("[boot-rs]: Starting a test decryption."); 252 | let (decrypted, decryption_time) = timed(|| { 253 | match hash_and_decrypt(&encrypted, pass.as_bytes()) { 254 | Ok(dec) => Ok(dec), 255 | Err(e) => { 256 | match e { 257 | BootDecryptError::InvalidContent => { 258 | Err("Failed to decrypt encrypted boot image (better here than at boot time), failed to find magic in decrypted bytes".to_string()) 259 | } 260 | BootDecryptError::Other(o) => { 261 | Err(format!("Failed to decrypt encrypted boot image: {o}")) 262 | } 263 | } 264 | } 265 | } 266 | })?; 267 | let decrypted = decrypted?; 268 | if decrypted != kernel_data.as_slice() { 269 | return Err("Failed to decrypt kernel image, input is not the same as output".to_string()); 270 | } 271 | println!("[boot-rs]: Successfully ran test-decryption in {decryption_time} seconds"); 272 | let cfg = BootCfg { 273 | device: &gen_opts.efi_device, 274 | encrypted_path_on_device: &efi_path, 275 | }; 276 | let cfg_out = cfg.serialize(); 277 | println!( 278 | "[boot-rs]: Writing encrypted kernel to {:?}", 279 | gen_opts.kernel_enc_path 280 | ); 281 | tiny_std::fs::write(gen_opts.kernel_enc_path, encrypted.as_slice()).map_err(|e| { 282 | format!( 283 | "Failed to write encrypted kernel to out path {:?}: {e}", 284 | gen_opts.kernel_enc_path 285 | ) 286 | })?; 287 | println!( 288 | "[boot-rs]: Writing configuration to {:?}", 289 | gen_opts.cfg_destination 290 | ); 291 | tiny_std::fs::write(gen_opts.cfg_destination, cfg_out.as_bytes()).map_err(|e| { 292 | format!( 293 | "Failed to write cfg to out path {:?}: {e}", 294 | gen_opts.cfg_destination 295 | ) 296 | })?; 297 | println!("[boot-rs]: Success!"); 298 | Ok(()) 299 | } 300 | 301 | #[inline] 302 | fn generate_nonce_and_salt( 303 | ) -> Result<([u8; REQUIRED_NONCE_LENGTH], [u8; REQUIRED_HASH_LENGTH]), String> { 304 | let mut iv: [u8; REQUIRED_NONCE_LENGTH] = [0u8; REQUIRED_NONCE_LENGTH]; 305 | system_random(&mut iv) 306 | .map_err(|e| format!("Failed to generate a random initialization vector: {e}"))?; 307 | let mut salt: [u8; REQUIRED_HASH_LENGTH] = [0u8; REQUIRED_HASH_LENGTH]; 308 | system_random(&mut salt).map_err(|e| format!("Failed to generate a random salt: {e}"))?; 309 | Ok((iv, salt)) 310 | } 311 | 312 | #[inline] 313 | fn prompt_passwords() -> Result { 314 | let mut pass_bytes = [0u8; 128]; 315 | println!("Enter kernel decryption password: "); 316 | let pass = get_pass(&mut pass_bytes) 317 | .map_err(|e| format!("Failed to get the decryption password from stdin: {e}"))?; 318 | let mut pass2_bytes = [0u8; 128]; 319 | println!("Repeat kernel decryption password: "); 320 | let pass2 = get_pass(&mut pass2_bytes) 321 | .map_err(|e| format!("Failed to get decryption password repetition from stdin: {e}"))?; 322 | if pass2 != pass { 323 | return Err("Password mismatch!".to_string()); 324 | } 325 | Ok(pass.trim_end_matches('\n').to_string()) 326 | } 327 | 328 | #[inline] 329 | fn timed R>(func: F) -> Result<(R, f32), String> { 330 | let now = SystemTime::now(); 331 | let res = (func)(); 332 | let elapsed = now.elapsed().ok_or_else(|| { 333 | "Failed to get elapsed time, system misconfiguration, or we're in a time vortex".to_string() 334 | })?; 335 | Ok((res, elapsed.as_secs_f32())) 336 | } 337 | 338 | #[inline] 339 | fn get_efi_path_string(input_path: &str) -> Result { 340 | let mut path = String::new(); 341 | let orig_len = path.len(); 342 | for component in input_path.split('/') { 343 | path.write_fmt(format_args!("{component}\\")) 344 | .map_err(|e| format!("Failed to append to string, should never happen: {e}"))?; 345 | } 346 | if path.len() > orig_len { 347 | path = path.trim_end_matches('\\').to_string(); 348 | } 349 | println!("Using efi path {path}"); 350 | Ok(path) 351 | } 352 | -------------------------------------------------------------------------------- /boot-strap/src/initramfs.rs: -------------------------------------------------------------------------------- 1 | use alloc::format; 2 | use alloc::string::{String, ToString}; 3 | use boot_lib::crypt::{Argon2Cfg, Argon2Salt, REQUIRED_HASH_LENGTH}; 4 | use initramfs_lib::{print_ok, print_pending, read_cfg, write_cfg, Cfg}; 5 | use rusl::platform::Mode; 6 | use rusl::string::unix_str::{UnixStr, UnixString}; 7 | use rusl::unix_lit; 8 | use tiny_std::fs::OpenOptions; 9 | use tiny_std::io::{Read, Write}; 10 | use tiny_std::linux::get_pass::get_pass; 11 | use tiny_std::process::Stdio; 12 | use tiny_std::unix::random::system_random; 13 | 14 | pub(crate) fn gen_cfg( 15 | home_uuid: String, 16 | root_uuid: String, 17 | swap_uuid: String, 18 | dest: Option<&'static UnixStr>, 19 | overwrite: bool, 20 | ) -> Result<(), String> { 21 | for disk_uuid in [&home_uuid, &root_uuid, &swap_uuid] { 22 | if let Err(e) = tiny_std::fs::metadata(&UnixString::from_format(format_args!( 23 | "/dev/disk/by-uuid/{disk_uuid}" 24 | ))) { 25 | return Err(format!("Failed to read metadata for disk specified by uuid {disk_uuid}, double check that the disk is correct: {e}")); 26 | } 27 | } 28 | let mut salt = [0u8; REQUIRED_HASH_LENGTH]; 29 | system_random(&mut salt).map_err(|e| format!("Failed to generate random salt {e}"))?; 30 | let cfg = Cfg { 31 | root_uuid, 32 | swap_uuid, 33 | home_uuid, 34 | pass_salt: Some(hex::encode(salt)), 35 | crypt_file: None, 36 | }; 37 | let dest = if let Some(dest) = dest { 38 | print_ok!("Using supplied location as destination {dest:?}"); 39 | UnixString::from(dest) 40 | } else { 41 | print_ok!("No cfg output destination supplied"); 42 | let config_dir = tiny_std::env::var("XDG_CONFIG_HOME").map_err(|_e| { 43 | "No cfg output destination supplied, couldn't find `XDG_CONFIG_HOME`".to_string() 44 | })?; 45 | let dest_dir = UnixString::from_format(format_args!("{config_dir}/boot-rs")); 46 | 47 | if tiny_std::fs::exists(&dest_dir) 48 | .map_err(|e| format!("Failed to check if {dest_dir:?} exists: {e}"))? 49 | { 50 | let dest = dest_dir.path_join(unix_lit!("initramfs.cfg")); 51 | if tiny_std::fs::exists(&dest) 52 | .map_err(|e| format!("Failed to check if {dest:?} exists: {e}"))? 53 | && !overwrite 54 | { 55 | return Err(format!( 56 | "Destination {dest:?} already exists and `overwrite` was not specified" 57 | )); 58 | } 59 | dest 60 | } else { 61 | tiny_std::fs::create_dir_all(&dest_dir) 62 | .map_err(|e| format!("Failed to create destination directory {dest_dir:?}: {e}"))?; 63 | dest_dir.path_join(unix_lit!("initramfs.cfg")) 64 | } 65 | }; 66 | print_ok!("Writing new CFG to {dest:?}"); 67 | write_cfg(&cfg, &dest) 68 | } 69 | 70 | pub(crate) fn gen_key_file( 71 | cfg_file: &UnixStr, 72 | argon2_cfg: &Argon2Cfg, 73 | dest: &UnixStr, 74 | ) -> Result<(), String> { 75 | let mut initramfs_cfg = cfg_from_path(cfg_file)?; 76 | if initramfs_cfg.pass_salt.is_some() { 77 | return Err("The provided cfg already has a provided salt, implying that a key has already been generated.\n\ 78 | If you want to generate a key for the same salt, use `regenerate-key`".to_string()); 79 | } 80 | let mut salt = [0u8; REQUIRED_HASH_LENGTH]; 81 | system_random(&mut salt).map_err(|e| format!("Failed to generate random salt {e}"))?; 82 | let mut pass_bytes = [0u8; 128]; 83 | print_ok!("Enter password for transient crypt-file: "); 84 | let pwd = get_pass(&mut pass_bytes) 85 | .map_err(|e| format!("Failed to get password to test disk decryption {e}"))?; 86 | let pwd = pwd.trim_end_matches('\n'); 87 | let key = boot_lib::crypt::derive_key(pwd.as_bytes(), &Argon2Salt(salt), argon2_cfg) 88 | .map_err(|e| format!("Failed to derive a key with salt {e}"))?; 89 | initramfs_cfg.pass_salt = Some(hex::encode(salt)); 90 | let cfg = cfg_file; 91 | write_cfg(&initramfs_cfg, cfg) 92 | .map_err(|e| format!("Failed to write config with new salt {e}"))?; 93 | OpenOptions::new() 94 | .create_new(true) 95 | .read(true) 96 | .write(true) 97 | .mode(Mode::S_IRUSR) 98 | .open(dest) 99 | .map_err(|e| format!("Failed to create new file at {dest:?}: {e}"))? 100 | .write_all(&key.0) 101 | .map_err(|e| format!("Failed to write regenerated key to destination {dest:?}: {e}"))?; 102 | for disk in [ 103 | &initramfs_cfg.root_uuid, 104 | &initramfs_cfg.home_uuid, 105 | &initramfs_cfg.swap_uuid, 106 | ] { 107 | print_pending!("Testing cryptokey for disk: {disk}"); 108 | test_cryptsetup_open(dest, disk).map_err(|e| { 109 | format!("Failed to verify supplied passphrase works with provided disks: {e}") 110 | })?; 111 | } 112 | print_ok!("Successfully verified cryptokeys"); 113 | Ok(()) 114 | } 115 | 116 | fn test_cryptsetup_open(key_file: &UnixStr, disk_uuid: &str) -> Result<(), String> { 117 | let path = UnixString::from_format(format_args!("/dev/disk/by-uuid/{disk_uuid}\0")); 118 | let mut child = tiny_std::process::Command::new(unix_lit!("/sbin/cryptsetup")) 119 | .map_err(|e| format!("Failed to construct command: {e}"))? 120 | .arg(unix_lit!("luksOpen")) 121 | .arg(unix_lit!("--key-file")) 122 | .arg(key_file) 123 | .arg(unix_lit!("--test-passphrase")) 124 | .arg(unix_lit!("-v")) 125 | .arg(&path) 126 | .spawn() 127 | .map_err(|e| format!("Failed to spawn cryptsetup: {e}"))?; 128 | let res = child 129 | .wait() 130 | .map_err(|e| format!("Failed to await child: {e}"))?; 131 | if res != 0 { 132 | return Err(format!( 133 | "Failed to test opening disks with supplied passphrase, got cryptsetup exit code {res}" 134 | )); 135 | } 136 | Ok(()) 137 | } 138 | 139 | pub(crate) fn regen_key_file( 140 | cfg_file: &UnixStr, 141 | argon2opts: &Argon2Cfg, 142 | dest: &UnixStr, 143 | ) -> Result<(), String> { 144 | let initramfs_cfg = cfg_from_path(cfg_file)?; 145 | let Some(salt) = initramfs_cfg.pass_salt else { 146 | return Err( 147 | "The provided cfg already no provided salt, implying that no key has been generated.\n\ 148 | If you want to generate a new key, use `generate-key`" 149 | .to_string(), 150 | ); 151 | }; 152 | let salt_bytes: [u8; REQUIRED_HASH_LENGTH] = hex::decode(salt) 153 | .map_err(|e| format!("Failed to decode salt hex {e}"))? 154 | .try_into() 155 | .map_err(|e| format!("The provided salt is not 32 bytes {e:?}"))?; 156 | let mut pass_bytes = [0u8; 128]; 157 | print_ok!("Enter password for transient crypt-file: "); 158 | let pwd = get_pass(&mut pass_bytes) 159 | .map_err(|e| format!("Failed to get password to test disk decryption {e}"))?; 160 | let pwd = pwd.trim_end_matches('\n'); 161 | let argon2_cfg = argon2opts; 162 | let key = boot_lib::crypt::derive_key(pwd.as_bytes(), &Argon2Salt(salt_bytes), argon2_cfg) 163 | .map_err(|e| format!("Failed to derive a key with salt {e}"))?; 164 | OpenOptions::new() 165 | .create_new(true) 166 | .write(true) 167 | .mode(Mode::S_IRUSR) 168 | .open(dest) 169 | .map_err(|e| format!("Failed to create new file at {dest:?}: {e}"))? 170 | .write_all(&key.0) 171 | .map_err(|e| format!("Failed to write regenerated key to destination {dest:?}: {e}"))?; 172 | for disk in [ 173 | &initramfs_cfg.root_uuid, 174 | &initramfs_cfg.home_uuid, 175 | &initramfs_cfg.swap_uuid, 176 | ] { 177 | print_pending!("Testing cryptokey for disk: {disk}"); 178 | test_cryptsetup_open(dest, disk).map_err(|e| { 179 | format!("Failed to verify supplied passphrase works with provided disks: {e}") 180 | })?; 181 | } 182 | print_ok!("Successfully verified cryptokeys"); 183 | Ok(()) 184 | } 185 | 186 | fn cfg_from_path(path: &UnixStr) -> Result { 187 | let cfg = 188 | read_cfg(path).map_err(|e| format!("Failed to read initramfs cfg from disk {e:?}"))?; 189 | Ok(cfg) 190 | } 191 | 192 | pub(crate) fn generate_initramfs( 193 | cfg_file: &UnixStr, 194 | argon2opts: &Argon2Cfg, 195 | dest: &UnixStr, 196 | ) -> Result<(), String> { 197 | let cfg = cfg_from_path(cfg_file) 198 | .map_err(|e| format!("Failed to read configuration at {cfg_file:?}: {e}"))?; 199 | if tiny_std::fs::exists(dest).map_err(|e| format!("Failed to check if {dest:?} exists: {e}"))? { 200 | return Err(format!( 201 | "Directory already exists at initramfs destination {dest:?}" 202 | )); 203 | } 204 | create_751_dir(dest)?; 205 | for dir in [ 206 | unix_lit!("bin"), 207 | unix_lit!("dev"), 208 | unix_lit!("lib64"), 209 | unix_lit!("mnt"), 210 | unix_lit!("proc"), 211 | unix_lit!("run"), 212 | unix_lit!("sbin"), 213 | unix_lit!("sys"), 214 | ] { 215 | let path = dest.path_join(dir); 216 | create_751_dir(&path)?; 217 | } 218 | 219 | let mnt_root_dest = dest.path_join(unix_lit!("/mnt/root")); 220 | create_751_dir(&mnt_root_dest)?; 221 | 222 | let copy_from_fs = [ 223 | ( 224 | unix_lit!("/bin/busybox"), 225 | dest.path_join(unix_lit!("/bin/busybox")), 226 | ), 227 | ( 228 | unix_lit!("/sbin/cryptsetup"), 229 | dest.path_join(unix_lit!("/sbin/cryptsetup")), 230 | ), 231 | ]; 232 | for (src, dest) in copy_from_fs { 233 | static_check_copy(src, &dest)?; 234 | } 235 | shell_out_copy_archive_dev(dest.as_str().unwrap())?; 236 | if cfg.pass_salt.is_some() { 237 | regen_key_file( 238 | cfg_file, 239 | argon2opts, 240 | &dest.path_join(unix_lit!("crypt.key")), 241 | ) 242 | } else { 243 | gen_key_file( 244 | cfg_file, 245 | argon2opts, 246 | &dest.path_join(unix_lit!("crypt.key")), 247 | ) 248 | } 249 | .map_err(|e| format!("Failed to create key {e}"))?; 250 | 251 | let cfg_path_utf8 = dest.path_join(unix_lit!("initramfs.cfg")); 252 | write_cfg(&cfg, &cfg_path_utf8).map_err(|e| format!("Failed to write initramfs.cfg to {e}"))?; 253 | Ok(()) 254 | } 255 | 256 | fn create_751_dir(dir: &UnixStr) -> Result<(), String> { 257 | tiny_std::fs::create_dir_mode(dir, Mode::from(0o751)) 258 | .map_err(|e| format!("Failed to create 751 mode directory {dir:?} for initramfs {e}")) 259 | } 260 | 261 | fn static_check_copy(src: &UnixStr, dest: &UnixStr) -> Result<(), String> { 262 | if !tiny_std::fs::exists(src).map_err(|e| format!("Failed to check if {src:?} exists: {e}"))? { 263 | return Err(format!( 264 | "Tried to copy {src:?} to {dest:?} but {src:?} does not exist" 265 | )); 266 | } 267 | let mut file_out = tiny_std::process::Command::new(unix_lit!("/usr/bin/file")) 268 | .unwrap() 269 | .stdout(Stdio::MakePipe) 270 | .arg(src) 271 | .spawn() 272 | .map_err(|e| { 273 | format!("Failed to spawn `file` command on {src:?} to check if statically linked {e}") 274 | })?; 275 | let status = file_out.wait().unwrap(); 276 | let mut output = String::new(); 277 | file_out 278 | .stdout 279 | .unwrap() 280 | .read_to_string(&mut output) 281 | .unwrap(); 282 | if status != 0 { 283 | return Err(format!("Failed to run `file` command on {src:?} to check if statically linked, got exit status {status}")); 284 | } 285 | if !output.contains("statically linked") && !output.contains("static-pie linked") { 286 | return Err(format!("`file` command on {src:?} produces output that did not contain the words 'statically linked' or 'static-pie linked' suggesting it's not statically linked, which would produce issues.")); 287 | } 288 | let content = 289 | tiny_std::fs::read(src).map_err(|e| format!("Failed to copy {src:?} to {dest:?}: {e}"))?; 290 | let metadata = tiny_std::fs::metadata(src) 291 | .map_err(|e| format!("Failed to copy {src:?} to {dest:?}: {e}"))?; 292 | let mut dest_file = OpenOptions::new() 293 | .create_new(true) 294 | .write(true) 295 | .mode(metadata.mode()) 296 | .open(dest) 297 | .map_err(|e| format!("Failed to copy {src:?} to {dest:?}: {e}"))?; 298 | dest_file 299 | .write_all(&content) 300 | .map_err(|e| format!("Failed to copy {src:?} to {dest:?}: {e}"))?; 301 | Ok(()) 302 | } 303 | 304 | fn shell_out_copy_archive_dev(dest_base: &str) -> Result<(), String> { 305 | for dir in ["null", "console", "tty"] { 306 | let mut output = tiny_std::process::Command::new(unix_lit!("/bin/cp")) 307 | .unwrap() 308 | .arg(unix_lit!("--archive")) 309 | .arg(&UnixString::from_format(format_args!("/dev/{dir}\0"))) 310 | .arg(&UnixString::from_format(format_args!("{dest_base}/dev\0"))) 311 | .spawn() 312 | .map_err(|e| format!("Failed to spawn copy command `cp --archive /dev/{dir} {dest_base:?}/dev` : {e}"))?; 313 | let status = output.wait().unwrap(); 314 | if status != 0 { 315 | //let maybe_utf8_out = String::from_utf8(output.stderr); 316 | return Err(format!("Failed to run `cp --archive /dev/{dir} {dest_base:?}/dev`, got exit status {status}")); 317 | } 318 | } 319 | 320 | Ok(()) 321 | } 322 | -------------------------------------------------------------------------------- /boot-strap/src/main.rs: -------------------------------------------------------------------------------- 1 | //! First of all, I want to say that I'm sorry about the name, I just couldn't help myself. 2 | #![no_std] 3 | #![no_main] 4 | #![warn(clippy::pedantic)] 5 | #![allow(clippy::similar_names)] 6 | 7 | mod app; 8 | mod initramfs; 9 | extern crate alloc; 10 | 11 | #[no_mangle] 12 | extern "Rust" fn main() -> i32 { 13 | app::run(); 14 | 0 15 | } 16 | -------------------------------------------------------------------------------- /boot.cfg.tst: -------------------------------------------------------------------------------- 1 | device=HD(1,GPT,bxy3cxx1-aa1f-6245-a8d5-854a096b17cx,0x800,0x100000) 2 | encrypted_path_on_device=\EFI\gentoo\gentoo-6.1.19.enc 3 | -------------------------------------------------------------------------------- /build_boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Needs aes_force_soft and polyval_force_soft to make sure that no incompatible instructions are included in the image 4 | RUSTFLAGS='-C panic=abort --cfg aes_force_soft --cfg polyval_force_soft' cargo b -p boot-rs --target x86_64-unknown-uefi "$@" -------------------------------------------------------------------------------- /build_init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | RUSTFLAGS='-C panic=abort -C link-arg=-nostartfiles -C target-cpu=native -C target-feature=+crt-static -C relocation-model=static' cargo b -p initramfs-rs --target x86_64-unknown-linux-gnu "$@" 4 | -------------------------------------------------------------------------------- /build_strap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | RUSTFLAGS='-C panic=abort -C link-arg=-nostartfiles -C target-cpu=native -C target-feature=+crt-static -C relocation-model=static' cargo b -p boot-strap --target x86_64-unknown-linux-gnu "$@" -------------------------------------------------------------------------------- /initramfs-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "initramfs-lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rusl = { workspace = true, features = ["alloc"] } 10 | tiny-std = { workspace = true, default-features = false, features = ["alloc"] } -------------------------------------------------------------------------------- /initramfs-lib/src/error.rs: -------------------------------------------------------------------------------- 1 | use alloc::string::String; 2 | 3 | pub type Result = core::result::Result; 4 | 5 | #[derive(Debug)] 6 | pub enum Error { 7 | App(String), 8 | Bail(String), 9 | Crypt(String), 10 | FindPartitions(String), 11 | MountPseudo(String), 12 | Mount(String), 13 | Cfg(String), 14 | Spawn(String), 15 | UnMount(String), 16 | } 17 | -------------------------------------------------------------------------------- /initramfs-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use crate::error::{Error, Result}; 4 | use alloc::string::{String, ToString}; 5 | use alloc::{format, vec}; 6 | use rusl::error::Errno; 7 | use rusl::platform::{FilesystemType, Mountflags}; 8 | use rusl::string::unix_str::{UnixStr, UnixString}; 9 | use rusl::unistd::{mount, unmount}; 10 | use rusl::unix_lit; 11 | use tiny_std::eprintln; 12 | use tiny_std::io::{Read, Write}; 13 | use tiny_std::linux::get_pass::get_pass; 14 | use tiny_std::process::{Child, Command, Stdio}; 15 | 16 | mod error; 17 | pub mod print; 18 | 19 | extern crate alloc; 20 | 21 | pub fn full_init(cfg: &Cfg) -> Result<()> { 22 | print_ok!("Mounting pseudo filesystems."); 23 | mount_pseudo_filesystems() 24 | .map_err(|e| Error::App(format!("Failed to mount pseudo filesystems {e:?}")))?; 25 | print_ok!("Running mdev."); 26 | run_mdev().map_err(|e| Error::App(format!("Failed to run mdev oneshot: {e:?}")))?; 27 | print_ok!("Preparing user filesystems."); 28 | prep_user_filesystems(cfg) 29 | .map_err(|e| Error::App(format!("Failed to mount user filesystems {e:?}")))?; 30 | print_ok!("Cleaning up."); 31 | try_unmount().map_err(|e| Error::App(format!("Failed to unmount pseudo filesystems {e:?}")))?; 32 | print_ok!("Done, switching root"); 33 | let e = switch_root(); 34 | Err(e) 35 | } 36 | 37 | const MOUNT_NONE_SOURCE: &UnixStr = UnixStr::from_str_checked("none\0"); 38 | const PROC: &UnixStr = UnixStr::from_str_checked("proc\0"); 39 | const SYS: &UnixStr = UnixStr::from_str_checked("sys\0"); 40 | const DEV: &UnixStr = UnixStr::from_str_checked("dev\0"); 41 | 42 | pub fn mount_pseudo_filesystems() -> Result<()> { 43 | mount( 44 | MOUNT_NONE_SOURCE, 45 | PROC, 46 | FilesystemType::PROC, 47 | Mountflags::empty(), 48 | None, 49 | ) 50 | .map_err(|e| Error::MountPseudo(format!("Failed to mount proc types at /proc: {e}")))?; 51 | mount( 52 | MOUNT_NONE_SOURCE, 53 | SYS, 54 | FilesystemType::SYSFS, 55 | Mountflags::empty(), 56 | None, 57 | ) 58 | .map_err(|e| Error::MountPseudo(format!("Failed to mount sysfs types at /sys: {e}")))?; 59 | mount( 60 | MOUNT_NONE_SOURCE, 61 | DEV, 62 | FilesystemType::DEVTMPFS, 63 | Mountflags::empty(), 64 | None, 65 | ) 66 | .map_err(|e| Error::MountPseudo(format!("Failed to mount devtmpfs at /dev: {e}")))?; 67 | Ok(()) 68 | } 69 | 70 | enum AuthMethod { 71 | File(UnixString), 72 | Pass(String), 73 | } 74 | 75 | const PASS_BUF_CAP: usize = 64; 76 | const DEV_MAPPER_CROOT: &UnixStr = UnixStr::from_str_checked("/dev/mapper/croot\0"); 77 | const MNT_ROOT: &UnixStr = UnixStr::from_str_checked("/mnt/root\0"); 78 | 79 | pub fn prep_user_filesystems(cfg: &Cfg) -> Result<()> { 80 | let parts = get_partitions(cfg) 81 | .map_err(|e| Error::Mount(format!("Failed to find partitions {e:?}")))?; 82 | let mut pass_buf = [0u8; PASS_BUF_CAP]; 83 | let auth_method = 84 | if let Some(file) = cfg.crypt_file.clone() { 85 | AuthMethod::File(UnixString::try_from_string(file).map_err(|_e| { 86 | Error::Crypt("Failed to convert crypt file to a unix str".to_string()) 87 | })?) 88 | } else { 89 | print_pending!("Enter passphrase for decryption: "); 90 | let pass = get_pass(&mut pass_buf) 91 | .map_err(|e| Error::Crypt(format!("Failed to get password for decryption: {e}")))?; 92 | AuthMethod::Pass(pass.trim_end_matches('\n').to_string()) 93 | }; 94 | try_decrypt_parallel( 95 | &UnixString::try_from_str(&parts.root) 96 | .map_err(|_e| Error::Crypt("Failed to convert root uuid to a unix str".to_string()))?, 97 | unix_lit!("croot"), 98 | &UnixString::try_from_str(&parts.swap) 99 | .map_err(|_e| Error::Crypt("Failed to convert swap uuid to a unix str".to_string()))?, 100 | tiny_std::unix_lit!("cswap"), 101 | &UnixString::try_from_str(&parts.home) 102 | .map_err(|_e| Error::Crypt("Failed to convert home uuid to a unix str".to_string()))?, 103 | tiny_std::unix_lit!("chome"), 104 | &auth_method, 105 | )?; 106 | 107 | mount( 108 | DEV_MAPPER_CROOT, 109 | MNT_ROOT, 110 | FilesystemType::EXT4, 111 | Mountflags::empty(), 112 | None, 113 | ) 114 | .map_err(|e| { 115 | Error::Mount(format!( 116 | "Failed to mount root partition {} to /mnt/root: {e:?}", 117 | parts.root 118 | )) 119 | })?; 120 | Ok(()) 121 | } 122 | 123 | fn try_decrypt_parallel( 124 | root_uuid: &UnixStr, 125 | root_target: &UnixStr, 126 | swap_uuid: &UnixStr, 127 | swap_target: &UnixStr, 128 | home_uuid: &UnixStr, 129 | home_target: &UnixStr, 130 | auth_method: &AuthMethod, 131 | ) -> Result<()> { 132 | let root_c = spawn_open_cryptodisk(root_uuid, root_target, auth_method)?; 133 | let swap_c = spawn_open_cryptodisk(swap_uuid, swap_target, auth_method)?; 134 | let home_c = spawn_open_cryptodisk(home_uuid, home_target, auth_method)?; 135 | match ( 136 | handle_crypto_child(root_c), 137 | handle_crypto_child(swap_c), 138 | handle_crypto_child(home_c), 139 | ) { 140 | (Ok(()), Ok(()), Ok(())) => return Ok(()), 141 | (mut root_res, mut home_res, mut swap_res) => { 142 | for i in 0..3 { 143 | print_error!("Failed to decrypt a partition: root-failed={}, home-failed={}, swap-failed={}, will try again with passphrase, attempt {i}", root_res.is_ok(), home_res.is_ok(), swap_res.is_ok()); 144 | print_pending!("Enter passphrase for decryption: "); 145 | let mut pass_buffer = [0u8; PASS_BUF_CAP]; 146 | let pass = get_pass(&mut pass_buffer) 147 | .map_err(|e| { 148 | Error::Crypt(format!("Failed to get password for decryption: {e}")) 149 | })? 150 | .trim(); 151 | let try_auth = AuthMethod::Pass(pass.trim_end_matches('\n').to_string()); 152 | if root_res.is_err() { 153 | root_res = handle_crypto_child(spawn_open_cryptodisk( 154 | root_uuid, 155 | root_target, 156 | &try_auth, 157 | )?); 158 | } 159 | if home_res.is_err() { 160 | home_res = handle_crypto_child(spawn_open_cryptodisk( 161 | home_uuid, 162 | home_target, 163 | &try_auth, 164 | )?); 165 | } 166 | if swap_res.is_err() { 167 | swap_res = handle_crypto_child(spawn_open_cryptodisk( 168 | swap_uuid, 169 | swap_target, 170 | &try_auth, 171 | )?); 172 | } 173 | } 174 | } 175 | } 176 | Ok(()) 177 | } 178 | 179 | const BIN_BUSYBOX: &UnixStr = UnixStr::from_str_checked("/bin/busybox\0"); 180 | 181 | pub fn run_mdev() -> Result<()> { 182 | const MDEV: &UnixStr = UnixStr::from_str_checked("mdev\0"); 183 | const S: &UnixStr = UnixStr::from_str_checked("-s\0"); 184 | let mut cmd = Command::new(BIN_BUSYBOX) 185 | .map_err(|e| Error::Spawn(format!("Failed to create command /bin/busybox: {e}")))?; 186 | cmd.arg(MDEV).arg(S); 187 | let exit = cmd 188 | .spawn() 189 | .map_err(|e| Error::Spawn(format!("Failed to spawn /bin/busybox mdev -s: {e}")))? 190 | .wait() 191 | .map_err(|e| { 192 | Error::Spawn(format!( 193 | "Failed to wait for process exit for /bin/busybox mdev -s: {e}" 194 | )) 195 | })?; 196 | if exit != 0 { 197 | return Err(Error::Spawn(format!( 198 | "Got bad exit code from /bin/busybox mdev -s: {exit}" 199 | ))); 200 | } 201 | Ok(()) 202 | } 203 | 204 | #[cfg_attr(test, derive(Debug))] 205 | pub struct Partitions { 206 | pub root: String, 207 | pub swap: String, 208 | pub home: String, 209 | } 210 | 211 | pub fn get_partitions(cfg: &Cfg) -> Result { 212 | const BLKID: &UnixStr = UnixStr::from_str_checked("blkid\0"); 213 | let mut cmd = Command::new(BIN_BUSYBOX) 214 | .map_err(|e| Error::Spawn(format!("Failed to instantiate busybox command {e}")))?; 215 | cmd.arg(BLKID); 216 | let tgt = spawn_await_stdout(cmd, 4096)?; 217 | let mut root = None; 218 | let mut swap = None; 219 | let mut home = None; 220 | for line in tgt.lines() { 221 | // Dirty just checking contains, which essentially mean we also accept part-uuids since they 222 | // are on the same line. 223 | 224 | // /dev/nvme1n1p4: ...UUID=... etc 225 | if line.contains(&cfg.root_uuid) { 226 | let (part, _discard_rest) = line.split_once(':') 227 | .ok_or_else(|| Error::FindPartitions(format!("Failed to find root partition device name on blkid line that contains the specified uuid={}, line={line}", cfg.root_uuid)))?; 228 | root = Some(part.to_string()) 229 | } else if line.contains(&cfg.swap_uuid) { 230 | let (part, _discard_rest) = line.split_once(':') 231 | .ok_or_else(|| Error::FindPartitions(format!("Failed to find swap partition device name on blkid line that contains the specified uuid={}, line={line}", cfg.swap_uuid)))?; 232 | swap = Some(part.to_string()) 233 | } else if line.contains(&cfg.home_uuid) { 234 | let (part, _discard_rest) = line.split_once(':') 235 | .ok_or_else(|| Error::FindPartitions(format!("Failed to find home partition device name on blkid line that contains the specified uuid={}, line={line}", cfg.home_uuid)))?; 236 | home = Some(part.to_string()) 237 | } 238 | } 239 | Ok(Partitions { 240 | root: root.ok_or_else(|| { 241 | Error::FindPartitions(format!( 242 | "Failed to find root partition={} from blkid", 243 | cfg.root_uuid 244 | )) 245 | })?, 246 | swap: swap.ok_or_else(|| { 247 | Error::FindPartitions(format!( 248 | "Failed to find swap partition={} from blkid", 249 | cfg.swap_uuid 250 | )) 251 | })?, 252 | home: home.ok_or_else(|| { 253 | Error::FindPartitions(format!( 254 | "Failed to find home partition={} from blkid", 255 | cfg.home_uuid 256 | )) 257 | })?, 258 | }) 259 | } 260 | 261 | const CRYPTSETUP: &UnixStr = UnixStr::from_str_checked("/sbin/cryptsetup\0"); 262 | 263 | pub(crate) fn spawn_open_cryptodisk( 264 | device_name: &UnixStr, 265 | target_name: &UnixStr, 266 | auth: &AuthMethod, 267 | ) -> Result { 268 | const KEY_FILE: &UnixStr = UnixStr::from_str_checked("keyfile\0"); 269 | const KEY_FILE_ARG: &UnixStr = UnixStr::from_str_checked("--key-file\0"); 270 | const OPEN: &UnixStr = UnixStr::from_str_checked("open\0"); 271 | let key_file = match auth { 272 | AuthMethod::File(f) => f.clone(), 273 | AuthMethod::Pass(pass) => match tiny_std::fs::metadata(KEY_FILE) { 274 | Ok(_) => UnixString::from(KEY_FILE), 275 | Err(e) => { 276 | if e.matches_errno(Errno::ENOENT) { 277 | let mut file = tiny_std::fs::OpenOptions::new() 278 | .write(true) 279 | .create(true) 280 | .open(KEY_FILE) 281 | .map_err(|e| Error::Crypt(format!("Failed to open/create keyfile {e}")))?; 282 | file.write_all(pass.as_bytes()) 283 | .map_err(|e| Error::Crypt(format!("Failed to write keyfile {e}")))?; 284 | UnixString::from(KEY_FILE) 285 | } else { 286 | return Err(Error::Crypt(format!( 287 | "Failed to check for existing keyfile {e}" 288 | ))); 289 | } 290 | } 291 | }, 292 | }; 293 | 294 | print_ok!("Attempting to open {:?}", target_name); 295 | let child = tiny_std::process::Command::new(CRYPTSETUP) 296 | .map_err(|e| { 297 | Error::Crypt(format!( 298 | "Failed to instantiate command /sbin/cryptsetup {e}" 299 | )) 300 | })? 301 | .arg(unix_lit!("--allow-discards")) 302 | .arg(KEY_FILE_ARG) 303 | .arg(&key_file) 304 | .arg(OPEN) 305 | .arg(device_name) 306 | .arg(target_name) 307 | .spawn() 308 | .map_err(|e| Error::Crypt(format!("Failed to spawn /sbin/cryptsetup {e}")))?; 309 | Ok(child) 310 | } 311 | 312 | pub(crate) fn handle_crypto_child(mut child: Child) -> Result<()> { 313 | let res = child.wait().map_err(|e| { 314 | Error::Crypt(format!( 315 | "Failed to await for child process /sbin/cryptsetup: {e}" 316 | )) 317 | })?; 318 | if res != 0 { 319 | return Err(Error::Crypt(format!( 320 | "Got error from /sbin/cryptsetup, code {res}" 321 | ))); 322 | } 323 | Ok(()) 324 | } 325 | 326 | pub(crate) fn spawn_await_stdout(mut cmd: Command, buf_size: usize) -> Result { 327 | let mut child = cmd 328 | .stdout(Stdio::MakePipe) 329 | .spawn() 330 | .map_err(|e| Error::Spawn(format!("Failed to spawn command {e}")))?; 331 | let res = child 332 | .wait() 333 | .map_err(|e| Error::Spawn(format!("Failed to wait for child to exit {e}")))?; 334 | if res != 0 { 335 | return Err(Error::Spawn(format!("Got bad exit code {res} from child"))); 336 | } 337 | let mut buf = vec![0u8; buf_size]; 338 | let mut stdout = child 339 | .stdout 340 | .ok_or_else(|| Error::Spawn("Failed to get child stdout handle".to_string()))?; 341 | let read_bytes = stdout 342 | .read(&mut buf) 343 | .map_err(|e| Error::Spawn(format!("Failed to read from child stdout handle {e}")))?; 344 | // Maybe don't double alloc here but who cares really 345 | String::from_utf8(buf[..read_bytes].to_vec()) 346 | .map_err(|e| Error::Spawn(format!("Failed to convert child stdout to utf8 {e}"))) 347 | } 348 | 349 | const ABS_PROC: &UnixStr = UnixStr::from_str_checked("/proc\0"); 350 | const ABS_SYS: &UnixStr = UnixStr::from_str_checked("/sys\0"); 351 | // This can fail without it necessarily being a problem 352 | pub fn try_unmount() -> Result<()> { 353 | if let Err(e) = unmount(ABS_PROC) { 354 | eprintln!("Failed to unmount proc fs: {e}"); 355 | } 356 | if let Err(e) = unmount(ABS_SYS) { 357 | eprintln!("Failed to unmount sysfs {e}"); 358 | } 359 | // Don't try to unmount /dev, we're using it 360 | Ok(()) 361 | } 362 | 363 | pub fn switch_root() -> Error { 364 | const SWITCH_ROOT: &UnixStr = UnixStr::from_str_checked("switch_root\0"); 365 | const SBIN_INIT: &UnixStr = UnixStr::from_str_checked("/sbin/init\0"); 366 | let mut cmd = match Command::new(BIN_BUSYBOX) { 367 | Ok(cmd) => cmd, 368 | Err(e) => return Error::Spawn(format!("Failed to create command /bin/busybox: {e}")), 369 | }; 370 | cmd.arg(SWITCH_ROOT).arg(MNT_ROOT).arg(SBIN_INIT); 371 | let e = cmd.exec(); 372 | Error::Spawn(format!( 373 | "Failed to execute '/bin/busybox switch_root /mnt/root /sbin/init': {e}" 374 | )) 375 | } 376 | 377 | pub fn bail_to_shell() -> Error { 378 | const SH: &UnixStr = UnixStr::from_str_checked("sh\0"); 379 | eprintln!("Bailing to shell, good luck."); 380 | let mut cmd = match Command::new(BIN_BUSYBOX) { 381 | Ok(cmd) => cmd, 382 | Err(e) => { 383 | return Error::Bail(format!( 384 | "Failed to create command /bin/busybox when bailing: {e}" 385 | )) 386 | } 387 | }; 388 | cmd.arg(SH); 389 | let e = cmd.exec(); 390 | Error::Bail(format!( 391 | "Failed to run exec on '/bin/busybox sh' when bailing: {e}" 392 | )) 393 | } 394 | 395 | #[derive(Debug)] 396 | pub struct Cfg { 397 | pub root_uuid: String, 398 | pub swap_uuid: String, 399 | pub home_uuid: String, 400 | pub pass_salt: Option, 401 | pub crypt_file: Option, 402 | } 403 | 404 | pub fn read_cfg(cfg_path: &UnixStr) -> Result { 405 | let content = tiny_std::fs::read_to_string(cfg_path) 406 | .map_err(|e| Error::Cfg(format!("Failed to read cfg at {cfg_path:?}: {e}")))?; 407 | let mut root_uuid = None; 408 | let mut swap_uuid = None; 409 | let mut home_uuid = None; 410 | let mut pass_salt = None; 411 | let mut crypt_file = None; 412 | for (ind, line) in content.lines().enumerate() { 413 | let trimmed = line.trim(); 414 | if trimmed.is_empty() { 415 | continue; 416 | } 417 | // Allow comments 418 | if trimmed.starts_with("//") { 419 | continue; 420 | } 421 | let (key, value) = trimmed.split_once('=') 422 | .ok_or_else(|| Error::Cfg(format!("Found non empty line that doesn't contain '=' or starts with '//' [{ind}]: '{line}'")))?; 423 | match key { 424 | "root" => root_uuid = Some(value.to_string()), 425 | "home" => home_uuid = Some(value.to_string()), 426 | "swap" => swap_uuid = Some(value.to_string()), 427 | "password_salt" => pass_salt = Some(value.to_string()), 428 | "crypt_file" => crypt_file = Some(value.to_string()), 429 | other => { 430 | return Err(Error::Cfg(format!( 431 | "Unrecognized key in config file {other} at [{ind}]: '{line}'" 432 | ))) 433 | } 434 | } 435 | } 436 | Ok(Cfg { 437 | root_uuid: root_uuid 438 | .ok_or_else(|| Error::Cfg(format!("No root uuid found in cfg at path {cfg_path:?}")))?, 439 | swap_uuid: swap_uuid 440 | .ok_or_else(|| Error::Cfg(format!("No swap uuid found in cfg at path {cfg_path:?}")))?, 441 | home_uuid: home_uuid 442 | .ok_or_else(|| Error::Cfg(format!("No home uuid found in cfg at path {cfg_path:?}")))?, 443 | pass_salt, 444 | crypt_file, 445 | }) 446 | } 447 | 448 | pub fn write_cfg(cfg: &Cfg, path: &UnixStr) -> core::result::Result<(), String> { 449 | let pass = if let Some(salt) = &cfg.pass_salt { 450 | format!("password_salt={salt}\n") 451 | } else { 452 | "".to_string() 453 | }; 454 | let crypt = if let Some(crypt) = &cfg.crypt_file { 455 | format!("crypt_file={crypt}\n") 456 | } else { 457 | "".to_string() 458 | }; 459 | let content = format!( 460 | "home={}\nroot={}\nswap={}\n{pass}{crypt}", 461 | cfg.home_uuid, cfg.root_uuid, cfg.swap_uuid 462 | ); 463 | let mut file = tiny_std::fs::OpenOptions::new() 464 | .write(true) 465 | .create(true) 466 | .truncate(true) 467 | .open(path) 468 | .map_err(|e| format!("Failed to open file for reading at {path:?}: {e}"))?; 469 | file.write_all(content.as_bytes()) 470 | .map_err(|e| format!("Failed to write content into file at {path:?}: {e}"))?; 471 | Ok(()) 472 | } 473 | 474 | #[cfg(test)] 475 | mod tests { 476 | use super::*; 477 | use tiny_std::println; 478 | 479 | // Needs your testing machine's disk uuids 480 | #[test] 481 | #[ignore] 482 | fn test_blkid() { 483 | let cfg = read_cfg(UnixStr::from_str_checked( 484 | "/home/gramar/code/rust/yubi-initramfs/initramfs.cfg\0", 485 | )) 486 | .unwrap(); 487 | let parts = get_partitions(&cfg).unwrap(); 488 | println!("{parts:?}"); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /initramfs-lib/src/print.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Copy, Clone)] 2 | pub enum Color { 3 | Green, 4 | Red, 5 | Yellow, 6 | } 7 | 8 | impl Color { 9 | pub fn to_code(self) -> &'static str { 10 | match self { 11 | Color::Green => "\u{001b}[32;1m", 12 | Color::Red => "\u{001b}[31;1m", 13 | Color::Yellow => "\u{001b}[33;1m", 14 | } 15 | } 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! format_colored { 20 | ($color: expr, $fmt:expr) => {{ 21 | format_args!("{}{}\u{001b}[0m", $color.to_code(), format_args!($fmt)) 22 | }}; 23 | 24 | ($color: expr, $fmt:expr, $($args:tt)*) => {{ 25 | format_args!("{}{}\u{001b}[0m", $color.to_code(), format_args!($fmt, $($args)*)) 26 | }}; 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! print_ok { 31 | ($fmt:expr) => { 32 | tiny_std::println!("{}{}", 33 | $crate::format_colored!($crate::print::Color::Green, "[initramfs-lib]: "), 34 | format_args!($fmt) 35 | ) 36 | }; 37 | ($fmt:expr, $($args:tt)*) => { 38 | tiny_std::println!("{}{}", 39 | $crate::format_colored!($crate::print::Color::Green, "[initramfs-lib]: "), 40 | format_args!($fmt, $($args)*) 41 | ) 42 | }; 43 | } 44 | 45 | #[macro_export] 46 | macro_rules! print_pending { 47 | ($fmt:expr) => { 48 | tiny_std::println!("{}{}", 49 | $crate::format_colored!($crate::print::Color::Yellow, "[initramfs-lib]: "), 50 | format_args!($fmt) 51 | ) 52 | }; 53 | ($fmt:expr, $($args:tt)*) => { 54 | tiny_std::println!("{}{}", 55 | $crate::format_colored!($crate::print::Color::Yellow, "[initramfs-lib]: "), 56 | format_args!($fmt, $($args)*) 57 | ) 58 | }; 59 | } 60 | 61 | #[macro_export] 62 | macro_rules! print_error { 63 | ($fmt:expr) => { 64 | tiny_std::println!("{}{}", 65 | $crate::format_colored!($crate::print::Color::Red, "[initramfs-lib]: "), 66 | format_args!($fmt) 67 | ) 68 | }; 69 | ($fmt:expr, $($args:tt)*) => { 70 | tiny_std::println!("{}{}", 71 | $crate::format_colored!($crate::print::Color::Red, "[initramfs-lib]: "), 72 | format_args!($fmt, $($args)*) 73 | ) 74 | }; 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use crate::print::Color; 80 | use tiny_std::println; 81 | 82 | #[test] 83 | fn test_print() { 84 | println!("{}", format_colored!(Color::Green, "Hello")); 85 | println!( 86 | "{}", 87 | format_colored!(Color::Red, "My fmt {}", "other thing") 88 | ); 89 | print_ok!("Hello again!"); 90 | print_ok!("Hello {} again!", "with format args"); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /initramfs-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "initramfs-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | initramfs-lib = { workspace = true } 10 | rusl = { workspace = true, features = ["alloc"] } 11 | tiny-std = { workspace = true, features = ["alloc", "executable", "global-allocator"]} -------------------------------------------------------------------------------- /initramfs-rs/src/app.rs: -------------------------------------------------------------------------------- 1 | use initramfs_lib::{bail_to_shell, print_error, print_ok, read_cfg, Cfg}; 2 | use tiny_std::{unix_lit, UnixString}; 3 | 4 | /// Some references [Gentoo custom initramfs](https://wiki.gentoo.org/wiki/Custom_Initramfs) 5 | /// [Boot kernel without bootloader](https://tecporto.pt/wiki/index.php/Booting_the_Linux_Kernel_without_a_bootloader) 6 | pub(crate) fn main_loop() -> Result<(), i32> { 7 | let mut args = tiny_std::env::args(); 8 | // First arg is the path to this binary, by convention at least, might be POSIX. 9 | let _self = args.next(); 10 | let cfg_path = args.next(); 11 | if cfg_path.is_none() { 12 | print_ok!("Invoked without arguments, assuming running as init."); 13 | let cfg = read_cfg(unix_lit!("initramfs.cfg")).map_err(|e| { 14 | print_error!( 15 | "Invoked without arguments and failed to read cfg at $pwd/initramfs.cfg; {e:?}" 16 | ); 17 | 1 18 | })?; 19 | return run_init(&cfg); 20 | } 21 | 22 | let cfg_path = cfg_path 23 | .ok_or_else(|| { 24 | print_error!("No cfg path supplied, required as first argument"); 25 | 1 26 | })? 27 | .map_err(|e| { 28 | print_error!("First arg not parseable as utf8: {e}"); 29 | 1 30 | })?; 31 | let command = args 32 | .next() 33 | .ok_or_else(|| { 34 | print_error!("Missing command argument"); 35 | 1 36 | })? 37 | .map_err(|e| { 38 | print_error!("Command arg not parseable as utf8: {e}"); 39 | 1 40 | })?; 41 | let cfg = read_cfg(&UnixString::try_from_str(cfg_path).map_err(|_e| { 42 | print_error!("Failed to convert cfg path to a UnixString"); 43 | 1 44 | })?) 45 | .map_err(|e| { 46 | print_error!("Failed to read cfg: {e:?}"); 47 | 1 48 | })?; 49 | match command { 50 | "--bail" | "-b" => { 51 | print_error!("Bailing to shell"); 52 | let e = bail_to_shell(); 53 | print_error!("Failed to bail to shell: {e:?}"); 54 | Err(1) 55 | } 56 | "--list-partitions" | "-l" => { 57 | let partitions = initramfs_lib::get_partitions(&cfg).map_err(|e| { 58 | print_error!("Error: Failed to get partitions: {e:?}"); 59 | 1 60 | })?; 61 | print_ok!( 62 | "Successfully found partitions.\nRoot: {}\nSwap: {}\nHome: {}", 63 | partitions.root, 64 | partitions.swap, 65 | partitions.home 66 | ); 67 | Ok(()) 68 | } 69 | "--mount-pseudo" | "-p" => { 70 | initramfs_lib::mount_pseudo_filesystems().map_err(|e| { 71 | print_error!("Error: Failed to mount pseudo filesystems {e:?}"); 72 | 1 73 | })?; 74 | print_ok!("Successfully mounted pseudo filesystem."); 75 | Ok(()) 76 | } 77 | "--run-mdev" | "-m" => { 78 | initramfs_lib::run_mdev().map_err(|e| { 79 | print_error!("Error: Failed to run mdev {e:?}"); 80 | 1 81 | })?; 82 | print_ok!("Successfully ran mdev."); 83 | Ok(()) 84 | } 85 | "--mount-user" | "-u" => { 86 | initramfs_lib::prep_user_filesystems(&cfg).map_err(|e| { 87 | print_error!( 88 | "Error: Failed to mount user filesystems using cfg at path {cfg:?}: {e:?}" 89 | ); 90 | 1 91 | })?; 92 | Ok(()) 93 | } 94 | "--switch" | "-s" => { 95 | // Cannot return with anything but an error 96 | let err = initramfs_lib::switch_root(); 97 | print_error!("Error: Failed to switch root {err:?}"); 98 | Err(1) 99 | } 100 | "--init" => run_init(&cfg), 101 | s => { 102 | print_error!("Unrecognized argument {s}"); 103 | Err(1) 104 | } 105 | } 106 | } 107 | 108 | fn run_init(cfg: &Cfg) -> Result<(), i32> { 109 | if let Err(e) = initramfs_lib::full_init(cfg) { 110 | print_error!("Error: Failed init full {e:?}"); 111 | let e = bail_to_shell(); 112 | print_error!("Error: Failed to bail to shell {e:?}, dying."); 113 | return Err(1); 114 | } 115 | print_ok!("Successfully ran init setup"); 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /initramfs-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | mod app; 5 | 6 | extern crate alloc; 7 | 8 | #[no_mangle] 9 | fn main() -> i32 { 10 | if let Err(exit) = app::main_loop() { 11 | exit 12 | } else { 13 | 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /new-boot-src.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Helper script to update bootloader change variables below, as-is, requires that .zshrc exists, 3 | # easily edited though 4 | set -e 5 | 6 | BOOT_LOCATION="" 7 | BOOT_DISK="/dev/" 8 | USER="" 9 | INITRAMFS_LOCATION="" 10 | 11 | # Path to built boot image 12 | IMG="/usr/src/$VERSION/arch/x86/boot/bzImage" 13 | REPO="/home/$USER/code/rust/boot-rs" 14 | BOOT_IMG_DEST="/boot/EFI/boot-rs.efi" 15 | # Get this from efibootmgr, don't know else you find this, check the Readme for creating a boot entry 16 | # efibootmgr outputs this info after that 17 | BOOT_EFI_DISK="HD(,GPT,,,)" 18 | 19 | BOOT_DISK_MOUNTED_CORRECTLY="y" 20 | BOOT_DISK_MOUNTED_INCORRECTLY="i" 21 | BOOT_DISK_NOT_MOUNTED="n" 22 | 23 | check_boot_mounted() { 24 | GREP_RES=$(grep "$BOOT_DISK" /proc/mounts) 25 | if [ -z "$GREP_RES" ] ; then 26 | echo "$BOOT_DISK_NOT_MOUNTED" 27 | else 28 | BOOT_GREP_RES=$(echo "$GREP_RES" | grep -qs "$BOOT_LOCATION") 29 | if [ $? == 0 ] ; then 30 | echo "$BOOT_DISK_MOUNTED_CORRECTLY" 31 | else 32 | echo "$BOOT_DISK_MOUNTED_INCORRECTLY" 33 | fi 34 | fi 35 | } 36 | 37 | cleanup() { 38 | BOOT_MOUNTED_RES=$(check_boot_mounted) 39 | if [ "$BOOT_MOUNTED_RES" == "$BOOT_DISK_MOUNTED_CORRECTLY" ] ; then 40 | echo "Unmounting boot" 41 | umount /boot 42 | fi 43 | } 44 | 45 | trap cleanup EXIT 46 | 47 | # Check args correctness 48 | 49 | if [ -z "$1" ]; then 50 | echo "Need kernel version to use supplied as first argument" 51 | exit 1 52 | fi 53 | 54 | # Check that first arg is at least an existing file 55 | 56 | if stat "$1" >> /dev/null ; then 57 | echo "Using $1" 58 | else 59 | echo "Failed to stat $1 needs to be kernel source directory" 60 | exit 1 61 | fi 62 | 63 | # Get Linux version, ex: "linux-6.5.2-gentoo" 64 | VERSION=$(basename $1) 65 | 66 | # Check that the image exists 67 | if stat "$IMG" >> /dev/null ; then 68 | echo "Found built kernel image at $IMG" 69 | else 70 | echo "Could not find built kernel image at $IMG, kernel needs to be built" 71 | exi t1 72 | fi 73 | 74 | # Check that the initramfs seems okay 75 | if stat "$INITRAMFS_LOCATION" >> /dev/null ; then 76 | # Easy to forget to place the init executable file in there 77 | if stat "$INITRAMFS_LOCATION/init" >> /dev/null ; then 78 | echo "Found correctly setup initramfs at $INITRAMFS_LOCATION" 79 | else 80 | echo "Initramfs directory $INITRAMFS_LOCATION does not contain $INITRAMFS_LOCATION/init" 81 | exit 1 82 | fi 83 | else 84 | echo "Didn't find the expected initramfs directory at $INITRAMFS_LOCATION" 85 | exit 1 86 | fi 87 | 88 | BOOT_MOUNTED_RES=$(check_boot_mounted) 89 | 90 | # Check that the boot disk mount point looks correct 91 | if [ "$BOOT_MOUNTED_RES" == "$BOOT_DISK_MOUNTED_INCORRECTLY" ] ; then 92 | echo "$BOOT_DISK is mounted on a bad mount point" 93 | exit 1 94 | elif [ "$BOOT_MOUNTED_RES" == "$BOOT_DISK_MOUNTED_CORRECTLY" ] ; then 95 | echo "$BOOT_DISK is mounted correctly" 96 | elif [ "$BOOT_MOUNTED_RES" == "$BOOT_DISK_NOT_MOUNTED" ] ; then 97 | echo "Mounting $BOOT_DISK to $BOOT_LOCATION" 98 | mount "$BOOT_DISK" "$BOOT_LOCATION" 99 | fi 100 | 101 | # Build boot-strap 102 | runuser -l "$USER" -c "source /home/$USER/.zshrc && cd $REPO && ./build_strap.sh --profile lto" 103 | 104 | # Make sure it was built 105 | if stat "$REPO/target/x86_64-unknown-linux-gnu/lto/boot-strap" >> /dev/null ; then 106 | echo "Successfully built boot-strap" 107 | else 108 | echo "Failed to find result binary for boot-strap" 109 | exit1 110 | fi 111 | 112 | read -p "Everything checks out, continue writing kernel image to boot disk? [y]: " WRITE_IMG 113 | 114 | # Continue to write the new kernel image to disk, if requested 115 | if [[ "y" == "$WRITE_IMG" ]] ; then 116 | "$REPO/target/x86_64-unknown-linux-gnu/lto/boot-strap" boot -i "$IMG" -e "/boot/EFI/gentoo/$VERSION.enc" -c "$REPO/boot.cfg" -d "$BOOT_EFI_DISK" -p "/EFI/gentoo/$VERSION.enc" 117 | sync 118 | else 119 | echo "Exiting" 120 | exit 0 121 | fi 122 | 123 | # Build boot image 124 | runuser -l "$USER" -c "source /home/$USER/.zshrc && cd $REPO && cargo clean && ./build_boot.sh --profile lto && sync" 125 | 126 | sync 127 | NEW_BOOT_IMG="$REPO/target/x86_64-unknown-uefi/lto/boot-rs.efi" 128 | 129 | # Check that the new boot image was built 130 | if stat "$NEW_BOOT_IMG" >> /dev/null ; then 131 | echo "Successfully built boot-image" 132 | else 133 | echo "Failed to find result binary for boot-image at $NEW_BOOT_IMG" 134 | exit1 135 | fi 136 | 137 | # Check if a boot image already exists 138 | if stat "$BOOT_IMG_DEST" >> /dev/null ; then 139 | read -p "Make a backup of the old boot image? [y]: " MAKE_BACKUP 140 | # Backup if requested 141 | if [[ "y" == $MAKE_BACKUP ]] ; then 142 | cp "$BOOT_IMG_DEST" "$BOOT_IMG_DEST.bak" && sync 143 | echo "Backed up old boot image to $BOOT_IMG_DEST.bak" 144 | fi 145 | else 146 | echo "No old boot image found at $BOOT_IMG_DEST, skipping backup" 147 | fi 148 | 149 | read -p "Write new boot image to $BOOT_IMG_DEST? [y]: " EXEC_WRITE 150 | 151 | # Check if user wants to do the bootloader overwrite 152 | if [[ "y" == $EXEC_WRITE ]] ; then 153 | # Copy over the new bootloader 154 | cp "$NEW_BOOT_IMG" "$BOOT_IMG_DEST" && sync 155 | echo "Wrote new boot image to $BOOT_IMG_DEST" 156 | else 157 | echo "Exiting" 158 | exit 0 159 | fi 160 | 161 | echo "Successfully built new kernel boot" 162 | 163 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | [toolchain] 3 | channel = "stable" 4 | components = ["clippy"] 5 | --------------------------------------------------------------------------------