diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f11415..4ffc8a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support CTAP 2.1 - Serialize PIN hash with `serde-bytes` ([#52][]) - Reduce the space taken by credential serializaiton ([#59][]) +- Remove the per-relying party directory to save space ([#55][]) [#26]: https://github.com/solokeys/fido-authenticator/issues/26 [#28]: https://github.com/solokeys/fido-authenticator/issues/28 @@ -41,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#63]: https://github.com/Nitrokey/fido-authenticator/pull/63 [#52]: https://github.com/Nitrokey/fido-authenticator/issues/52 [#59]: https://github.com/Nitrokey/fido-authenticator/issues/59 +[#55]: https://github.com/Nitrokey/fido-authenticator/issues/55 ## [0.1.1] - 2022-08-22 - Fix bug that treated U2F payloads as APDU over APDU in NFC transport @conorpp diff --git a/Cargo.toml b/Cargo.toml index 54f3841..cc2db63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ log-warn = [] log-error = [] [dev-dependencies] +admin-app = { version = "0.1.0", features = ["migration-tests"] } aes = "0.8.4" cbc = { version = "0.1.2", features = ["alloc"] } ciborium = { version = "0.2.2" } @@ -80,11 +81,13 @@ x509-parser = "0.16.0" features = ["dispatch"] [patch.crates-io] +admin-app = { git = "https://github.com/Nitrokey/admin-app.git", tag = "v0.1.0-nitrokey.18" } ctaphid-dispatch = { git = "https://github.com/trussed-dev/ctaphid-dispatch.git", rev = "57cb3317878a8593847595319aa03ef17c29ec5b" } trussed = { git = "https://github.com/trussed-dev/trussed.git", rev = "046478b7a4f6e2315acf9112d98308379c2e3eee" } trussed-chunked = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "chunked-v0.1.0" } trussed-fs-info = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "fs-info-v0.1.0" } trussed-hkdf = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "hkdf-v0.2.0" } +trussed-manage = { git = "https://github.com/trussed-dev/trussed-staging.git", tag = "manage-v0.1.0" } trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", rev = "53eba84d2cd0bcacc3a7096d4b7a2490dcf6f069" } trussed-usbip = { git = "https://github.com/Nitrokey/pc-usbip-runner.git", tag = "v0.0.1-nitrokey.5" } usbd-ctaphid = { git = "https://github.com/trussed-dev/usbd-ctaphid.git", rev = "dcff9009c3cd1ef9e5b09f8f307aca998fc9a8c8" } diff --git a/src/ctap2.rs b/src/ctap2.rs index 1a9b82b..52a354d 100644 --- a/src/ctap2.rs +++ b/src/ctap2.rs @@ -392,7 +392,7 @@ impl Authenticator for crate::Authenti .ok(); let mut key_store_full = self.can_fit(serialized_credential.len()) == Some(false) - || CredentialManagement::new(self).count_credentials() + || CredentialManagement::new(self).count_credentials()? >= self .config .max_resident_credential_count @@ -914,7 +914,7 @@ impl Authenticator for crate::Authenti // TODO: use custom enum of known commands match parameters.sub_command { // 0x1 - Subcommand::GetCredsMetadata => Ok(cred_mgmt.get_creds_metadata()), + Subcommand::GetCredsMetadata => cred_mgmt.get_creds_metadata(), // 0x2 Subcommand::EnumerateRpsBegin => cred_mgmt.first_relying_party(), @@ -1011,7 +1011,7 @@ impl Authenticator for crate::Authenti // If no allowList is passed, credential is None and the retrieved credentials // are stored in state.runtime.credential_heap let (credential, num_credentials) = self - .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed) + .prepare_credentials(&rp_id_hash, ¶meters.allow_list, uv_performed)? .ok_or(Error::NoCredentials)?; info_now!("found {:?} applicable credentials", num_credentials); @@ -1152,7 +1152,7 @@ impl crate::Authenticator { rp_id_hash: &[u8; 32], allow_list: &Option, uv_performed: bool, - ) -> Option<(Credential, u32)> { + ) -> Result> { debug_now!("remaining stack size: {} bytes", msp() - 0x2000_0000); self.state.runtime.clear_credential_cache(); @@ -1186,50 +1186,74 @@ impl crate::Authenticator { continue; } - return Some((credential, 1)); + return Ok(Some((credential, 1))); } // we don't recognize any credentials in the allowlist - return None; + return Ok(None); } } // we are only dealing with discoverable credentials. debug_now!("Allowlist not passed, fetching RKs"); + self.prepare_cache(rp_id_hash, uv_performed)?; - let mut maybe_path = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_rk_dir(rp_id_hash), None,)) - .entry - .map(|entry| PathBuf::from(entry.path())); + let num_credentials = self.state.runtime.remaining_credentials(); + let credential = self.state.runtime.pop_credential(&mut self.trussed); + Ok(credential.map(|credential| (Credential::Full(credential), num_credentials))) + } + /// Populate the cache with the RP credentials. + /// + /// Returns true if legacy credentials are present and therefore prepare_cache_legacy should be called too + #[inline(never)] + fn prepare_cache(&mut self, rp_id_hash: &[u8; 32], uv_performed: bool) -> Result<()> { use crate::state::CachedCredential; use core::str::FromStr; - while let Some(path) = maybe_path { - let credential_data = - syscall!(self.trussed.read_file(Location::Internal, path.clone(),)).data; + let rp_rk_dir = rp_rk_dir(rp_id_hash); + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + PathBuf::from(RK_DIR), + Some(rp_rk_dir.clone()) + )) + .entry; + + while let Some(entry) = maybe_entry.take() { + if !entry.path().as_ref().starts_with(rp_rk_dir.as_ref()) { + // We got past all credentials for the relevant RP + break; + } - let credential = FullCredential::deserialize(&credential_data).ok()?; + if entry.path() == &*rp_rk_dir { + debug_assert!(entry.metadata().is_dir()); + error!("Migration missing"); + return Err(Error::Other); + } + + let credential_data = syscall!(self + .trussed + .read_file(Location::Internal, entry.path().into(),)) + .data; + + let credential = FullCredential::deserialize(&credential_data).map_err(|_err| { + error!("Failed to deserialize credential: {_err:?}"); + Error::Other + })?; let timestamp = credential.creation_time; let credential = Credential::Full(credential); if self.check_credential_applicable(&credential, false, uv_performed) { self.state.runtime.push_credential(CachedCredential { timestamp, - path: String::from_str(path.as_str_ref_with_trailing_nul()).ok()?, + path: String::from_str(entry.path().as_str_ref_with_trailing_nul()) + .map_err(|_| Error::Other)?, }); } - maybe_path = syscall!(self.trussed.read_dir_next()) - .entry - .map(|entry| PathBuf::from(entry.path())); + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; } - - let num_credentials = self.state.runtime.remaining_credentials(); - let credential = self.state.runtime.pop_credential(&mut self.trussed); - credential.map(|credential| (Credential::Full(credential), num_credentials)) + Ok(()) } fn decrypt_pin_hash_and_maybe_escalate( @@ -2078,11 +2102,12 @@ fn rp_rk_dir(rp_id_hash: &[u8; 32]) -> PathBuf { } fn rk_path(rp_id_hash: &[u8; 32], credential_id_hash: &[u8; 32]) -> PathBuf { - let mut path = rp_rk_dir(rp_id_hash); - - let mut hex = [0u8; 16]; - format_hex(&credential_id_hash[..8], &mut hex); - path.push(&PathBuf::try_from(&hex).unwrap()); + let mut buf = [0; 33]; + buf[16] = b'.'; + format_hex(&rp_id_hash[..8], &mut buf[..16]); + format_hex(&credential_id_hash[..8], &mut buf[17..]); + let mut path = PathBuf::from(RK_DIR); + path.push(&PathBuf::try_from(buf.as_slice()).unwrap()); path } diff --git a/src/ctap2/credential_management.rs b/src/ctap2/credential_management.rs index 99f84e2..00f6b3e 100644 --- a/src/ctap2/credential_management.rs +++ b/src/ctap2/credential_management.rs @@ -1,6 +1,7 @@ //! TODO: T -use core::{cmp, convert::TryFrom}; +use core::cmp::{self, Ordering}; +use core::{convert::TryFrom, num::NonZeroU32}; use trussed::{ syscall, try_syscall, @@ -57,16 +58,26 @@ where } } +/// Get the hex hashed ID of the RP from the filename of a RP directory OR a "new" RK path +fn get_id_hex(entry: &DirEntry) -> &str { + entry + .file_name() + .as_str() + .split('.') + .next() + .expect("Split always returns at least one empty string") +} + impl CredentialManagement<'_, UP, T> where UP: UserPresence, T: TrussedRequirements, { - pub fn get_creds_metadata(&mut self) -> Response { + pub fn get_creds_metadata(&mut self) -> Result { info!("get metadata"); let mut response: Response = Default::default(); - let credential_count = self.count_credentials(); + let credential_count = self.count_credentials()?; // We have a fixed limit determined by the configuration and an estimated limit determined // by the available space on the filesystem. The effective limit is the lower of the two. let max_remaining = self @@ -80,123 +91,96 @@ where response.max_possible_remaining_residential_credentials_count = Some(cmp::min(max_remaining, estimate_remaining)); - response + Ok(response) } - pub fn count_credentials(&mut self) -> u32 { + pub fn count_credentials(&mut self) -> Result { let dir = PathBuf::from(RK_DIR); - let maybe_first_rp = + let mut num_rks = 0; + + let mut maybe_next = syscall!(self .trussed .read_dir_first(Location::Internal, dir.clone(), None)) .entry; - let first_rp = match maybe_first_rp { - None => return 0, - Some(rp) => rp, - }; - - let (mut num_rks, _) = self.count_rp_rks(PathBuf::from(first_rp.path())); - let mut last_rp = PathBuf::from(first_rp.file_name()); - - loop { - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir.clone(), Some(last_rp),)) - .entry - .unwrap(); - let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - - match maybe_next_rp { - None => { - return num_rks; - } - Some(rp) => { - last_rp = PathBuf::from(rp.file_name()); - info!("counting.."); - let (this_rp_rk_count, _) = self.count_rp_rks(PathBuf::from(rp.path())); - info!("{:?}", this_rp_rk_count); - num_rks += this_rp_rk_count; - } + while let Some(rp) = maybe_next { + if rp.metadata().is_dir() { + error!("Migration not complete"); + return Err(Error::Other); } + + num_rks += 1; + maybe_next = syscall!(self.trussed.read_dir_next()).entry; } + + Ok(num_rks) } pub fn first_relying_party(&mut self) -> Result { info!("first rp"); - // rp (0x03): PublicKeyCredentialRpEntity - // rpIDHash (0x04) : RP ID SHA-256 hash. - // totalRPs (0x05) : Total number of RPs present on the authenticator. - - let mut response: Response = Default::default(); - + let mut response = Response::default(); let dir = PathBuf::from(RK_DIR); let maybe_first_rp = - syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)).entry; - - response.total_rps = Some(match maybe_first_rp { - None => 0, - _ => { - let mut num_rps = 1; - loop { - let maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - match maybe_next_rp { - None => break, - _ => num_rps += 1, - } - } - num_rps - } - }); - - if let Some(rp) = maybe_first_rp { - // load credential and extract rp and rpIdHash - let maybe_first_credential = syscall!(self.trussed.read_dir_first( - Location::Internal, - PathBuf::from(rp.path()), - None - )) + syscall!(self + .trussed + .read_dir_first(Location::Internal, dir.clone(), None)) .entry; - match maybe_first_credential { - None => panic!("chaos! disorder!"), - Some(rk_entry) => { - let serialized = syscall!(self - .trussed - .read_file(Location::Internal, rk_entry.path().into(),)) - .data; + let Some(first_rp) = maybe_first_rp else { + response.total_rps = Some(0); + return Ok(response); + }; - let credential = FullCredential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; + // The first one counts + let mut total_rps = 1; - let rp = credential.data.rp; + if first_rp.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); + } - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); - response.rp = Some(rp.into()); - } + let first_credential_data = syscall!(self + .trussed + .read_file(Location::Internal, first_rp.path().into())) + .data; + + let credential = FullCredential::deserialize(&first_credential_data)?; + let rp_id_hash: [u8; 32] = syscall!(self.trussed.hash_sha256(credential.rp.id.as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + + let mut current_rp = first_rp; + + let mut current_id_hex = get_id_hex(¤t_rp); + + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_id_hex(&entry); + if id_hex != current_id_hex { + total_rps += 1; + current_rp = entry; + current_id_hex = get_id_hex(¤t_rp) } + } - // cache state for next call - if let Some(total_rps) = response.total_rps { - if total_rps > 1 { - let rp_id_hash = response.rp_id_hash.unwrap().into_array(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: total_rps - 1, - rp_id_hash, - }); - } - } + if let Some(remaining) = NonZeroU32::new(total_rps - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining, + rp_id_hash, + }); } + response.total_rps = Some(total_rps); + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); Ok(response) } pub fn next_relying_party(&mut self) -> Result { - info!("next rp"); - let CredentialManagementEnumerateRps { remaining, rp_id_hash: last_rp_id_hash, @@ -207,90 +191,66 @@ where .clone() .ok_or(Error::NotAllowed)?; - let dir = PathBuf::from(RK_DIR); - let mut hex = [b'0'; 16]; super::format_hex(&last_rp_id_hash[..8], &mut hex); let filename = PathBuf::try_from(&hex).unwrap(); - let mut maybe_next_rp = - syscall!(self - .trussed - .read_dir_first(Location::Internal, dir, Some(filename),)) - .entry; + let dir = PathBuf::from(RK_DIR); + + let maybe_next_rp = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + dir, + Some(filename) + )) + .entry; - // Advance to the next - if maybe_next_rp.is_some() { - maybe_next_rp = syscall!(self.trussed.read_dir_next()).entry; - } else { + let mut response = Response::default(); + + let Some(current_rp) = maybe_next_rp else { return Err(Error::NotAllowed); - } + }; - let mut response: Response = Default::default(); + let current_id_hex = get_id_hex(¤t_rp); - if let Some(rp) = maybe_next_rp { - // load credential and extract rp and rpIdHash - let maybe_first_credential = syscall!(self.trussed.read_dir_first( - Location::Internal, - PathBuf::from(rp.path()), - None - )) - .entry; + debug_assert!(current_rp.file_name().as_str().as_bytes().starts_with(&hex)); - match maybe_first_credential { - None => panic!("chaos! disorder!"), - Some(rk_entry) => { - let serialized = syscall!(self - .trussed - .read_file(Location::Internal, rk_entry.path().into(),)) - .data; - - let credential = FullCredential::deserialize(&serialized) - // this may be a confusing error message - .map_err(|_| Error::InvalidCredential)?; - - let rp = credential.data.rp; - - response.rp_id_hash = Some(ByteArray::new(self.hash(rp.id.as_ref()))); - response.rp = Some(rp.into()); - - // cache state for next call - if remaining > 1 { - let rp_id_hash = response.rp_id_hash.unwrap().into_array(); - self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { - remaining: remaining - 1, - rp_id_hash, - }); - } else { - self.state.runtime.cached_rp = None; - } - } + while let Some(entry) = syscall!(self.trussed.read_dir_next()).entry { + let id_hex = get_id_hex(&entry); + if id_hex == current_id_hex { + continue; } - } else { - self.state.runtime.cached_rp = None; - } - Ok(response) - } + if entry.metadata().is_dir() { + warn!("While iterating: migration is not finished"); + return Err(Error::Other); + } - fn count_rp_rks(&mut self, rp_dir: PathBuf) -> (u32, Option) { - let maybe_first_rk = - syscall!(self + let data = syscall!(self .trussed - .read_dir_first(Location::Internal, rp_dir, None)) - .entry; - - let Some(first_rk) = maybe_first_rk else { - warn!("empty RP directory"); - return (0, None); - }; + .read_file(Location::Internal, entry.path().into())) + .data; + + let credential = FullCredential::deserialize(&data)?; + let rp_id_hash: [u8; 32] = + syscall!(self.trussed.hash_sha256(credential.rp.id.as_ref())) + .hash + .as_slice() + .try_into() + .map_err(|_| Error::Other)?; + response.rp_id_hash = Some(ByteArray::new(rp_id_hash)); + response.rp = Some(credential.data.rp.into()); + + if let Some(new_remaining) = NonZeroU32::new(remaining.get() - 1) { + self.state.runtime.cached_rp = Some(CredentialManagementEnumerateRps { + remaining: new_remaining, + rp_id_hash, + }); + } - // count the rest of them - let mut num_rks = 1; - while syscall!(self.trussed.read_dir_next()).entry.is_some() { - num_rks += 1; + return Ok(response); } - (num_rks, Some(first_rk)) + + Err(Error::NotAllowed) } pub fn first_credential(&mut self, rp_id_hash: &[u8; 32]) -> Result { @@ -301,8 +261,40 @@ where let mut hex = [b'0'; 16]; super::format_hex(&rp_id_hash[..8], &mut hex); - let rp_dir = PathBuf::from(RK_DIR).join(&PathBuf::try_from(&hex).unwrap()); - let (num_rks, first_rk) = self.count_rp_rks(rp_dir); + let rk_dir = PathBuf::from(RK_DIR); + let rp_dir_start = PathBuf::try_from(&hex).unwrap(); + + let mut num_rks = 0; + + let mut maybe_entry = syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + rk_dir.clone(), + Some(rp_dir_start.clone()) + )) + .entry; + + let mut first_rk = None; + + while let Some(entry) = maybe_entry { + if !entry.file_name().as_str().as_bytes().starts_with(&hex) { + // We got past all credentials for the relevant RP + break; + } + + if entry.file_name() == &*rp_dir_start { + // This is the case where we + debug_assert!(entry.metadata().is_dir()); + error!("Migration did not run"); + return Err(Error::Other); + } + + first_rk = first_rk.or(Some(entry)); + num_rks += 1; + + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } + + // TODO: FIX let first_rk = first_rk.ok_or(Error::NoCredentials)?; // extract data required into response @@ -315,8 +307,8 @@ where // let rp_id_hash = response.rp_id_hash.as_ref().unwrap().clone(); self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { remaining: num_rks - 1, - rp_dir: first_rk.path().parent().unwrap(), - prev_filename: PathBuf::from(first_rk.file_name()), + rp_dir: rk_dir, + prev_filename: Some(first_rk.file_name().into()), }); } } @@ -327,60 +319,55 @@ where pub fn next_credential(&mut self) -> Result { info!("next credential"); - let CredentialManagementEnumerateCredentials { - remaining, - rp_dir, - prev_filename, - } = self + let cache = self .state .runtime .cached_rk - .clone() + .take() .ok_or(Error::NotAllowed)?; - // let (remaining, rp_dir, prev_filename) = match self.state.runtime.cached_rk { - // Some(CredentialManagementEnumerateCredentials( - // x, ref y, ref z)) - // => (x, y.clone(), z.clone()), - // _ => return Err(Error::NotAllowed), - // }; - self.state.runtime.cached_rk = None; + let CredentialManagementEnumerateCredentials { + remaining, + rp_dir, + prev_filename, + } = cache; - // let mut hex = [b'0'; 16]; - // super::format_hex(&rp_id_hash[..8], &mut hex); - // let rp_dir = PathBuf::from(b"rk").join(&PathBuf::from(&hex)); + debug_assert!(prev_filename.is_some()); - let mut maybe_next_rk = - syscall!(self - .trussed - .read_dir_first(Location::Internal, rp_dir, Some(prev_filename))) - .entry; + syscall!(self.trussed.read_dir_first_alphabetical( + Location::Internal, + rp_dir.clone(), + prev_filename + )) + .entry; - // Advance to the next - if maybe_next_rk.is_some() { - maybe_next_rk = syscall!(self.trussed.read_dir_next()).entry; - } else { - return Err(Error::NotAllowed); + // The previous entry was already read. Skip to the next + let Some(entry) = syscall!(self.trussed.read_dir_next()).entry else { + return Err(Error::NoCredentials); + }; + + if entry.file_name().cmp_lfs(&rp_dir) == Ordering::Greater { + // We reached the end of the credentials for the rp + return Err(Error::NoCredentials); } - match maybe_next_rk { - Some(rk) => { - // extract data required into response - let response = self.extract_response_from_credential_file(rk.path())?; - - // cache state for next call - if remaining > 1 { - self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { - remaining: remaining - 1, - rp_dir: rk.path().parent().unwrap(), - prev_filename: PathBuf::from(rk.file_name()), - }); - } - - Ok(response) - } - None => Err(Error::NoCredentials), + if entry.metadata().is_dir() { + warn!("Migration did not finish"); + return Err(Error::Other); + } + + let response = self.extract_response_from_credential_file(entry.path())?; + + // cache state for next call + if remaining > 1 { + self.state.runtime.cached_rk = Some(CredentialManagementEnumerateCredentials { + remaining: remaining - 1, + rp_dir, + prev_filename: Some(entry.file_name().into()), + }); } + + Ok(response) } fn extract_response_from_credential_file(&mut self, rk_path: &Path) -> Result { @@ -472,14 +459,20 @@ where ) -> Option { let credential_id_hash = self.hash(credential.id); let mut hex = [b'0'; 16]; - super::format_hex(&credential_id_hash[..8], &mut hex); + let hex_str = super::format_hex(&credential_id_hash[..8], &mut hex); let dir = PathBuf::from(RK_DIR); - let filename = PathBuf::try_from(&hex).unwrap(); - syscall!(self - .trussed - .locate_file(Location::Internal, Some(dir), filename,)) - .path + let mut maybe_entry = + try_syscall!(self.trussed.read_dir_first(Location::Internal, dir, None)) + .ok()? + .entry; + while let Some(entry) = maybe_entry { + if entry.file_name().as_str().ends_with(&hex_str) { + return Some(entry.path().into()); + } + maybe_entry = syscall!(self.trussed.read_dir_next()).entry; + } + None } pub fn delete_credential( diff --git a/src/lib.rs b/src/lib.rs index ce9d949..80d3143 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ extern crate delog; generate_macros!(); +pub use state::migrate; + use core::time::Duration; use trussed::{client, syscall, types::Location, Client as TrussedClient}; @@ -150,13 +152,16 @@ where } // EWW.. this is a bit unsafe isn't it -fn format_hex(data: &[u8], mut buffer: &mut [u8]) { +fn format_hex<'a>(data: &[u8], buffer: &'a mut [u8]) -> &'a str { const HEX_CHARS: &[u8] = b"0123456789abcdef"; - for byte in data.iter() { - buffer[0] = HEX_CHARS[(byte >> 4) as usize]; - buffer[1] = HEX_CHARS[(byte & 0xf) as usize]; - buffer = &mut buffer[2..]; + assert!(data.len() * 2 >= buffer.len()); + for (idx, byte) in data.iter().enumerate() { + buffer[idx * 2] = HEX_CHARS[(byte >> 4) as usize]; + buffer[idx * 2 + 1] = HEX_CHARS[(byte & 0xf) as usize]; } + + // SAFETY: we just added only ascii chars to buffer from 0 to data.len() - 1 + unsafe { core::str::from_utf8_unchecked(&buffer[0..data.len() * 2]) } } // NB: to actually use this, replace the constant implementation with the inline assembly. @@ -323,4 +328,14 @@ where } #[cfg(test)] -mod test {} +mod test { + use super::*; + + #[test] + fn hex() { + let data = [0x01, 0x02, 0xB1, 0xA1]; + let buffer = &mut [0; 8]; + assert_eq!(format_hex(&data, buffer), "0102b1a1"); + assert_eq!(buffer, b"0102b1a1"); + } +} diff --git a/src/state.rs b/src/state.rs index 38c4095..17112d3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,6 +2,10 @@ //! //! Needs cleanup. +pub mod migrate; + +use core::num::NonZeroU32; + use ctap_types::{ ctap2::AttestationFormatsPreference, // 2022-02-27: 10 credentials @@ -199,7 +203,7 @@ impl Identity { #[derive(Clone, Debug, Eq, PartialEq)] pub struct CredentialManagementEnumerateRps { - pub remaining: u32, + pub remaining: NonZeroU32, pub rp_id_hash: [u8; 32], } @@ -207,7 +211,9 @@ pub struct CredentialManagementEnumerateRps { pub struct CredentialManagementEnumerateCredentials { pub remaining: u32, pub rp_dir: PathBuf, - pub prev_filename: PathBuf, + /// None means that we finished iterating over the legacy credentials, + /// and are starting to iterate over the legacy credentials + pub prev_filename: Option, } #[derive(Clone, Debug, Default)] @@ -250,7 +256,7 @@ pub struct RuntimeState { // Currently, this causes the entire authnr to reset state. Maybe it should even reformat disk // // - An alternative would be `heapless::Map`, but I'd prefer something more typed. -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Default)] pub struct PersistentState { #[serde(skip)] // TODO: there has to be a better way than.. this @@ -287,15 +293,15 @@ impl PersistentState { let data = result.unwrap().data; - let result = trussed::cbor_deserialize(&data); - - if result.is_err() { - info!("err deser'ing: {:?}", result.err().unwrap()); + let state: Self = trussed::cbor_deserialize(&data).map_err(|_err| { + info!("err deser'ing: {_err:?}",); info!("{}", hex_str!(&data)); - return Err(Error::Other); - } + Error::Other + })?; + + debug!("Loaded state: {state:#?}"); - result.map_err(|_| Error::Other) + Ok(state) } pub fn save(&self, trussed: &mut T) -> Result<()> { diff --git a/src/state/migrate.rs b/src/state/migrate.rs new file mode 100644 index 0000000..b65040d --- /dev/null +++ b/src/state/migrate.rs @@ -0,0 +1,278 @@ +use littlefs2_core::{path, DirEntry, DynFilesystem, Error, Path, PathBuf}; + +fn ignore_does_not_exists(error: Error) -> Result<(), Error> { + if matches!(error, Error::NO_SUCH_ENTRY) { + return Ok(()); + } + Err(error) +} + +/// Migration function, to be used with trussed-staging's `migrate` management syscall +/// +/// `base_path` must be the base of the file directory of the fido app (often `/fido/dat`) +pub fn migrate_no_rp_dir(fs: &dyn DynFilesystem, base_path: &Path) -> Result<(), Error> { + let rk_dir = base_path.join(path!("rk")); + + let mut res = Ok(()); + + fs.read_dir_and_then_unit(&rk_dir, &mut |dir| { + res = migrate_rk_dir(fs, &rk_dir, dir); + Ok(()) + }) + .or_else(ignore_does_not_exists)?; + + res +} + +fn migrate_rk_dir( + fs: &dyn DynFilesystem, + rk_dir: &Path, + dir: &mut dyn Iterator>, +) -> Result<(), Error> { + for rp in dir.skip(2) { + let rp = rp?; + if rp.metadata().is_file() { + continue; + } + + migrate_rp_dir(fs, rk_dir, rp.path())?; + } + Ok(()) +} + +fn migrate_rp_dir( + fs: &dyn DynFilesystem, + rk_dir: &Path, + rp_path: &Path, +) -> trussed::types::LfsResult<()> { + let rp_id_hex = rp_path.file_name().unwrap().as_str(); + debug_assert_eq!(rp_id_hex.len(), 16); + + fs.read_dir_and_then(rp_path, &mut |rp_dir| { + for file in rp_dir.skip(2) { + let file = file?; + let cred_id_hex = file.file_name().as_str(); + let mut buf = [0; 33]; + buf[0..16].copy_from_slice(rp_id_hex.as_bytes()); + buf[16] = b'.'; + buf[17..].copy_from_slice(cred_id_hex.as_bytes()); + fs.rename( + file.path(), + &rk_dir.join(&PathBuf::try_from(buf.as_slice()).unwrap()), + )?; + } + Ok(()) + })?; + + fs.remove_dir(rp_path)?; + + Ok(()) +} + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use admin_app::migrations::test_utils::{test_migration_one, FsValues}; + + use super::*; + + const FIDO_DAT_DIR_BEFORE: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[( + path!("74a6ea9213c99c2f"), + FsValues::Dir(&[ + (path!("038dfc6165b78be9"), FsValues::File(128)), + (path!("1ecbbfbed8992287"), FsValues::File(122)), + (path!("7c24db95312eac56"), FsValues::File(122)), + (path!("978cba44dfe39871"), FsValues::File(155)), + (path!("ac889a0433749726"), FsValues::File(138)), + ]), + )]), + ), + ]); + + const FIDO_DAT_DIR_AFTER: FsValues = FsValues::Dir(&[ + (path!("persistent-state.cbor"), FsValues::File(137)), + ( + path!("rk"), + FsValues::Dir(&[ + ( + path!("74a6ea9213c99c2f.038dfc6165b78be9"), + FsValues::File(128), + ), + ( + path!("74a6ea9213c99c2f.1ecbbfbed8992287"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.7c24db95312eac56"), + FsValues::File(122), + ), + ( + path!("74a6ea9213c99c2f.978cba44dfe39871"), + FsValues::File(155), + ), + ( + path!("74a6ea9213c99c2f.ac889a0433749726"), + FsValues::File(138), + ), + ]), + ), + ]); + + const FIDO_SEC_DIR: FsValues = FsValues::Dir(&[ + ( + path!("069386c3c735689061ac51b8bca9f160"), + FsValues::File(48), + ), + ( + path!("233d86bfc2f196ff7c108cf23a282bd5"), + FsValues::File(36), + ), + ( + path!("2bdef14a0e18d28191162f8c1599d598"), + FsValues::File(36), + ), + ( + path!("3efe6394c20aa8128e27b376e226a58b"), + FsValues::File(36), + ), + ( + path!("4711aa79b4834ef8e551f80e523ba8d2"), + FsValues::File(36), + ), + ( + path!("b43bf8b7897087b7195b8ac53dcb5f11"), + FsValues::File(36), + ), + ]); + + #[test] + fn migration_no_auth() { + const TEST_VALUES_BEFORE: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + const TEST_VALUES_AFTER: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + + test_migration_one(&TEST_VALUES_BEFORE, &TEST_VALUES_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_auth() { + const AUTH_SECRETS_DIR: (&Path, FsValues) = ( + path!("secrets"), + FsValues::Dir(&[( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[ + (path!("application_salt"), FsValues::File(16)), + (path!("pin.00"), FsValues::File(118)), + ]), + )]), + )]), + ); + + const BACKEND_DIR: (&Path, FsValues) = ( + path!("backend-auth"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("salt"), FsValues::File(16))]), + )]), + ); + + const TRUSSED_DIR: (&Path, FsValues) = ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ); + + const TEST_BEFORE: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_BEFORE), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + const TEST_AFTER: FsValues = FsValues::Dir(&[ + BACKEND_DIR, + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FIDO_DAT_DIR_AFTER), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + AUTH_SECRETS_DIR, + TRUSSED_DIR, + ]); + + test_migration_one(&TEST_BEFORE, &TEST_AFTER, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } + + #[test] + fn migration_empty() { + const TEST_VALUES: FsValues = FsValues::Dir(&[ + ( + path!("fido"), + FsValues::Dir(&[ + (path!("dat"), FsValues::Dir(&[])), + (path!("sec"), FIDO_SEC_DIR), + ]), + ), + ( + path!("trussed"), + FsValues::Dir(&[( + path!("dat"), + FsValues::Dir(&[(path!("rng-state.bin"), FsValues::File(32))]), + )]), + ), + ]); + test_migration_one(&TEST_VALUES, &TEST_VALUES, |fs| { + migrate_no_rp_dir(fs, path!("fido/dat")) + }); + } +} diff --git a/test_fs/fido-trussed-auth.lfs b/test_fs/fido-trussed-auth.lfs new file mode 100644 index 0000000..f3e16fd Binary files /dev/null and b/test_fs/fido-trussed-auth.lfs differ diff --git a/test_fs/fido-trussed.lfs b/test_fs/fido-trussed.lfs new file mode 100644 index 0000000..ca6495c Binary files /dev/null and b/test_fs/fido-trussed.lfs differ