diff --git a/Cargo.lock b/Cargo.lock index b1f2f05..5b94073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,7 @@ dependencies = [ "nix", "owo-colors", "plist", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -1333,6 +1334,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1394,6 +1401,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -1409,6 +1437,15 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -1808,7 +1845,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "rand", + "rand 0.4.6", "remove_dir_all", ] diff --git a/Cargo.toml b/Cargo.toml index 2f9e5fe..6486b30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,3 +48,4 @@ dirs = "4.0.0" erased-serde = "0.3.23" typetag = "0.2.3" dyn-clone = "1.0.9" +rand = "0.8.5" diff --git a/src/action/darwin/create_apfs_volume.rs b/src/action/darwin/create_apfs_volume.rs index 2d280fd..c3b1c15 100644 --- a/src/action/darwin/create_apfs_volume.rs +++ b/src/action/darwin/create_apfs_volume.rs @@ -18,14 +18,14 @@ use crate::{ BoxableError, }; -const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; +pub const NIX_VOLUME_MOUNTD_DEST: &str = "/Library/LaunchDaemons/org.nixos.darwin-store.plist"; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct CreateApfsVolume { disk: PathBuf, name: String, case_sensitive: bool, - encrypt: Option, + encrypt: bool, create_or_append_synthetic_conf: CreateOrAppendFile, create_synthetic_objects: CreateSyntheticObjects, unmount_volume: UnmountVolume, @@ -44,7 +44,7 @@ impl CreateApfsVolume { disk: impl AsRef, name: String, case_sensitive: bool, - encrypt: Option, + encrypt: bool, ) -> Result> { let disk = disk.as_ref(); let create_or_append_synthetic_conf = CreateOrAppendFile::plan( @@ -73,13 +73,13 @@ impl CreateApfsVolume { .await .map_err(|e| e.boxed())?; - let encrypt_volume = if let Some(password) = encrypt.as_ref() { - Some(EncryptVolume::plan(disk, password.to_string()).await?) + let encrypt_volume = if encrypt { + Some(EncryptVolume::plan(disk, &name).await?) } else { None }; - let mount_command = if encrypt.is_some() { + let mount_command = if encrypt { vec![ "/bin/sh", "-c", diff --git a/src/action/darwin/encrypt_volume.rs b/src/action/darwin/encrypt_volume.rs index 8e8f5c3..c7ce6b1 100644 --- a/src/action/darwin/encrypt_volume.rs +++ b/src/action/darwin/encrypt_volume.rs @@ -1,11 +1,15 @@ +use crate::{ + action::{darwin::NIX_VOLUME_MOUNTD_DEST, Action, ActionDescription, ActionState}, + execute_command, +}; +use rand::Rng; use std::path::{Path, PathBuf}; - -use crate::action::{Action, ActionDescription, ActionState}; +use tokio::process::Command; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct EncryptVolume { disk: PathBuf, - password: String, + name: String, action_state: ActionState, } @@ -13,11 +17,12 @@ impl EncryptVolume { #[tracing::instrument(skip_all)] pub async fn plan( disk: impl AsRef, - password: String, + name: impl AsRef, ) -> Result> { + let name = name.as_ref().to_owned(); Ok(Self { + name, disk: disk.as_ref().to_path_buf(), - password, action_state: ActionState::Uncompleted, }) } @@ -42,8 +47,8 @@ impl Action for EncryptVolume { ))] async fn execute(&mut self) -> Result<(), Box> { let Self { - disk: _, - password: _, + disk, + name, action_state, } = self; if *action_state == ActionState::Completed { @@ -52,7 +57,65 @@ impl Action for EncryptVolume { } tracing::debug!("Encrypting volume"); - todo!(); + // Generate a random password. + let password: String = { + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789)(*&^%$#@!~"; + const PASSWORD_LEN: usize = 32; + let mut rng = rand::thread_rng(); + + (0..PASSWORD_LEN) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect() + }; + + let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ + + // Add the password to the user keychain so they can unlock it later. + let _password_output = execute_command( + Command::new("/usr/bin/security").args([ + "add-generic-password", + "-a", + disk_str, + "-s", + name.as_str(), + "-l", + format!("{} encryption password", disk_str).as_str(), + "-D", + "Encrypted volume password", + "-j", + format!( + "Added automatically by the Nix installer for use by {NIX_VOLUME_MOUNTD_DEST}" + ) + .as_str(), + "-w", + password.as_str(), + "-T", + "/System/Library/CoreServices/APFSUserAgent", + "-T", + "/System/Library/CoreServices/CSUserAgent", + "-T", + "/usr/bin/security", + "/Library/Keychains/System.keychain", + ]), + ) + .await?; + + // Encrypt the mounted volume + execute_command(Command::new("/usr/sbin/diskutil").args([ + "apfs", + "encryptVolume", + disk_str, + "-user", + "disk", + "-passphrase", + password.as_str(), + ])) + .await?; tracing::trace!("Encrypted volume"); *action_state = ActionState::Completed; @@ -73,7 +136,7 @@ impl Action for EncryptVolume { async fn revert(&mut self) -> Result<(), Box> { let Self { disk: _, - password: _, + name: _, action_state, } = self; if *action_state == ActionState::Uncompleted { diff --git a/src/action/darwin/mod.rs b/src/action/darwin/mod.rs index 0cf48c7..bc3a525 100644 --- a/src/action/darwin/mod.rs +++ b/src/action/darwin/mod.rs @@ -8,7 +8,7 @@ mod kickstart_launchctl_service; mod unmount_volume; pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; -pub use create_apfs_volume::{CreateApfsVolume, CreateApfsVolumeError}; +pub use create_apfs_volume::{CreateApfsVolume, CreateApfsVolumeError, NIX_VOLUME_MOUNTD_DEST}; pub use create_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError}; pub use create_volume::{CreateVolume, CreateVolumeError}; pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; diff --git a/src/planner/darwin/multi.rs b/src/planner/darwin/multi.rs index 08da5eb..200943a 100644 --- a/src/planner/darwin/multi.rs +++ b/src/planner/darwin/multi.rs @@ -18,6 +18,7 @@ use crate::{ pub struct DarwinMulti { #[clap(flatten)] pub settings: CommonSettings, + /// Force encryption on the volume #[clap( long, action(ArgAction::SetTrue), @@ -25,6 +26,14 @@ pub struct DarwinMulti { env = "HARMONIC_VOLUME_ENCRYPT" )] pub volume_encrypt: bool, + /// Use a case sensitive volume + #[clap( + long, + action(ArgAction::SetTrue), + default_value = "false", + env = "HARMONIC_CASE_SENSITIVE" + )] + pub case_sensitive: bool, #[clap(long, default_value = "Nix Store", env = "HARMONIC_VOLUME_LABEL")] pub volume_label: String, #[clap(long, env = "HARMONIC_ROOT_DISK")] @@ -48,6 +57,7 @@ impl Planner for DarwinMulti { Ok(Self { settings: CommonSettings::default()?, root_disk: Some(default_root_disk().await?), + case_sensitive: false, volume_encrypt: false, volume_label: "Nix Store".into(), }) @@ -71,7 +81,14 @@ impl Planner for DarwinMulti { }, }; - let volume_label = "Nix Store".into(); + if self.volume_encrypt == false { + self.volume_encrypt = execute_command(Command::new("/usr/bin/fdesetup").arg("isactive")) + .await? + .status + .code() + .map(|v| if v == 0 { false } else { true }) + .unwrap_or(false) + } Ok(InstallPlan { planner: Box::new(self.clone()), @@ -83,9 +100,9 @@ impl Planner for DarwinMulti { Box::new( CreateApfsVolume::plan( self.root_disk.unwrap(), /* We just ensured it was populated */ - volume_label, + self.volume_label, false, - None, + self.volume_encrypt, ) .await?, ), @@ -105,6 +122,7 @@ impl Planner for DarwinMulti { settings, volume_encrypt, volume_label, + case_sensitive, root_disk, } = self; let mut map = HashMap::default(); @@ -116,6 +134,10 @@ impl Planner for DarwinMulti { ); map.insert("volume_label".into(), serde_json::to_value(volume_label)?); map.insert("root_disk".into(), serde_json::to_value(root_disk)?); + map.insert( + "case_sensitive".into(), + serde_json::to_value(case_sensitive)?, + ); Ok(map) }