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/README.md b/README.md index 9e0b6a2..fc37909 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ Planned support: * [x] Multi-user x86_64 Linux with systemd init * [ ] Multi-user aarch64 Linux with systemd init * [x] Multi-user x86_64 MacOS - + Note: Uninstall and encrypted volume support are incomplete + + Note: User deletion is currently unimplemented, you need to use a user with a secure token and `dscl . -delete /Users/_nixbuild*` where `*` is each user number. +* [x] Multi-user aarch64 MacOS + + Note: User deletion is currently unimplemented, you need to use a user with a secure token and `dscl . -delete /Users/_nixbuild*` where `*` is each user number. * [ ] Single-user x86_64 Linux with systemd init * [ ] Single-user aarch64 Linux with systemd init -* [ ] Multi-user aarch64 MacOS * [ ] Others... ## Installation Differences diff --git a/src/action/base/create_group.rs b/src/action/base/create_group.rs index 4c81ff3..3e62f96 100644 --- a/src/action/base/create_group.rs +++ b/src/action/base/create_group.rs @@ -70,17 +70,26 @@ impl Action for CreateGroup { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/sbin/dseditgroup").args([ - "-o", - "create", - "-r", - "Nix build group for nix-daemon", - "-i", - &format!("{gid}"), - name.as_str(), - ])) - .await - .map_err(|e| CreateGroupError::Command(e).boxed())?; + if Command::new("/usr/bin/dscl") + .args([".", "-read", &format!("/Groups/{name}")]) + .status() + .await? + .success() + { + () + } else { + execute_command(Command::new("/usr/sbin/dseditgroup").args([ + "-o", + "create", + "-r", + "Nix build group for nix-daemon", + "-i", + &format!("{gid}"), + name.as_str(), + ])) + .await + .map_err(|e| CreateGroupError::Command(e).boxed())?; + } }, _ => { execute_command(Command::new("groupadd").args([ @@ -141,13 +150,16 @@ impl Action for CreateGroup { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-delete", - &format!("/Groups/{name}"), - ])) - .await - .map_err(|e| CreateGroupError::Command(e).boxed())?; + // TODO(@hoverbear): Make this actually work... + // Right now, our test machines do not have a secure token and cannot delete users. + tracing::warn!("Harmonic currently cannot delete groups on Mac due to https://github.com/DeterminateSystems/harmonic/issues/33. This is a no-op, installing with harmonic again will use the existing group."); + // execute_command(Command::new("/usr/bin/dscl").args([ + // ".", + // "-delete", + // &format!("/Groups/{name}"), + // ])) + // .await + // .map_err(|e| CreateGroupError::Command(e).boxed())?; }, _ => { execute_command(Command::new("groupdel").arg(&name)) diff --git a/src/action/base/create_user.rs b/src/action/base/create_user.rs index 589b26a..cc4d068 100644 --- a/src/action/base/create_user.rs +++ b/src/action/base/create_user.rs @@ -81,81 +81,93 @@ impl Action for CreateUser { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "UniqueID", - &format!("{uid}"), - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "PrimaryGroupID", - &format!("{gid}"), - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "NFSHomeDirectory", - "/var/empty", - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "UserShell", - "/sbin/nologin", - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command( - Command::new("/usr/bin/dscl") - .args([ - ".", - "-append", - &format!("/Groups/{groupname}"), - "GroupMembership", - ]) - .arg(&name), - ) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-create", - &format!("/Users/{name}"), - "IsHidden", - "1", - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; - execute_command( - Command::new("/usr/sbin/dseditgroup") - .args(["-o", "edit"]) - .arg("-a") - .arg(&name) - .arg("-t") - .arg(&name) - .arg(groupname), - ) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; + // TODO(@hoverbear): Make this actually work... + // Right now, our test machines do not have a secure token and cannot delete users. + + if Command::new("/usr/bin/dscl") + .args([".", "-read", &format!("/Users/{name}")]) + .status() + .await? + .success() + { + () + } else { + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + "UniqueID", + &format!("{uid}"), + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + "PrimaryGroupID", + &format!("{gid}"), + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + "NFSHomeDirectory", + "/var/empty", + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + "UserShell", + "/sbin/nologin", + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command( + Command::new("/usr/bin/dscl") + .args([ + ".", + "-append", + &format!("/Groups/{groupname}"), + "GroupMembership", + ]) + .arg(&name), + ) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command(Command::new("/usr/bin/dscl").args([ + ".", + "-create", + &format!("/Users/{name}"), + "IsHidden", + "1", + ])) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + execute_command( + Command::new("/usr/sbin/dseditgroup") + .args(["-o", "edit"]) + .arg("-a") + .arg(&name) + .arg("-t") + .arg(&name) + .arg(groupname), + ) + .await + .map_err(|e| CreateUserError::Command(e).boxed())?; + } }, _ => { execute_command(Command::new("useradd").args([ @@ -235,13 +247,16 @@ impl Action for CreateUser { patch: _, } | OperatingSystem::Darwin => { - execute_command(Command::new("/usr/bin/dscl").args([ - ".", - "-delete", - &format!("/Users/{name}"), - ])) - .await - .map_err(|e| CreateUserError::Command(e).boxed())?; + // TODO(@hoverbear): Make this actually work... + // Right now, our test machines do not have a secure token and cannot delete users. + tracing::warn!("Harmonic currently cannot delete groups on Mac due to https://github.com/DeterminateSystems/harmonic/issues/33. This is a no-op, installing with harmonic again will use the existing user."); + // execute_command(Command::new("/usr/bin/dscl").args([ + // ".", + // "-delete", + // &format!("/Users/{name}"), + // ])) + // .await + // .map_err(|e| CreateUserError::Command(e).boxed())?; }, _ => { execute_command(Command::new("userdel").args([&name.to_string()])) diff --git a/src/action/darwin/create_apfs_volume.rs b/src/action/darwin/create_apfs_volume.rs index 6e886ab..f92f174 100644 --- a/src/action/darwin/create_apfs_volume.rs +++ b/src/action/darwin/create_apfs_volume.rs @@ -17,14 +17,14 @@ use std::{ }; use tokio::process::Command; -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, @@ -43,7 +43,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( @@ -72,31 +72,25 @@ 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() { - vec![ - "/bin/sh", - "-c", - "/usr/bin/security find-generic-password", - "-s", - "{name}", - "-w", - "|", - "/usr/sbin/diskutil", - "apfs", - "unlockVolume", - &name, - "-mountpoint", - "/nix", - "-stdinpassphrase", - ] + let name_with_qoutes = format!("\"{name}\""); + let encrypted_command; + let mount_command = if encrypt { + encrypted_command = format!("/usr/bin/security find-generic-password -s {name_with_qoutes} -w | /usr/sbin/diskutil apfs unlockVolume {name_with_qoutes} -mountpoint /nix -stdinpassphrase"); + vec!["/bin/sh", "-c", encrypted_command.as_str()] } else { - vec!["/usr/sbin/diskutil", "mount", "-mountPoint", "/nix", &name] + vec![ + "/usr/sbin/diskutil", + "mount", + "-mountPoint", + "/nix", + name.as_str(), + ] }; // TODO(@hoverbear): Use plist lib we have in tree... let mount_plist = format!( diff --git a/src/action/darwin/enable_ownership.rs b/src/action/darwin/enable_ownership.rs index a1a456b..06fa7dd 100644 --- a/src/action/darwin/enable_ownership.rs +++ b/src/action/darwin/enable_ownership.rs @@ -60,8 +60,7 @@ impl Action for EnableOwnership { .args(["info", "-plist"]) .arg(&path), ) - .await - .unwrap() + .await? .stdout; let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf)).unwrap(); diff --git a/src/action/darwin/encrypt_volume.rs b/src/action/darwin/encrypt_volume.rs index d982177..f411b65 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,75 @@ 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 */ + + execute_command(Command::new("/usr/sbin/diskutil").arg("mount").arg(&name)).await?; + + // Add the password to the user keychain so they can unlock it later. + execute_command( + Command::new("/usr/bin/security").args([ + "add-generic-password", + "-a", + name.as_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", + name.as_str(), + "-user", + "disk", + "-passphrase", + password.as_str(), + ])) + .await?; + + execute_command( + Command::new("/usr/sbin/diskutil") + .arg("unmount") + .arg("force") + .arg(&name), + ) + .await?; tracing::trace!("Encrypted volume"); *action_state = ActionState::Completed; @@ -72,17 +145,40 @@ impl Action for EncryptVolume { ))] async fn revert(&mut self) -> Result<(), Box> { let Self { - disk: _, - password: _, + disk, + name, action_state, } = self; if *action_state == ActionState::Uncompleted { - tracing::trace!("Already reverted: Unencrypted volume (noop)"); + tracing::trace!("Already reverted: Unencrypted volume"); return Ok(()); } - tracing::debug!("Unencrypted volume (noop)"); + tracing::debug!("Unencrypted volume"); - tracing::trace!("Unencrypted volume (noop)"); + let disk_str = disk.to_str().expect("Could not turn disk into string"); /* Should not reasonably ever fail */ + + // TODO: This seems very rough and unsafe + execute_command( + Command::new("/usr/bin/security").args([ + "delete-generic-password", + "-a", + name.as_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(), + ]), + ) + .await?; + + tracing::trace!("Unencrypted volume"); *action_state = ActionState::Completed; Ok(()) } 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 e677b9c..14f7c88 100644 --- a/src/planner/darwin/multi.rs +++ b/src/planner/darwin/multi.rs @@ -18,13 +18,22 @@ use crate::{ pub struct DarwinMulti { #[clap(flatten)] pub settings: CommonSettings, + /// Force encryption on the volume + #[clap( + long, + action(ArgAction::Set), + default_value = "false", + env = "HARMONIC_ENCRYPT" + )] + pub encrypt: Option, + /// Use a case sensitive volume #[clap( long, action(ArgAction::SetTrue), default_value = "false", - env = "HARMONIC_VOLUME_ENCRYPT" + env = "HARMONIC_CASE_SENSITIVE" )] - pub volume_encrypt: bool, + 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,7 +57,8 @@ impl Planner for DarwinMulti { Ok(Self { settings: CommonSettings::default()?, root_disk: Some(default_root_disk().await?), - volume_encrypt: false, + case_sensitive: false, + encrypt: None, volume_label: "Nix Store".into(), }) } @@ -71,7 +81,17 @@ impl Planner for DarwinMulti { }, }; - let volume_label = "Nix Store".into(); + let encrypt = if self.encrypt == None { + Command::new("/usr/bin/fdesetup") + .arg("isactive") + .status() + .await? + .code() + .map(|v| if v == 0 { false } else { true }) + .unwrap_or(false) + } else { + false + }; Ok(InstallPlan { planner: Box::new(self.clone()), @@ -83,9 +103,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, + encrypt, ) .await?, ), @@ -103,19 +123,21 @@ impl Planner for DarwinMulti { ) -> Result, Box> { let Self { settings, - volume_encrypt, + encrypt, volume_label, + case_sensitive, root_disk, } = self; let mut map = HashMap::default(); map.extend(settings.describe()?.into_iter()); - map.insert( - "volume_encrypt".into(), - serde_json::to_value(volume_encrypt)?, - ); + map.insert("volume_encrypt".into(), serde_json::to_value(encrypt)?); 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) }