Merge pull request #31 from DeterminateSystems/nix-mac-encrypted-volume

Nix Mac encrypted volume support
This commit is contained in:
Ana Hobden 2022-11-10 09:08:07 -08:00 committed by GitHub
commit 673464ede7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 330 additions and 153 deletions

39
Cargo.lock generated
View file

@ -786,6 +786,7 @@ dependencies = [
"nix", "nix",
"owo-colors", "owo-colors",
"plist", "plist",
"rand 0.8.5",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@ -1333,6 +1334,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -1394,6 +1401,27 @@ dependencies = [
"winapi", "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]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.3.1" version = "0.3.1"
@ -1409,6 +1437,15 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 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]] [[package]]
name = "rdrand" name = "rdrand"
version = "0.4.0" version = "0.4.0"
@ -1808,7 +1845,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [ dependencies = [
"rand", "rand 0.4.6",
"remove_dir_all", "remove_dir_all",
] ]

View file

@ -48,3 +48,4 @@ dirs = "4.0.0"
erased-serde = "0.3.23" erased-serde = "0.3.23"
typetag = "0.2.3" typetag = "0.2.3"
dyn-clone = "1.0.9" dyn-clone = "1.0.9"
rand = "0.8.5"

View file

@ -13,10 +13,11 @@ Planned support:
* [x] Multi-user x86_64 Linux with systemd init * [x] Multi-user x86_64 Linux with systemd init
* [ ] Multi-user aarch64 Linux with systemd init * [ ] Multi-user aarch64 Linux with systemd init
* [x] Multi-user x86_64 MacOS * [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 x86_64 Linux with systemd init
* [ ] Single-user aarch64 Linux with systemd init * [ ] Single-user aarch64 Linux with systemd init
* [ ] Multi-user aarch64 MacOS
* [ ] Others... * [ ] Others...
## Installation Differences ## Installation Differences

View file

@ -70,6 +70,14 @@ impl Action for CreateGroup {
patch: _, patch: _,
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
if Command::new("/usr/bin/dscl")
.args([".", "-read", &format!("/Groups/{name}")])
.status()
.await?
.success()
{
()
} else {
execute_command(Command::new("/usr/sbin/dseditgroup").args([ execute_command(Command::new("/usr/sbin/dseditgroup").args([
"-o", "-o",
"create", "create",
@ -81,6 +89,7 @@ impl Action for CreateGroup {
])) ]))
.await .await
.map_err(|e| CreateGroupError::Command(e).boxed())?; .map_err(|e| CreateGroupError::Command(e).boxed())?;
}
}, },
_ => { _ => {
execute_command(Command::new("groupadd").args([ execute_command(Command::new("groupadd").args([
@ -141,13 +150,16 @@ impl Action for CreateGroup {
patch: _, patch: _,
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
execute_command(Command::new("/usr/bin/dscl").args([ // TODO(@hoverbear): Make this actually work...
".", // Right now, our test machines do not have a secure token and cannot delete users.
"-delete", 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.");
&format!("/Groups/{name}"), // execute_command(Command::new("/usr/bin/dscl").args([
])) // ".",
.await // "-delete",
.map_err(|e| CreateGroupError::Command(e).boxed())?; // &format!("/Groups/{name}"),
// ]))
// .await
// .map_err(|e| CreateGroupError::Command(e).boxed())?;
}, },
_ => { _ => {
execute_command(Command::new("groupdel").arg(&name)) execute_command(Command::new("groupdel").arg(&name))

View file

@ -81,6 +81,17 @@ impl Action for CreateUser {
patch: _, patch: _,
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
// 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([ execute_command(Command::new("/usr/bin/dscl").args([
".", ".",
"-create", "-create",
@ -156,6 +167,7 @@ impl Action for CreateUser {
) )
.await .await
.map_err(|e| CreateUserError::Command(e).boxed())?; .map_err(|e| CreateUserError::Command(e).boxed())?;
}
}, },
_ => { _ => {
execute_command(Command::new("useradd").args([ execute_command(Command::new("useradd").args([
@ -235,13 +247,16 @@ impl Action for CreateUser {
patch: _, patch: _,
} }
| OperatingSystem::Darwin => { | OperatingSystem::Darwin => {
execute_command(Command::new("/usr/bin/dscl").args([ // TODO(@hoverbear): Make this actually work...
".", // Right now, our test machines do not have a secure token and cannot delete users.
"-delete", 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.");
&format!("/Users/{name}"), // execute_command(Command::new("/usr/bin/dscl").args([
])) // ".",
.await // "-delete",
.map_err(|e| CreateUserError::Command(e).boxed())?; // &format!("/Users/{name}"),
// ]))
// .await
// .map_err(|e| CreateUserError::Command(e).boxed())?;
}, },
_ => { _ => {
execute_command(Command::new("userdel").args([&name.to_string()])) execute_command(Command::new("userdel").args([&name.to_string()]))

View file

@ -17,14 +17,14 @@ use std::{
}; };
use tokio::process::Command; 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)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct CreateApfsVolume { pub struct CreateApfsVolume {
disk: PathBuf, disk: PathBuf,
name: String, name: String,
case_sensitive: bool, case_sensitive: bool,
encrypt: Option<String>, encrypt: bool,
create_or_append_synthetic_conf: CreateOrAppendFile, create_or_append_synthetic_conf: CreateOrAppendFile,
create_synthetic_objects: CreateSyntheticObjects, create_synthetic_objects: CreateSyntheticObjects,
unmount_volume: UnmountVolume, unmount_volume: UnmountVolume,
@ -43,7 +43,7 @@ impl CreateApfsVolume {
disk: impl AsRef<Path>, disk: impl AsRef<Path>,
name: String, name: String,
case_sensitive: bool, case_sensitive: bool,
encrypt: Option<String>, encrypt: bool,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let disk = disk.as_ref(); let disk = disk.as_ref();
let create_or_append_synthetic_conf = CreateOrAppendFile::plan( let create_or_append_synthetic_conf = CreateOrAppendFile::plan(
@ -72,31 +72,25 @@ impl CreateApfsVolume {
.await .await
.map_err(|e| e.boxed())?; .map_err(|e| e.boxed())?;
let encrypt_volume = if let Some(password) = encrypt.as_ref() { let encrypt_volume = if encrypt {
Some(EncryptVolume::plan(disk, password.to_string()).await?) Some(EncryptVolume::plan(disk, &name).await?)
} else { } else {
None None
}; };
let mount_command = if encrypt.is_some() { let name_with_qoutes = format!("\"{name}\"");
vec![ let encrypted_command;
"/bin/sh", let mount_command = if encrypt {
"-c", 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");
"/usr/bin/security find-generic-password", vec!["/bin/sh", "-c", encrypted_command.as_str()]
"-s",
"{name}",
"-w",
"|",
"/usr/sbin/diskutil",
"apfs",
"unlockVolume",
&name,
"-mountpoint",
"/nix",
"-stdinpassphrase",
]
} else { } 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... // TODO(@hoverbear): Use plist lib we have in tree...
let mount_plist = format!( let mount_plist = format!(

View file

@ -60,8 +60,7 @@ impl Action for EnableOwnership {
.args(["info", "-plist"]) .args(["info", "-plist"])
.arg(&path), .arg(&path),
) )
.await .await?
.unwrap()
.stdout; .stdout;
let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf)).unwrap(); let the_plist: DiskUtilOutput = plist::from_reader(Cursor::new(buf)).unwrap();

View file

@ -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 std::path::{Path, PathBuf};
use tokio::process::Command;
use crate::action::{Action, ActionDescription, ActionState};
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct EncryptVolume { pub struct EncryptVolume {
disk: PathBuf, disk: PathBuf,
password: String, name: String,
action_state: ActionState, action_state: ActionState,
} }
@ -13,11 +17,12 @@ impl EncryptVolume {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn plan( pub async fn plan(
disk: impl AsRef<Path>, disk: impl AsRef<Path>,
password: String, name: impl AsRef<str>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let name = name.as_ref().to_owned();
Ok(Self { Ok(Self {
name,
disk: disk.as_ref().to_path_buf(), disk: disk.as_ref().to_path_buf(),
password,
action_state: ActionState::Uncompleted, action_state: ActionState::Uncompleted,
}) })
} }
@ -42,8 +47,8 @@ impl Action for EncryptVolume {
))] ))]
async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn execute(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
disk: _, disk,
password: _, name,
action_state, action_state,
} = self; } = self;
if *action_state == ActionState::Completed { if *action_state == ActionState::Completed {
@ -52,7 +57,75 @@ impl Action for EncryptVolume {
} }
tracing::debug!("Encrypting volume"); 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"); tracing::trace!("Encrypted volume");
*action_state = ActionState::Completed; *action_state = ActionState::Completed;
@ -72,17 +145,40 @@ impl Action for EncryptVolume {
))] ))]
async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { async fn revert(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Self { let Self {
disk: _, disk,
password: _, name,
action_state, action_state,
} = self; } = self;
if *action_state == ActionState::Uncompleted { if *action_state == ActionState::Uncompleted {
tracing::trace!("Already reverted: Unencrypted volume (noop)"); tracing::trace!("Already reverted: Unencrypted volume");
return Ok(()); 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; *action_state = ActionState::Completed;
Ok(()) Ok(())
} }

View file

@ -8,7 +8,7 @@ mod kickstart_launchctl_service;
mod unmount_volume; mod unmount_volume;
pub use bootstrap_volume::{BootstrapVolume, BootstrapVolumeError}; 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_synthetic_objects::{CreateSyntheticObjects, CreateSyntheticObjectsError};
pub use create_volume::{CreateVolume, CreateVolumeError}; pub use create_volume::{CreateVolume, CreateVolumeError};
pub use enable_ownership::{EnableOwnership, EnableOwnershipError}; pub use enable_ownership::{EnableOwnership, EnableOwnershipError};

View file

@ -18,13 +18,22 @@ use crate::{
pub struct DarwinMulti { pub struct DarwinMulti {
#[clap(flatten)] #[clap(flatten)]
pub settings: CommonSettings, pub settings: CommonSettings,
/// Force encryption on the volume
#[clap(
long,
action(ArgAction::Set),
default_value = "false",
env = "HARMONIC_ENCRYPT"
)]
pub encrypt: Option<bool>,
/// Use a case sensitive volume
#[clap( #[clap(
long, long,
action(ArgAction::SetTrue), action(ArgAction::SetTrue),
default_value = "false", 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")] #[clap(long, default_value = "Nix Store", env = "HARMONIC_VOLUME_LABEL")]
pub volume_label: String, pub volume_label: String,
#[clap(long, env = "HARMONIC_ROOT_DISK")] #[clap(long, env = "HARMONIC_ROOT_DISK")]
@ -48,7 +57,8 @@ impl Planner for DarwinMulti {
Ok(Self { Ok(Self {
settings: CommonSettings::default()?, settings: CommonSettings::default()?,
root_disk: Some(default_root_disk().await?), root_disk: Some(default_root_disk().await?),
volume_encrypt: false, case_sensitive: false,
encrypt: None,
volume_label: "Nix Store".into(), 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 { Ok(InstallPlan {
planner: Box::new(self.clone()), planner: Box::new(self.clone()),
@ -83,9 +103,9 @@ impl Planner for DarwinMulti {
Box::new( Box::new(
CreateApfsVolume::plan( CreateApfsVolume::plan(
self.root_disk.unwrap(), /* We just ensured it was populated */ self.root_disk.unwrap(), /* We just ensured it was populated */
volume_label, self.volume_label,
false, false,
None, encrypt,
) )
.await?, .await?,
), ),
@ -103,19 +123,21 @@ impl Planner for DarwinMulti {
) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<HashMap<String, serde_json::Value>, Box<dyn std::error::Error + Sync + Send>> {
let Self { let Self {
settings, settings,
volume_encrypt, encrypt,
volume_label, volume_label,
case_sensitive,
root_disk, root_disk,
} = self; } = self;
let mut map = HashMap::default(); let mut map = HashMap::default();
map.extend(settings.describe()?.into_iter()); map.extend(settings.describe()?.into_iter());
map.insert( map.insert("volume_encrypt".into(), serde_json::to_value(encrypt)?);
"volume_encrypt".into(),
serde_json::to_value(volume_encrypt)?,
);
map.insert("volume_label".into(), serde_json::to_value(volume_label)?); map.insert("volume_label".into(), serde_json::to_value(volume_label)?);
map.insert("root_disk".into(), serde_json::to_value(root_disk)?); map.insert("root_disk".into(), serde_json::to_value(root_disk)?);
map.insert(
"case_sensitive".into(),
serde_json::to_value(case_sensitive)?,
);
Ok(map) Ok(map)
} }