diff --git a/src/cli.rs b/src/cli.rs index 8be3944..c392c83 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,4 @@ use crate::error::*; -use crate::luks; use crate::*; use structopt::StructOpt; @@ -12,8 +11,10 @@ use std::io::Write; use std::process::exit; use std::thread; +use crate::luks::{Fido2LuksToken, LuksDevice}; use crate::util::sha256; use std::borrow::Cow; +use std::collections::HashSet; use std::time::SystemTime; #[derive(Debug, Eq, PartialEq, Clone)] @@ -25,6 +26,12 @@ impl Display for HexEncoded { } } +impl AsRef<[u8]> for HexEncoded { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + impl FromStr for HexEncoded { type Err = hex::FromHexError; @@ -59,7 +66,7 @@ impl FromStr for CommaSeparated { #[derive(Debug, StructOpt)] pub struct Credentials { - /// FIDO credential ids, seperated by ',' generate using fido2luks credential + /// FIDO credential ids, separated by ',' generate using fido2luks credential #[structopt(name = "credential-id", env = "FIDO2LUKS_CREDENTIAL_ID")] pub ids: CommaSeparated, } @@ -282,6 +289,45 @@ pub enum Command { /// Check if an authenticator is connected #[structopt(name = "connected")] Connected, + Token(TokenCommand), +} + +///LUKS2 token related operations +#[derive(Debug, StructOpt)] +pub enum TokenCommand { + /// List all tokens associated with the specified device + List { + #[structopt(env = "FIDO2LUKS_DEVICE")] + device: PathBuf, + /// Dump all credentials as CSV + #[structopt(long = "csv")] + csv: bool, + }, + /// Add credential to a keyslot + Add { + #[structopt(env = "FIDO2LUKS_DEVICE")] + device: PathBuf, + #[structopt(flatten)] + credentials: Credentials, + /// Slot to which the credentials will be added + #[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")] + slot: u32, + }, + /// Remove credentials from token(s) + Remove { + #[structopt(env = "FIDO2LUKS_DEVICE")] + device: PathBuf, + #[structopt(flatten)] + credentials: Credentials, + /// Token from which the credentials will be removed + #[structopt(long = "token")] + token_id: Option, + }, + /// Remove all unassigned tokens + GC { + #[structopt(env = "FIDO2LUKS_DEVICE")] + device: PathBuf, + }, } pub fn parse_cmdline() -> Args { @@ -371,7 +417,9 @@ pub fn run_cli() -> Fido2LuksResult<()> { secret.salt.obtain(&secret.password_helper) } }; - let other_secret = |salt_q: &str, verify: bool| -> Fido2LuksResult<(Vec, Option)> { + let other_secret = |salt_q: &str, + verify: bool| + -> Fido2LuksResult<(Vec, Option)> { match other_secret { OtherSecret { keyfile: Some(file), @@ -386,7 +434,10 @@ pub fn run_cli() -> Fido2LuksResult<()> { pin.as_deref(), ) .map(|(secret, cred)| (secret[..].to_vec(), Some(cred)))?), - _ => Ok((util::read_password(salt_q, verify)?.as_bytes().to_vec(), None)), + _ => Ok(( + util::read_password(salt_q, verify)?.as_bytes().to_vec(), + None, + )), } }; let secret = |verify: bool| -> Fido2LuksResult<([u8; 32], FidoCredential)> { @@ -397,22 +448,20 @@ pub fn run_cli() -> Fido2LuksResult<()> { pin.as_deref(), ) }; + let mut luks_dev = LuksDevice::load(&luks.device)?; // Non overlap match &args.command { - Command::AddKey { - exclusive, .. - } => { + Command::AddKey { exclusive, .. } => { let (existing_secret, _) = other_secret("Current password", false)?; let (new_secret, cred) = secret(true)?; - let added_slot = luks::add_key( - &luks.device, + let added_slot = luks_dev.add_key( &new_secret, &existing_secret[..], luks_mod.kdf_time.or(Some(10)), Some(&cred.id[..]).filter(|_| *token), )?; if *exclusive { - let destroyed = luks::remove_keyslots(&luks.device, &[added_slot])?; + let destroyed = luks_dev.remove_keyslots(&[added_slot])?; println!( "Added to key to device {}, slot: {}\nRemoved {} old keys", luks.device.display(), @@ -432,16 +481,14 @@ pub fn run_cli() -> Fido2LuksResult<()> { let (existing_secret, _) = secret(false)?; let (replacement_secret, cred) = other_secret("Replacement password", true)?; let slot = if *add_password { - luks::add_key( - &luks.device, + luks_dev.add_key( &replacement_secret[..], &existing_secret, luks_mod.kdf_time, cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]), ) } else { - luks::replace_key( - &luks.device, + luks_dev.replace_key( &replacement_secret[..], &existing_secret, luks_mod.kdf_time, @@ -499,14 +546,12 @@ pub fn run_cli() -> Fido2LuksResult<()> { }; 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::open_container(&luks.device, &name, &secret, luks.slot) - }), - Command::OpenToken { .. } => luks::open_container_token( - &luks.device, + .and_then(|(secret, _cred)| luks_dev.activate(&name, &secret, luks.slot)), + Command::OpenToken { .. } => luks_dev.activate_token( &name, Box::new(|credentials: Vec| { let creds = credentials @@ -516,6 +561,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { secret(Cow::Owned(creds)) .map(|(secret, cred)| (secret, hex::encode(&cred.id))) }), + luks.slot, ), _ => unreachable!(), }; @@ -531,7 +577,7 @@ pub fn run_cli() -> Fido2LuksResult<()> { retries -= 1; eprintln!("{}", e); } - res => break res, + res => break res.map(|_| ()), } } } @@ -542,5 +588,129 @@ pub fn run_cli() -> Fido2LuksResult<()> { } _ => exit(1), }, + Command::Token(cmd) => match cmd { + TokenCommand::List { + device, + csv: dump_credentials, + } => { + let mut dev = LuksDevice::load(device)?; + let mut creds = Vec::new(); + for token in dev.tokens()? { + let (id, token) = token?; + for cred in token.credential.iter() { + if !creds.contains(cred) { + creds.push(cred.clone()); + if *dump_credentials { + print!("{}{}", if creds.len() == 1 { "" } else { "," }, cred); + } + } + } + if *dump_credentials { + continue; + } + println!( + "{}:\n\tSlots: {}\n\tCredentials: {}", + id, + if token.keyslots.is_empty() { + "None".into() + } else { + token.keyslots.iter().cloned().collect::>().join(",") + }, + token + .credential + .iter() + .map(|cred| format!( + "{} ({})", + cred, + creds.iter().position(|c| c == cred).unwrap().to_string() + )) + .collect::>() + .join(",") + ); + } + if *dump_credentials { + println!(); + } + Ok(()) + } + TokenCommand::Add { + device, + credentials, + slot, + } => { + let mut dev = LuksDevice::load(device)?; + let mut tokens = Vec::new(); + for token in dev.tokens()? { + let (id, token) = token?; + if token.keyslots.contains(&slot.to_string()) { + tokens.push((id, token)); + } + } + let count = if tokens.is_empty() { + dev.add_token(&Fido2LuksToken::with_credentials(&credentials.ids.0, *slot))?; + 1 + } else { + tokens.len() + }; + for (id, mut token) in tokens { + token + .credential + .extend(credentials.ids.0.iter().map(|h| h.to_string())); + dev.update_token(id, &token)?; + } + println!("Updated {} tokens", count); + Ok(()) + } + TokenCommand::Remove { + device, + credentials, + token_id, + } => { + let mut dev = LuksDevice::load(device)?; + let mut tokens = Vec::new(); + for token in dev.tokens()? { + let (id, token) = token?; + if let Some(token_id) = token_id { + if id == *token_id { + tokens.push((id, token)); + } + } else { + tokens.push((id, token)); + } + } + let count = tokens.len(); + for (id, mut token) in tokens { + token.credential = token + .credential + .into_iter() + .filter(|cred| !credentials.ids.0.iter().any(|h| &h.to_string() == cred)) + .collect(); + dev.update_token(id, &token)?; + } + println!("Updated {} tokens", count); + Ok(()) + } + TokenCommand::GC { device } => { + let mut dev = LuksDevice::load(device)?; + let mut creds: HashSet = HashSet::new(); + let mut remove = Vec::new(); + for token in dev.tokens()? { + let (id, token) = token?; + if token.keyslots.is_empty() || token.credential.is_empty() { + creds.extend(token.credential); + remove.push(id); + } + } + for id in remove.iter().rev() { + dev.remove_token(*id)?; + } + println!( + "Removed {} tokens, affected credentials: {}", + remove.len(), + creds.into_iter().collect::>().join(",") + ); + Ok(()) + } + }, } } diff --git a/src/error.rs b/src/error.rs index 18e3bee..0f7a34c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -75,6 +75,12 @@ impl LuksError { } } +impl From for Fido2LuksError { + fn from(e: LuksError) -> Self { + Fido2LuksError::LuksError { cause: e } + } +} + use libcryptsetup_rs::LibcryptErr; use std::io::ErrorKind; use std::string::FromUtf8Error; diff --git a/src/luks.rs b/src/luks.rs index c38359c..6996eef 100644 --- a/src/luks.rs +++ b/src/luks.rs @@ -7,245 +7,321 @@ use libcryptsetup_rs::{ use std::collections::{HashMap, HashSet}; use std::path::Path; -fn load_device_handle>(path: P) -> Fido2LuksResult { - let mut device = CryptInit::init(path.as_ref())?; - device.context_handle().load::<()>(None, None)?; - Ok(device) +pub struct LuksDevice { + device: CryptDevice, + luks2: Option, } -fn check_luks2(device: &mut CryptDevice) -> Fido2LuksResult<()> { - match device.format_handle().get_type()? { - EncryptionFormat::Luks2 => Ok(()), - _ => Err(Fido2LuksError::LuksError { - cause: LuksError::Luks2Required, - }), +impl LuksDevice { + pub fn load>(path: P) -> Fido2LuksResult { + let mut device = CryptInit::init(path.as_ref())?; + device.context_handle().load::<()>(None, None)?; + Ok(Self { + device, + luks2: None, + }) + } + + pub fn is_luks2(&mut self) -> Fido2LuksResult { + if let Some(luks2) = self.luks2 { + Ok(luks2) + } else { + self.luks2 = Some(match self.device.format_handle().get_type()? { + EncryptionFormat::Luks2 => true, + _ => false, + }); + self.is_luks2() + } + } + + fn require_luks2(&mut self) -> Fido2LuksResult<()> { + if !self.is_luks2()? { + return Err(LuksError::Luks2Required.into()); + } + Ok(()) + } + + pub fn tokens<'a>( + &'a mut self, + ) -> Fido2LuksResult> + 'a>> + { + self.require_luks2()?; + Ok(Box::new( + (0..32) + .map(move |i| { + let status = match self.device.token_handle().status(i) { + Ok(status) => status, + Err(err) => return Some(Err(Fido2LuksError::from(err))), + }; + match status { + CryptTokenInfo::Inactive => return None, + CryptTokenInfo::Internal(s) + | CryptTokenInfo::InternalUnknown(s) + | CryptTokenInfo::ExternalUnknown(s) + | CryptTokenInfo::External(s) + if &s != Fido2LuksToken::default_type() => + { + return None + } + _ => (), + }; + let json = match self.device.token_handle().json_get(i) { + Ok(json) => json, + Err(err) => return Some(Err(Fido2LuksError::from(err))), + }; + let info: Fido2LuksToken = + match serde_json::from_value(json.clone()).map_err(|_| { + Fido2LuksError::LuksError { + cause: LuksError::InvalidToken(json.to_string()), + } + }) { + Ok(info) => info, + Err(err) => return Some(Err(Fido2LuksError::from(err))), + }; + Some(Ok((i, info))) + }) + .filter_map(|o| o), + )) + } + + pub fn find_token(&mut self, slot: u32) -> Fido2LuksResult> { + let slot_str = slot.to_string(); + for token in self.tokens()? { + let (id, token) = token?; + if token.keyslots.contains(&slot_str) { + return Ok(Some((id, token))); + } + } + Ok(None) + } + + pub fn add_token(&mut self, data: &Fido2LuksToken) -> Fido2LuksResult<()> { + self.require_luks2()?; + self.device + .token_handle() + .json_set(TokenInput::AddToken(&serde_json::to_value(&data).unwrap()))?; + Ok(()) + } + + pub fn remove_token(&mut self, token: u32) -> Fido2LuksResult<()> { + self.require_luks2()?; + self.device + .token_handle() + .json_set(TokenInput::RemoveToken(token))?; + Ok(()) + } + + pub fn update_token(&mut self, token: u32, data: &Fido2LuksToken) -> Fido2LuksResult<()> { + self.require_luks2()?; + self.device + .token_handle() + .json_set(TokenInput::ReplaceToken( + token, + &serde_json::to_value(&data).unwrap(), + ))?; + Ok(()) + } + + pub fn add_key( + &mut self, + secret: &[u8], + old_secret: &[u8], + iteration_time: Option, + credential_id: Option<&[u8]>, + ) -> Fido2LuksResult { + if let Some(millis) = iteration_time { + self.device.settings_handle().set_iteration_time(millis) + } + let slot = self + .device + .keyslot_handle() + .add_by_passphrase(None, old_secret, secret)?; + if let Some(id) = credential_id { + self.device.token_handle().json_set(TokenInput::AddToken( + &serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(), + ))?; + } + + Ok(slot) + } + + pub fn remove_keyslots(&mut self, exclude: &[u32]) -> Fido2LuksResult { + let mut destroyed = 0; + let mut tokens = Vec::new(); + for slot in 0..256 { + match self.device.keyslot_handle().status(slot)? { + KeyslotInfo::Inactive => continue, + KeyslotInfo::Active | KeyslotInfo::ActiveLast if !exclude.contains(&slot) => { + if self.is_luks2()? { + if let Some((id, _token)) = self.find_token(slot)? { + tokens.push(id); + } + } + self.device.keyslot_handle().destroy(slot)?; + destroyed += 1; + } + KeyslotInfo::ActiveLast => break, + _ => (), + } + if self.device.keyslot_handle().status(slot)? == KeyslotInfo::ActiveLast { + break; + } + } + // Ensure indices stay valid + tokens.sort(); + for token in tokens.iter().rev() { + self.remove_token(*token)?; + } + Ok(destroyed) + } + + pub fn replace_key( + &mut self, + secret: &[u8], + old_secret: &[u8], + iteration_time: Option, + credential_id: Option<&[u8]>, + ) -> Fido2LuksResult { + if let Some(millis) = iteration_time { + self.device.settings_handle().set_iteration_time(millis) + } + // Use activate dry-run to locate keyslot + let slot = self.device.activate_handle().activate_by_passphrase( + None, + None, + old_secret, + CryptActivateFlags::empty(), + )?; + self.device.keyslot_handle().change_by_passphrase( + Some(slot), + Some(slot), + old_secret, + secret, + )? as u32; + if let Some(id) = credential_id { + if self.is_luks2()? { + let token = self.find_token(slot)?.map(|(t, _)| t); + let json = serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(); + if let Some(token) = token { + self.device + .token_handle() + .json_set(TokenInput::ReplaceToken(token, &json))?; + } else { + self.device + .token_handle() + .json_set(TokenInput::AddToken(&json))?; + } + } + } + Ok(slot) + } + + pub fn activate( + &mut self, + name: &str, + secret: &[u8], + slot_hint: Option, + ) -> Fido2LuksResult { + self.device + .activate_handle() + .activate_by_passphrase(Some(name), slot_hint, secret, CryptActivateFlags::empty()) + .map_err(LuksError::activate) + } + + pub fn activate_token( + &mut self, + name: &str, + secret: impl Fn(Vec) -> Fido2LuksResult<([u8; 32], String)>, + slot_hint: Option, + ) -> Fido2LuksResult { + if !self.is_luks2()? { + return Err(LuksError::Luks2Required.into()); + } + let mut creds: HashMap> = HashMap::new(); + for token in self.tokens()? { + let token = match token { + Ok((_id, t)) => t, + _ => continue, // An corrupted token should't lock the user out + }; + let slots = || { + token + .keyslots + .iter() + .filter_map(|slot| slot.parse::().ok()) + }; + for cred in token.credential.iter() { + creds + .entry(cred.clone()) + .or_insert_with(|| slots().collect::>()) + .extend(slots()); + } + } + if creds.is_empty() { + return Err(Fido2LuksError::LuksError { + cause: LuksError::NoToken, + }); + } + let (secret, credential) = secret(creds.keys().cloned().collect())?; + let empty; + let slots = if let Some(slots) = creds.get(&credential) { + slots + } else { + empty = HashSet::new(); + &empty + }; + //Try slots associated with the credential used + let slots = slots.iter().cloned().map(Option::Some).chain( + std::iter::once(slot_hint) // Try slot hint if there is one + .take(slot_hint.is_some() as usize) + .chain(std::iter::once(None).take(slots.is_empty() as usize)), // Try all slots as last resort + ); + for slot in slots { + match self.activate(name, &secret, slot) { + Err(Fido2LuksError::WrongSecret) => (), + res => return res, + } + } + Err(Fido2LuksError::WrongSecret) } } #[derive(Debug, Clone, Serialize, Deserialize)] -struct Fido2LuksToken { +pub struct Fido2LuksToken { #[serde(rename = "type")] - type_: String, - credential: Vec, - keyslots: Vec, + pub type_: String, + pub credential: HashSet, + pub keyslots: HashSet, } impl Fido2LuksToken { - fn new(credential_id: impl AsRef<[u8]>, slot: u32) -> Self { + pub fn new(credential_id: impl AsRef<[u8]>, slot: u32) -> Self { + Self::with_credentials(std::iter::once(credential_id), slot) + } + + pub fn with_credentials, B: AsRef<[u8]>>( + credentials: I, + slot: u32, + ) -> Self { Self { - type_: "fido2luks\0".into(), // Doubles as c style string - credential: vec![hex::encode(credential_id)], - keyslots: vec![slot.to_string()], + credential: credentials + .into_iter() + .map(|cred| hex::encode(cred.as_ref())) + .collect(), + keyslots: vec![slot.to_string()].into_iter().collect(), + ..Default::default() } } + pub fn default_type() -> &'static str { + "fido2luks" + } } -pub fn open_container>( - path: P, - name: &str, - secret: &[u8], - slot_hint: Option, -) -> Fido2LuksResult<()> { - let mut device = load_device_handle(path)?; - device - .activate_handle() - .activate_by_passphrase(Some(name), slot_hint, secret, CryptActivateFlags::empty()) - .map(|_slot| ()) - .map_err(|_e| Fido2LuksError::WrongSecret) -} - -pub fn open_container_token>( - path: P, - name: &str, - secret: impl Fn(Vec) -> Fido2LuksResult<([u8; 32], String)>, -) -> Fido2LuksResult<()> { - let mut device = load_device_handle(path)?; - check_luks2(&mut device)?; - - let mut creds = HashMap::new(); - for i in 0..256 { - let status = device.token_handle().status(i)?; - match status { - CryptTokenInfo::Inactive => break, - CryptTokenInfo::Internal(s) - | CryptTokenInfo::InternalUnknown(s) - | CryptTokenInfo::ExternalUnknown(s) - | CryptTokenInfo::External(s) - if &s != "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()), - })?; - let slots = || { - info.keyslots - .iter() - .filter_map(|slot| slot.parse::().ok()) - }; - for cred in info.credential.iter().cloned() { - creds - .entry(cred) - .or_insert_with(|| slots().collect::>()) - .extend(slots()); +impl Default for Fido2LuksToken { + fn default() -> Self { + Self { + type_: Self::default_type().into(), + credential: HashSet::new(), + keyslots: HashSet::new(), } } - if creds.is_empty() { - return Err(Fido2LuksError::LuksError { - cause: LuksError::NoToken, - }); - } - let (secret, credential) = secret(creds.keys().cloned().collect())?; - let slots = creds.get(&credential).unwrap(); - let slots = slots - .iter() - .cloned() - .map(Option::Some) - .chain(std::iter::once(None).take(slots.is_empty() as usize)); - for slot in slots { - match device - .activate_handle() - .activate_by_passphrase(Some(name), slot, &secret, CryptActivateFlags::empty()) - .map(|_slot| ()) - .map_err(LuksError::activate) - { - Err(Fido2LuksError::WrongSecret) => (), - res => return res, - } - } - Err(Fido2LuksError::WrongSecret) -} - -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 { - device.settings_handle().set_iteration_time(millis) - } - let slot = device - .keyslot_handle() - .add_by_passphrase(None, 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(TokenInput::AddToken( - &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 = device.token_handle().status(i)?; - match status { - CryptTokenInfo::Inactive => break, - CryptTokenInfo::Internal(s) - | CryptTokenInfo::InternalUnknown(s) - | CryptTokenInfo::ExternalUnknown(s) - | CryptTokenInfo::External(s) - if &s != "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) -} - -pub fn remove_keyslots>(path: P, exclude: &[u32]) -> Fido2LuksResult { - let mut device = load_device_handle(path)?; - let mut destroyed = 0; - let mut tokens = Vec::new(); - for slot in 0..256 { - match device.keyslot_handle().status(slot)? { - KeyslotInfo::Inactive => continue, - KeyslotInfo::Active | KeyslotInfo::ActiveLast if !exclude.contains(&slot) => { - if let Ok(_) = check_luks2(&mut device) { - if let Some((token, _)) = find_token(&mut device, slot)? { - tokens.push(token); - } - } - device.keyslot_handle().destroy(slot)?; - destroyed += 1; - } - KeyslotInfo::ActiveLast => break, - _ => (), - } - if device.keyslot_handle().status(slot)? == KeyslotInfo::ActiveLast { - break; - } - } - // Ensure indices stay valid - tokens.sort(); - for token in tokens.iter().rev() { - device - .token_handle() - .json_set(TokenInput::RemoveToken(*token))?; - } - Ok(destroyed) -} - -pub fn replace_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 { - device.settings_handle().set_iteration_time(millis) - } - // Use activate dry-run to locate keyslot - let slot = device.activate_handle().activate_by_passphrase( - None, - None, - old_secret, - CryptActivateFlags::empty(), - )?; - device - .keyslot_handle() - .change_by_passphrase(Some(slot), Some(slot), 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); - let json = serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(); - if let Some(token) = token { - device - .token_handle() - .json_set(TokenInput::ReplaceToken(token, &json))?; - } else { - device - .token_handle() - .json_set(TokenInput::AddToken(&json))?; - } - } - } - Ok(slot) }