diff --git a/Cargo.lock b/Cargo.lock index accb097..cc44930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,9 @@ dependencies = [ "libcryptsetup-rs", "ring", "rpassword", + "serde", + "serde_derive", + "serde_json", "structopt", ] @@ -891,6 +894,17 @@ version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" +[[package]] +name = "serde_derive" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "serde_json" version = "1.0.51" diff --git a/Cargo.toml b/Cargo.toml index 4e4bda1..4d308c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ failure = "0.1.5" rpassword = "4.0.1" structopt = "0.3.2" libcryptsetup-rs = "0.2.0" +serde_json = "1.0.51" +serde_derive = "1.0.106" +serde = "1.0.106" [profile.release] lto = true diff --git a/src/cli.rs b/src/cli.rs index 3f3e39b..200e1de 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -120,6 +120,14 @@ impl SecretGeneration { } pub fn obtain_secret(&self, password_query: &str) -> Fido2LuksResult<[u8; 32]> { + self.obtain_secret_and_credential(password_query) + .map(|(secret, _)| secret) + } + + pub fn obtain_secret_and_credential( + &self, + password_query: &str, + ) -> Fido2LuksResult<([u8; 32], FidoCredential)> { let mut salt = [0u8; 32]; match self.password_helper { PasswordHelper::Stdin if !self.verify_password.unwrap_or(true) => { @@ -158,10 +166,9 @@ impl SecretGeneration { }) .collect::>(); let credentials = credentials.iter().collect::>(); - Ok(assemble_secret( - &perform_challenge(&credentials[..], &salt, timeout - start.elapsed().unwrap())?, - &salt, - )) + let (secret, credential) = + perform_challenge(&credentials[..], &salt, timeout - start.elapsed().unwrap())?; + Ok((assemble_secret(&secret, &salt), credential.clone())) } } @@ -220,6 +227,9 @@ pub enum Command { /// Will wipe all other keys #[structopt(short = "e", long = "exclusive")] exclusive: bool, + /// Will add an token to your LUKS 2 header, including the credential id + #[structopt(short = "t", long = "token")] + token: bool, #[structopt(flatten)] existing_secret: OtherSecret, #[structopt(flatten)] @@ -235,6 +245,9 @@ pub enum Command { /// Add the password and keep the key #[structopt(short = "a", long = "add-password")] add_password: bool, + // /// Will add an token to your LUKS 2 header, including the credential id + // #[structopt(short = "t", long = "token")] + // token: bool, #[structopt(flatten)] replacement: OtherSecret, #[structopt(flatten)] @@ -296,18 +309,20 @@ pub fn run_cli() -> Fido2LuksResult<()> { Command::AddKey { device, exclusive, + token, existing_secret, ref secret_gen, luks_settings, } => { let secret_gen = secret_gen.patch(&args, None); let old_secret = existing_secret.obtain(&secret_gen, false, "Existing password")?; - let secret = secret_gen.obtain_secret("Password")?; + let (secret, credential) = secret_gen.obtain_secret_and_credential("Password")?; let added_slot = luks::add_key( device.clone(), &secret, &old_secret[..], luks_settings.kdf_time.or(Some(10)), + Some(&credential.id[..]).filter(|_| *token), )?; if *exclusive { let destroyed = luks::remove_keyslots(&device, &[added_slot])?; @@ -329,6 +344,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { Command::ReplaceKey { device, add_password, + //token, replacement, ref secret_gen, luks_settings, @@ -337,9 +353,21 @@ pub fn run_cli() -> Fido2LuksResult<()> { let secret = secret_gen.obtain_secret("Password")?; let new_secret = replacement.obtain(&secret_gen, true, "Replacement password")?; let slot = if *add_password { - luks::add_key(device, &new_secret[..], &secret, luks_settings.kdf_time) + luks::add_key( + device, + &new_secret[..], + &secret, + luks_settings.kdf_time, + None, + ) } else { - luks::replace_key(device, &new_secret[..], &secret, luks_settings.kdf_time) + luks::replace_key( + device, + &new_secret[..], + &secret, + luks_settings.kdf_time, + None, + ) }?; println!( "Added to password to device {}, slot: {}", diff --git a/src/device.rs b/src/device.rs index 12cae6f..251cca4 100644 --- a/src/device.rs +++ b/src/device.rs @@ -24,11 +24,11 @@ pub fn make_credential_id(name: Option<&str>) -> Fido2LuksResult )?) } -pub fn perform_challenge( - credentials: &[&FidoCredential], +pub fn perform_challenge<'a>( + credentials: &'a [&'a FidoCredential], salt: &[u8; 32], timeout: Duration, -) -> Fido2LuksResult<[u8; 32]> { +) -> Fido2LuksResult<([u8; 32], &'a FidoCredential)> { let request = FidoAssertionRequestBuilder::default() .rp_id(RP_ID) .credentials(credentials) @@ -37,13 +37,13 @@ pub fn perform_challenge( let get_assertion = |device: &mut FidoDevice| { device.get_hmac_assertion(&request, &util::sha256(&[&salt[..]]), None) }; - let (_, (secret, _)) = request_multiple_devices( + let (credential, (secret, _)) = request_multiple_devices( get_devices()? .iter_mut() .map(|device| (device, &get_assertion)), Some(timeout), )?; - Ok(secret) + Ok((secret, credential)) } pub fn get_devices() -> Fido2LuksResult> { diff --git a/src/error.rs b/src/error.rs index 3af80c3..eb3685d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,11 +13,13 @@ pub enum Fido2LuksError { AuthenticatorError { cause: ctap::FidoError }, #[fail(display = "no authenticator found, please ensure your device is plugged in")] NoAuthenticatorError, - #[fail(display = "luks err")] - LuksError { + #[fail(display = " {}", cause)] + CryptsetupError { cause: libcryptsetup_rs::LibcryptErr, }, - #[fail(display = "no authenticator found, please ensure your device is plugged in")] + #[fail(display = "{}", cause)] + LuksError { cause: LuksError }, + #[fail(display = "{}", cause)] IoError { cause: io::Error }, #[fail(display = "supplied secret isn't valid for this device")] WrongSecret, @@ -46,6 +48,14 @@ pub enum AskPassError { Mismatch, } +#[derive(Debug, Fail)] +pub enum LuksError { + #[fail(display = "This feature requires to the LUKS device to be formatted as LUKS 2")] + Luks2Required, + #[fail(display = "Invalid token: {}", _0)] + InvalidToken(String), +} + use libcryptsetup_rs::LibcryptErr; use std::string::FromUtf8Error; use Fido2LuksError::*; @@ -62,7 +72,7 @@ impl From for Fido2LuksError { LibcryptErr::IOError(e) if e.raw_os_error().iter().any(|code| code == &1i32) => { WrongSecret } - _ => LuksError { cause: e }, + _ => CryptsetupError { cause: e }, } } } diff --git a/src/luks.rs b/src/luks.rs index c09a5ad..03037f4 100644 --- a/src/luks.rs +++ b/src/luks.rs @@ -1,15 +1,53 @@ use crate::error::*; -use libcryptsetup_rs::{CryptActivateFlags, CryptDevice, CryptInit, KeyslotInfo}; +use failure::{Fail, ResultExt}; +use libcryptsetup_rs::{ + CryptActivateFlags, CryptDevice, CryptInit, CryptTokenInfo, EncryptionFormat, KeyslotInfo, +}; use std::path::Path; fn load_device_handle>(path: P) -> Fido2LuksResult { let mut device = CryptInit::init(path.as_ref())?; - let format = device.format_handle().get_type()?; - device.context_handle().load::<()>(format, None).map(|_| ())?; + //TODO: determine luks version some way other way than just trying + let mut load = |format| device.context_handle().load::<()>(format, None).map(|_| ()); + vec![EncryptionFormat::Luks2, EncryptionFormat::Luks1] + .into_iter() + .fold(None, |res, format| match res { + Some(Ok(())) => res, + Some(e) => Some(e.or_else(|_| load(format))), + None => Some(load(format)), + }) + .unwrap()?; Ok(device) } +fn check_luks2(device: &mut CryptDevice) -> Fido2LuksResult<()> { + match device.format_handle().get_type()? { + EncryptionFormat::Luks2 => Ok(()), + other => Err(Fido2LuksError::LuksError { + cause: LuksError::Luks2Required, + }), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Fido2LuksToken { + #[serde(rename = "type")] + type_: String, + credential: Vec, + keyslots: Vec, +} + +impl Fido2LuksToken { + fn new(credential_id: impl AsRef<[u8]>, slot: u32) -> Self { + Self { + type_: "fido2luks".into(), + credential: vec![hex::encode(credential_id)], + keyslots: vec![slot.to_string()], + } + } +} + pub fn open_container>(path: P, name: &str, secret: &[u8]) -> Fido2LuksResult<()> { let mut device = load_device_handle(path)?; device @@ -19,11 +57,18 @@ pub fn open_container>(path: P, name: &str, secret: &[u8]) -> Fid .map_err(|_e| Fido2LuksError::WrongSecret) } +/*pub fn open_container_token>(path: P, name: &str, secret: &[u8]) -> Fido2LuksResult<()> { + let mut device = load_device_handle(path)?; + check_luks2(&mut device)?; + unimplemented!() +}*/ + pub fn add_key>( path: P, secret: &[u8], old_secret: &[u8], iteration_time: Option, + credential_id: Option<&[u8]>, ) -> Fido2LuksResult { let mut device = load_device_handle(path)?; if let Some(millis) = iteration_time { @@ -32,9 +77,53 @@ pub fn add_key>( let slot = device .keyslot_handle(None) .add_by_passphrase(old_secret, secret)?; + if let Some(id) = credential_id { + /* if let e @ Err(_) = check_luks2(&mut device) { + //rollback + device.keyslot_handle(Some(slot)).destroy()?; + return e.map(|_| 0u32); + }*/ + device.token_handle().json_set( + None, + &serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(), + )?; + } + Ok(slot) } +fn find_token( + device: &mut CryptDevice, + slot: u32, +) -> Fido2LuksResult> { + for i in 0..256 { + let (status, type_) = device.token_handle().status(i)?; + if status == CryptTokenInfo::Inactive { + break; + } + if type_ != "fido2luks" { + continue; + } + let json = device.token_handle().json_get(i)?; + let info: Fido2LuksToken = + serde_json::from_value(json.clone()).map_err(|_| Fido2LuksError::LuksError { + cause: LuksError::InvalidToken(json.to_string()), + })?; + if info.keyslots.contains(&slot.to_string()) { + return Ok(Some((i, info))); + } + } + Ok((None)) +} + +fn remove_token(device: &mut CryptDevice, slot: u32) -> Fido2LuksResult<()> { + if let Some((token, _)) = find_token(device, slot)? { + // remove API?? + // device.token_handle() + } + Ok(()) +} + pub fn remove_keyslots>(path: P, exclude: &[u32]) -> Fido2LuksResult { let mut device = load_device_handle(path)?; let mut handle; @@ -42,15 +131,17 @@ pub fn remove_keyslots>(path: P, exclude: &[u32]) -> Fido2LuksRes //TODO: detect how many keyslots there are instead of trying within a given range for slot in 0..1024 { handle = device.keyslot_handle(Some(slot)); + let last = KeyslotInfo::ActiveLast == handle.status()?; match handle.status()? { KeyslotInfo::Inactive => continue, KeyslotInfo::Active if !exclude.contains(&slot) => { handle.destroy()?; + remove_token(&mut device, slot)?; destroyed += 1; } _ => (), } - if let KeyslotInfo::ActiveLast = handle.status()? { + if last { break; } } @@ -62,13 +153,24 @@ pub fn replace_key>( secret: &[u8], old_secret: &[u8], iteration_time: Option, + credential_id: Option<&[u8]>, ) -> Fido2LuksResult { let mut device = load_device_handle(path)?; // Set iteration time not sure wether this applies to luks2 as well if let Some(millis) = iteration_time { device.settings_handle().set_iteration_time(millis) } - Ok(device + let slot = device .keyslot_handle(None) - .change_by_passphrase(None, None, old_secret, secret)? as u32) + .change_by_passphrase(None, None, old_secret, secret)? as u32; + if let Some(id) = credential_id { + if check_luks2(&mut device).is_ok() { + let token = find_token(&mut device, slot)?.map(|(t, _)| t); + device.token_handle().json_set( + token, + &serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(), + )?; + } + } + Ok(slot) } diff --git a/src/main.rs b/src/main.rs index e0c701c..d23eda4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #[macro_use] extern crate failure; extern crate ctap_hmac as ctap; +#[macro_use] +extern crate serde_derive; use crate::cli::*; use crate::config::*; use crate::device::*;