diff --git a/.drone.yml b/.drone.yml index 0536eed..79f57a1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,13 +8,13 @@ steps: - rustup component add rustfmt - cargo fmt --all -- --check - name: test - image: shimun/fido2luks@sha256:c1c9b0fb36f24555bc575dd6f643efd3a83f9db25a7ed88af3e22eacf44a89b8 + image: shimun/fido2luks@sha256:6d0b4017bffbec5fac8f25d383d68671fcc9930efb02e97ce5ea81acf0060ece environment: DEBIAN_FRONTEND: noninteractive commands: - cargo test --locked - name: publish - image: shimun/fido2luks@sha256:c1c9b0fb36f24555bc575dd6f643efd3a83f9db25a7ed88af3e22eacf44a89b8 + image: shimun/fido2luks@sha256:6d0b4017bffbec5fac8f25d383d68671fcc9930efb02e97ce5ea81acf0060ece environment: DEBIAN_FRONTEND: noninteractive CARGO_REGISTRY_TOKEN: diff --git a/src/cli.rs b/src/cli.rs index 06522fd..451f9d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,17 +20,13 @@ use std::fs::File; use std::time::SystemTime; pub use cli_args::Args; +use failure::ResultExt; +use std::collections::hash_map::RandomState; use std::iter::FromIterator; use std::path::PathBuf; -fn read_pin(ap: &AuthenticatorParameters) -> Fido2LuksResult { - if let Some(src) = ap.pin_source.as_ref() { - let mut pin = String::new(); - File::open(src)?.read_to_string(&mut pin)?; - Ok(pin.trim_end_matches("\n").to_string()) //remove trailing newline - } else { - util::read_password("Authenticator PIN", false) - } +fn read_pin() -> Fido2LuksResult { + util::read_password("Authenticator PIN", false) } fn derive_secret( @@ -74,10 +70,15 @@ pub fn extend_creds_device( creds: &[HexEncoded], luks_dev: &mut LuksDevice, ) -> Fido2LuksResult> { - let mut additional = HashSet::from_iter(creds.iter().cloned()); + let mut additional = HashSet::new(); + additional.extend(creds.iter().cloned()); for token in luks_dev.tokens()? { for cred in token?.1.credential { - let parsed = HexEncoded::from_str(cred.as_str())?; + let parsed = HexEncoded::from_str(cred.as_str()).map_err(|_e| { + Fido2LuksError::HexEncodingError { + string: cred.clone(), + } + })?; additional.insert(parsed); } } @@ -107,7 +108,6 @@ pub fn parse_cmdline() -> Args { pub fn run_cli() -> Fido2LuksResult<()> { let mut stdout = io::stdout(); let args = parse_cmdline(); - let interactive = args.interactive; match &args.command { Command::Credential { authenticator, @@ -115,7 +115,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { } => { let pin_string; let pin = if authenticator.pin { - pin_string = read_pin(authenticator)?; + pin_string = read_pin()?; Some(pin_string.as_ref()) } else { None @@ -131,13 +131,6 @@ pub fn run_cli() -> Fido2LuksResult<()> { secret, device, } => { - let pin_string; - let pin = if authenticator.pin { - pin_string = read_pin(authenticator)?; - Some(pin_string.as_ref()) - } else { - None - }; let (pin, salt) = match ( &secret.password_helper, authenticator.pin, @@ -147,15 +140,12 @@ pub fn run_cli() -> Fido2LuksResult<()> { (Some(phelper), true, true, false) => { read_password_pin_prefixed(|| phelper.obtain())? } - (Some(phelper), false, false, false) => (None, secret.salt.obtain_sha256(&phelper)), + (Some(phelper), false, false, false) => { + (None, secret.salt.obtain_sha256(&phelper)?) + } - (phelper, pin, _, true) => ( - if pin { - pin_string = read_pin(authenticator)?; - Some(pin_string.as_ref()) - } else { - None - }, + (phelper, pin, _, _) => ( + if pin { Some(read_pin()?) } else { None }, match phelper { None | Some(PasswordHelper::Stdin) => { util::read_password_hashed("Password", false) @@ -165,9 +155,17 @@ pub fn run_cli() -> Fido2LuksResult<()> { ), }; let credentials = if let Some(dev) = device { - extend_creds_device(credentials.ids.0.as_slice(), &mut LuksDevice::load(dev)?)? + extend_creds_device( + credentials + .ids + .clone() + .map(|cs| cs.0) + .unwrap_or_default() + .as_slice(), + &mut LuksDevice::load(dev)?, + )? } else { - credentials.ids.0 + credentials.ids.clone().map(|cs| cs.0).unwrap_or_default() }; let (secret, _cred) = derive_secret( credentials.as_slice(), @@ -189,7 +187,6 @@ pub fn run_cli() -> Fido2LuksResult<()> { secret, luks_mod, existing_secret: other_secret, - auto_credential, .. } | Command::ReplaceKey { @@ -199,21 +196,54 @@ pub fn run_cli() -> Fido2LuksResult<()> { secret, luks_mod, replacement: other_secret, - remove_cred, .. } => { - let pin = if authenticator.pin { - Some(read_pin(authenticator)?) + let mut luks_dev = LuksDevice::load(&luks.device)?; + + let luks2 = luks_dev.is_luks2()?; + + let credentials = if !luks.disable_token && luks2 { + extend_creds_device( + credentials + .ids + .clone() + .map(|cs| cs.0) + .unwrap_or_default() + .as_slice(), + &mut luks_dev, + )? } else { - None + credentials.ids.clone().map(|cs| cs.0).unwrap_or_default() }; - let salt = |q: &str, verify: bool| -> Fido2LuksResult<[u8; 32]> { - if interactive || secret.password_helper == PasswordHelper::Stdin { - util::read_password_hashed(q, verify) - } else { - secret.salt.obtain_sha256(&secret.password_helper) - } + + let inputs = |q: &str, verify: bool| -> Fido2LuksResult<(Option, [u8; 32])> { + Ok( + match ( + &secret.password_helper, + authenticator.pin, + authenticator.pin_prefixed, + args.interactive, + ) { + (Some(phelper), true, true, false) => { + read_password_pin_prefixed(|| phelper.obtain())? + } + (Some(phelper), false, false, false) => { + (None, secret.salt.obtain_sha256(&phelper)?) + } + + (phelper, pin, _, _) => ( + if pin { Some(read_pin()?) } else { None }, + match &secret.password_helper { + None | Some(PasswordHelper::Stdin) => { + util::read_password_hashed(q, verify) + } + Some(phelper) => secret.salt.obtain_sha256(&phelper), + }?, + ), + }, + ) }; + let other_secret = |salt_q: &str, verify: bool| -> Fido2LuksResult<(Vec, Option)> { @@ -224,38 +254,53 @@ pub fn run_cli() -> Fido2LuksResult<()> { } => Ok((util::read_keyfile(file)?, None)), OtherSecret { fido_device: true, .. - } => Ok(derive_secret( - &credentials.ids.0, - &salt(salt_q, verify)?, - authenticator.await_time, - pin.as_deref(), - ) - .map(|(secret, cred)| (secret[..].to_vec(), Some(cred)))?), + } => { + let (pin, salt) = inputs(salt_q, verify)?; + Ok(derive_secret( + &credentials, + &salt, + authenticator.await_time, + pin.as_deref(), + ) + .map(|(secret, cred)| (secret[..].to_vec(), Some(cred)))?) + } _ => Ok(( util::read_password(salt_q, verify)?.as_bytes().to_vec(), None, )), } }; - let secret = |verify: bool| -> Fido2LuksResult<([u8; 32], FidoCredential)> { - derive_secret( - &credentials.ids.0, - &salt("Password", verify)?, - authenticator.await_time, - pin.as_deref(), - ) + let secret = |verify: bool, + credentials: &[HexEncoded]| + -> Fido2LuksResult<([u8; 32], FidoCredential)> { + let (pin, salt) = inputs("Password", verify)?; + derive_secret(credentials, &salt, authenticator.await_time, pin.as_deref()) }; - let mut luks_dev = LuksDevice::load(&luks.device)?; // Non overlap match &args.command { - Command::AddKey { exclusive, .. } => { + Command::AddKey { + exclusive, + auto_credential, + .. + } => { let (existing_secret, _) = other_secret("Current password", false)?; - let (new_secret, cred) = secret(true)?; + let (new_secret, cred) = if *auto_credential && luks2 { + let cred = make_credential_id( + Some(luks.device.display().to_string().as_str()), + None, + )?; //TODO: do ask for PIN + let creds = vec![HexEncoded(cred.id)]; + secret(true, &creds) + } else { + secret(true, &credentials) + }?; let added_slot = luks_dev.add_key( &new_secret, &existing_secret[..], luks_mod.kdf_time.or(Some(10)), - Some(&cred.id[..]).filter(|_| *token), + Some(&cred.id[..]) + .filter(|_| !luks.disable_token || *auto_credential) + .filter(|_| luks2), )?; if *exclusive { let destroyed = luks_dev.remove_keyslots(&[added_slot])?; @@ -274,23 +319,37 @@ pub fn run_cli() -> Fido2LuksResult<()> { } Ok(()) } - Command::ReplaceKey { add_password, .. } => { - let (existing_secret, _) = secret(false)?; + Command::ReplaceKey { + add_password, + remove_cred, + .. + } => { + let (existing_secret, _prev_cred) = secret(false, &credentials)?; let (replacement_secret, cred) = other_secret("Replacement password", true)?; let slot = if *add_password { luks_dev.add_key( &replacement_secret[..], &existing_secret, luks_mod.kdf_time, - cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]), + cred.as_ref() + .filter(|_| !luks.disable_token) + .filter(|_| luks2) + .map(|cred| &cred.id[..]), ) } else { - luks_dev.replace_key( + let slot = luks_dev.replace_key( &replacement_secret[..], &existing_secret, luks_mod.kdf_time, - cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]), - ) + cred.as_ref() + .filter(|_| !luks.disable_token) + .filter(|_| luks2) + .map(|cred| &cred.id[..]), + )?; + if *remove_cred && cred.is_none() { + luks_dev.remove_token_slot(slot)?; + } + Ok(slot) }?; println!( "Added to password to device {}, slot: {}", @@ -307,48 +366,56 @@ pub fn run_cli() -> Fido2LuksResult<()> { authenticator, secret, name, - retries, - .. - } - | Command::OpenToken { - luks, - authenticator, - secret, - name, + credentials, retries, } => { - let pin_string; - let pin = if authenticator.pin { - pin_string = read_pin(authenticator)?; - Some(pin_string.as_ref()) - } else { - None - }; - let salt = |q: &str, verify: bool| -> Fido2LuksResult<[u8; 32]> { - if interactive || secret.password_helper == PasswordHelper::Stdin { - util::read_password_hashed(q, verify) - } else { - secret.salt.obtain_sha256(&secret.password_helper) - } + let inputs = |q: &str, verify: bool| -> Fido2LuksResult<(Option, [u8; 32])> { + Ok( + match ( + &secret.password_helper, + authenticator.pin, + authenticator.pin_prefixed, + args.interactive, + ) { + (Some(phelper), true, true, false) => { + read_password_pin_prefixed(|| phelper.obtain())? + } + (Some(phelper), false, false, false) => { + (None, secret.salt.obtain_sha256(&phelper)?) + } + + (phelper, pin, _, _) => ( + if pin { Some(read_pin()?) } else { None }, + match &phelper { + None | Some(PasswordHelper::Stdin) => { + util::read_password_hashed(q, verify) + } + Some(phelper) => secret.salt.obtain_sha256(&phelper), + }?, + ), + }, + ) }; // Cow shouldn't be necessary let secret = |credentials: Cow<'_, Vec>| { + let (pin, salt) = inputs("Password", false)?; derive_secret( credentials.as_ref(), - &salt("Password", false)?, + &salt, authenticator.await_time, - pin, + pin.as_deref(), ) }; let mut retries = *retries; let mut luks_dev = LuksDevice::load(&luks.device)?; loop { - let secret = match &args.command { - Command::Open { credentials, .. } => secret(Cow::Borrowed(&credentials.ids.0)) - .and_then(|(secret, _cred)| luks_dev.activate(&name, &secret, luks.slot)), - Command::OpenToken { .. } => luks_dev.activate_token( + let slot = if let Some(ref credentials) = credentials.ids { + secret(Cow::Borrowed(&credentials.0)) + .and_then(|(secret, _cred)| luks_dev.activate(&name, &secret, luks.slot)) + } else if luks_dev.is_luks2()? { + luks_dev.activate_token( &name, Box::new(|credentials: Vec| { let creds = credentials @@ -359,10 +426,11 @@ pub fn run_cli() -> Fido2LuksResult<()> { .map(|(secret, cred)| (secret, hex::encode(&cred.id))) }), luks.slot, - ), - _ => unreachable!(), + ) + } else { + return Err(Fido2LuksError::WrongSecret); // creds or luks2 }; - match secret { + match slot { Err(e) => { match e { Fido2LuksError::WrongSecret if retries > 0 => {} @@ -444,7 +512,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { } } let count = if tokens.is_empty() { - dev.add_token(&Fido2LuksToken::with_credentials(&credentials.ids.0, *slot))?; + dev.add_token(&Fido2LuksToken::with_credentials(&credentials.0, *slot))?; 1 } else { tokens.len() @@ -452,7 +520,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { for (id, mut token) in tokens { token .credential - .extend(credentials.ids.0.iter().map(|h| h.to_string())); + .extend(credentials.0.iter().map(|h| h.to_string())); dev.update_token(id, &token)?; } println!("Updated {} tokens", count); @@ -480,7 +548,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { token.credential = token .credential .into_iter() - .filter(|cred| !credentials.ids.0.iter().any(|h| &h.to_string() == cred)) + .filter(|cred| !credentials.0.iter().any(|h| &h.to_string() == cred)) .collect(); dev.update_token(id, &token)?; } @@ -512,11 +580,8 @@ pub fn run_cli() -> Fido2LuksResult<()> { Command::GenerateCompletions { shell, out_dir } => { Args::clap().gen_completions( env!("CARGO_PKG_NAME"), - match shell.as_ref() { - "bash" => Shell::Bash, - "fish" => Shell::Fish, - _ => unreachable!("structopt shouldn't allow us to reach this point"), - }, + Shell::from_str(shell.as_str()) + .expect("structopt shouldn't allow us to reach this point"), &out_dir, ); Ok(()) diff --git a/src/cli_args/mod.rs b/src/cli_args/mod.rs index fffadce..435e6ca 100644 --- a/src/cli_args/mod.rs +++ b/src/cli_args/mod.rs @@ -1,4 +1,5 @@ use std::fmt::{Display, Error, Formatter}; +use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::str::FromStr; use structopt::clap::AppSettings; @@ -31,6 +32,12 @@ impl FromStr for HexEncoded { } } +impl Hash for HexEncoded { + fn hash(&self, state: &mut H) { + self.0.hash(state) + } +} + #[derive(Debug, Eq, PartialEq, Clone)] pub struct CommaSeparated(pub Vec); @@ -128,8 +135,7 @@ pub struct SecretParameters { #[structopt( name = "password-helper", env = "FIDO2LUKS_PASSWORD_HELPER", - long = "password-helper", - default_value = "/usr/bin/env systemd-ask-password 'Please enter second factor for LUKS disk encryption!'" + long = "password-helper" )] pub password_helper: Option, } @@ -269,8 +275,14 @@ pub enum TokenCommand { Add { #[structopt(env = "FIDO2LUKS_DEVICE")] device: PathBuf, - #[structopt(flatten)] - credentials: Credentials, + /// FIDO credential ids, separated by ',' generate using fido2luks credential + #[structopt( + name = "credential-ids", + env = "FIDO2LUKS_CREDENTIAL_ID", + short = "c", + long = "creds" + )] + credentials: CommaSeparated, /// Slot to which the credentials will be added #[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")] slot: u32, @@ -279,8 +291,14 @@ pub enum TokenCommand { Remove { #[structopt(env = "FIDO2LUKS_DEVICE")] device: PathBuf, - #[structopt(flatten)] - credentials: Credentials, + /// FIDO credential ids, separated by ',' generate using fido2luks credential + #[structopt( + name = "credential-ids", + env = "FIDO2LUKS_CREDENTIAL_ID", + short = "c", + long = "creds" + )] + credentials: CommaSeparated, /// Token from which the credentials will be removed #[structopt(long = "token")] token_id: Option, diff --git a/src/error.rs b/src/error.rs index 03d9741..f0f1f76 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,6 +29,8 @@ pub enum Fido2LuksError { WrongSecret, #[fail(display = "not an utf8 string")] StringEncodingError { cause: FromUtf8Error }, + #[fail(display = "not an hex string: {}", string)] + HexEncodingError { string: String }, } impl Fido2LuksError { diff --git a/src/luks.rs b/src/luks.rs index 6996eef..4a91e73 100644 --- a/src/luks.rs +++ b/src/luks.rs @@ -103,6 +103,10 @@ impl LuksDevice { Ok(()) } + pub fn add_credential(&mut self, slot: u32, credential: Vec) -> Fido2LuksResult<()> { + self.add_token(&Fido2LuksToken::with_credentials(&[credential], slot)) + } + pub fn remove_token(&mut self, token: u32) -> Fido2LuksResult<()> { self.require_luks2()?; self.device @@ -111,6 +115,20 @@ impl LuksDevice { Ok(()) } + pub fn remove_token_slot(&mut self, slot: u32) -> Fido2LuksResult<()> { + let mut remove = HashSet::new(); + for token in self.tokens()? { + let (id, token) = token?; + if token.keyslots.contains(&slot.to_string()) { + remove.insert(id); + } + } + for rm in remove { + self.remove_token(rm)?; + } + Ok(()) + } + pub fn update_token(&mut self, token: u32, data: &Fido2LuksToken) -> Fido2LuksResult<()> { self.require_luks2()?; self.device @@ -192,7 +210,9 @@ impl LuksDevice { old_secret, CryptActivateFlags::empty(), )?; - self.device.keyslot_handle().change_by_passphrase( + + // slot should stay the same but better be safe than sorry + let slot = self.device.keyslot_handle().change_by_passphrase( Some(slot), Some(slot), old_secret,