-
Notifications
You must be signed in to change notification settings - Fork 293
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a high level persistent storage API
- Loading branch information
1 parent
4339600
commit 02d66ba
Showing
9 changed files
with
512 additions
and
210 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,367 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
mod keys; | ||
|
||
use crate::ctap::status_code::{Ctap2StatusCode, CtapResult}; | ||
use crate::ctap::PIN_AUTH_LENGTH; | ||
use alloc::boxed::Box; | ||
use alloc::vec::Vec; | ||
use core::cmp; | ||
|
||
pub type PersistIter<'a> = Box<dyn Iterator<Item = CtapResult<usize>> + 'a>; | ||
pub type PersistCredentialIter<'a> = Box<dyn Iterator<Item = CtapResult<(usize, Vec<u8>)>> + 'a>; | ||
|
||
/// Stores data that persists across reboots. | ||
/// | ||
/// This trait might get appended to with new versions of CTAP. | ||
/// | ||
/// To implement this trait, you have 2 options: | ||
/// - Implement all high level functions with default implementations, | ||
/// calling `unimplemented!` in the key-value accessors. | ||
/// When we update this trait in a new version, OpenSK will panic when calling any new functions. | ||
/// - Implement the key-value accessors, and special case as many default implemented high level | ||
/// functions as desired. | ||
/// When the trait gets extended, new features will silently work. | ||
/// Credentials still need keys to be identified by. | ||
pub trait Persist { | ||
/// Retrieves the value for a given key. | ||
fn find(&self, key: usize) -> CtapResult<Option<Vec<u8>>>; | ||
|
||
/// Inserts the value at the given key. | ||
/// | ||
/// Values up to a length of 1023 Byte must be supported. | ||
fn insert(&mut self, key: usize, value: &[u8]) -> CtapResult<()>; | ||
|
||
/// Removes a key, if present. | ||
fn remove(&mut self, key: usize) -> CtapResult<()>; | ||
|
||
/// Iterator for all present keys. | ||
fn iter(&self) -> CtapResult<PersistIter<'_>>; | ||
|
||
/// Checks consistency on boot, and if necessary fixes or initializes problems. | ||
fn init(&mut self) -> CtapResult<()> { | ||
if self.find(keys::RESET_COMPLETION)?.is_some() { | ||
self.reset()?; | ||
} | ||
// TODO don't forget to call, add other init functionality, e.g. from KeyStore | ||
Ok(()) | ||
} | ||
|
||
/// Returns the byte array representation of a stored credential. | ||
fn credential_bytes(&self, key: usize) -> CtapResult<Vec<u8>> { | ||
if !keys::CREDENTIALS.contains(&key) { | ||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
} | ||
self.find(key)? | ||
.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) | ||
} | ||
|
||
/// Writes a credential at the given key. | ||
fn write_credential_bytes(&mut self, key: usize, value: &[u8]) -> CtapResult<()> { | ||
if !keys::CREDENTIALS.contains(&key) { | ||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
} | ||
self.insert(key, value) | ||
} | ||
|
||
/// Removes a credential at the given key. | ||
fn remove_credential(&mut self, key: usize) -> CtapResult<()> { | ||
if !keys::CREDENTIALS.contains(&key) { | ||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
} | ||
self.remove(key) | ||
} | ||
|
||
/// Iterates all stored credentials. | ||
fn iter_credentials(&self) -> CtapResult<PersistCredentialIter<'_>> { | ||
Ok(Box::new(self.iter()?.filter_map(move |key| match key { | ||
Ok(k) => { | ||
if keys::CREDENTIALS.contains(&k) { | ||
match self.find(k) { | ||
Ok(Some(v)) => Some(Ok((k, v))), | ||
Ok(None) => None, | ||
Err(e) => Some(Err(e)), | ||
} | ||
} else { | ||
None | ||
} | ||
} | ||
Err(e) => Some(Err(e)), | ||
}))) | ||
} | ||
|
||
/// Returns a key where a new credential can be inserted. | ||
fn free_credential_key(&self) -> CtapResult<usize> { | ||
for key in keys::CREDENTIALS { | ||
if self.find(key)?.is_none() { | ||
return Ok(key); | ||
} | ||
} | ||
Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL) | ||
} | ||
|
||
/// Returns the global signature counter. | ||
fn global_signature_counter(&self) -> CtapResult<u32> { | ||
const INITIAL_SIGNATURE_COUNTER: u32 = 1; | ||
match self.find(keys::GLOBAL_SIGNATURE_COUNTER)? { | ||
None => Ok(INITIAL_SIGNATURE_COUNTER), | ||
Some(value) if value.len() == 4 => Ok(u32::from_ne_bytes(*array_ref!(&value, 0, 4))), | ||
Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Increments the global signature counter. | ||
fn incr_global_signature_counter(&mut self, increment: u32) -> CtapResult<()> { | ||
let old_value = self.global_signature_counter()?; | ||
// In hopes that servers handle the wrapping gracefully. | ||
let new_value = old_value.wrapping_add(increment); | ||
self.insert(keys::GLOBAL_SIGNATURE_COUNTER, &new_value.to_ne_bytes()) | ||
} | ||
|
||
/// Returns the PIN hash if defined. | ||
fn pin_hash(&self) -> CtapResult<Option<[u8; PIN_AUTH_LENGTH]>> { | ||
let pin_properties = match self.find(keys::PIN_PROPERTIES)? { | ||
None => return Ok(None), | ||
Some(pin_properties) => pin_properties, | ||
}; | ||
if pin_properties.len() != 1 + PIN_AUTH_LENGTH { | ||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
} | ||
Ok(Some(*array_ref![pin_properties, 1, PIN_AUTH_LENGTH])) | ||
} | ||
|
||
/// Returns the length of the currently set PIN if defined. | ||
#[cfg(feature = "config_command")] | ||
fn pin_code_point_length(&self) -> CtapResult<Option<u8>> { | ||
let pin_properties = match self.find(keys::PIN_PROPERTIES)? { | ||
None => return Ok(None), | ||
Some(pin_properties) => pin_properties, | ||
}; | ||
if pin_properties.is_empty() { | ||
return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
} | ||
Ok(Some(pin_properties[0])) | ||
} | ||
|
||
/// Sets the PIN hash and length. | ||
/// | ||
/// If it was already defined, it is updated. | ||
fn set_pin( | ||
&mut self, | ||
pin_hash: &[u8; PIN_AUTH_LENGTH], | ||
pin_code_point_length: u8, | ||
) -> CtapResult<()> { | ||
let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; | ||
pin_properties[0] = pin_code_point_length; | ||
pin_properties[1..].clone_from_slice(pin_hash); | ||
self.insert(keys::PIN_PROPERTIES, &pin_properties[..])?; | ||
// If power fails between these 2 transactions, PIN has to be set again. | ||
self.remove(keys::FORCE_PIN_CHANGE) | ||
} | ||
|
||
/// Returns the number of failed PIN attempts. | ||
fn pin_fails(&self) -> CtapResult<u8> { | ||
match self.find(keys::PIN_RETRIES)? { | ||
None => Ok(0), | ||
Some(value) if value.len() == 1 => Ok(value[0]), | ||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Decrements the number of remaining PIN retries. | ||
fn incr_pin_fails(&mut self) -> CtapResult<()> { | ||
let old_value = self.pin_fails()?; | ||
let new_value = old_value.saturating_add(1); | ||
self.insert(keys::PIN_RETRIES, &[new_value]) | ||
} | ||
|
||
/// Resets the number of remaining PIN retries. | ||
fn reset_pin_retries(&mut self) -> CtapResult<()> { | ||
self.remove(keys::PIN_RETRIES) | ||
} | ||
|
||
/// Returns the minimum PIN length, if stored. | ||
fn min_pin_length(&self) -> CtapResult<Option<u8>> { | ||
match self.find(keys::MIN_PIN_LENGTH)? { | ||
None => Ok(None), | ||
Some(value) if value.len() == 1 => Ok(Some(value[0])), | ||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Sets the minimum PIN length. | ||
#[cfg(feature = "config_command")] | ||
fn set_min_pin_length(&mut self, min_pin_length: u8) -> CtapResult<()> { | ||
self.insert(keys::MIN_PIN_LENGTH, &[min_pin_length]) | ||
} | ||
|
||
/// Returns the list of RP IDs that may read the minimum PIN length. | ||
fn min_pin_length_rp_ids_bytes(&self) -> CtapResult<Vec<u8>> { | ||
Ok(self | ||
.find(keys::MIN_PIN_LENGTH_RP_IDS)? | ||
.unwrap_or(Vec::new())) | ||
} | ||
|
||
/// Sets the list of RP IDs that may read the minimum PIN length. | ||
#[cfg(feature = "config_command")] | ||
fn set_min_pin_length_rp_ids(&mut self, min_pin_length_rp_ids_bytes: &[u8]) -> CtapResult<()> { | ||
self.insert(keys::MIN_PIN_LENGTH_RP_IDS, min_pin_length_rp_ids_bytes) | ||
} | ||
|
||
/// Reads the byte vector stored as the serialized large blobs array. | ||
/// | ||
/// If too few bytes exist at that offset, return the maximum number | ||
/// available. This includes cases of offset being beyond the stored array. | ||
/// | ||
/// If no large blob is committed to the store, get responds as if an empty | ||
/// CBOR array (0x80) was written, together with the 16 byte prefix of its | ||
/// SHA256, to a total length of 17 byte (which is the shortest legitimate | ||
/// large blob entry possible). | ||
fn get_large_blob_array( | ||
&self, | ||
mut offset: usize, | ||
byte_count: usize, | ||
) -> CtapResult<Option<Vec<u8>>> { | ||
let mut result = Vec::with_capacity(byte_count); | ||
for key in keys::LARGE_BLOB_SHARDS { | ||
if offset >= VALUE_LENGTH { | ||
offset = offset.saturating_sub(VALUE_LENGTH); | ||
continue; | ||
} | ||
let end = offset.saturating_add(byte_count - result.len()); | ||
let end = cmp::min(end, VALUE_LENGTH); | ||
let value = self.find(key)?.unwrap_or(Vec::new()); | ||
if key == keys::LARGE_BLOB_SHARDS.start && value.is_empty() { | ||
return Ok(None); | ||
} | ||
let end = cmp::min(end, value.len()); | ||
if end < offset { | ||
return Ok(Some(result)); | ||
} | ||
result.extend(&value[offset..end]); | ||
offset = offset.saturating_sub(VALUE_LENGTH); | ||
} | ||
Ok(Some(result)) | ||
} | ||
|
||
/// Sets a byte vector as the serialized large blobs array. | ||
fn commit_large_blob_array(&mut self, large_blob_array: &[u8]) -> CtapResult<()> { | ||
debug_assert!(large_blob_array.len() <= keys::LARGE_BLOB_SHARDS.len() * VALUE_LENGTH); | ||
let mut offset = 0; | ||
for key in keys::LARGE_BLOB_SHARDS { | ||
let cur_len = cmp::min(large_blob_array.len().saturating_sub(offset), VALUE_LENGTH); | ||
let slice = &large_blob_array[offset..][..cur_len]; | ||
if slice.is_empty() { | ||
self.remove(key)?; | ||
} else { | ||
self.insert(key, slice)?; | ||
} | ||
offset += cur_len; | ||
} | ||
Ok(()) | ||
} | ||
|
||
/// Resets persistent data, consistent with a CTAP reset. | ||
/// | ||
/// In particular, entries that are persistent across factory reset are not removed. | ||
fn reset(&mut self) -> CtapResult<()> { | ||
self.insert(keys::RESET_COMPLETION, &[])?; | ||
let mut removed_keys = Vec::new(); | ||
for key in self.iter()? { | ||
let key = key?; | ||
if key >= keys::NUM_PERSISTENT_KEYS && key != keys::RESET_COMPLETION { | ||
removed_keys.push(key); | ||
} | ||
} | ||
for key in removed_keys { | ||
self.remove(key)?; | ||
} | ||
self.remove(keys::RESET_COMPLETION) | ||
} | ||
|
||
/// Returns whether the PIN needs to be changed before its next usage. | ||
fn has_force_pin_change(&self) -> CtapResult<bool> { | ||
match self.find(keys::FORCE_PIN_CHANGE)? { | ||
None => Ok(false), | ||
Some(value) if value.is_empty() => Ok(true), | ||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Marks the PIN as outdated with respect to the new PIN policy. | ||
#[cfg(feature = "config_command")] | ||
fn force_pin_change(&mut self) -> CtapResult<()> { | ||
self.insert(keys::FORCE_PIN_CHANGE, &[]) | ||
} | ||
|
||
/// Returns whether enterprise attestation is enabled. | ||
#[cfg(feature = "config_command")] | ||
fn enterprise_attestation(&self) -> CtapResult<bool> { | ||
match self.find(keys::ENTERPRISE_ATTESTATION)? { | ||
None => Ok(false), | ||
Some(value) if value.is_empty() => Ok(true), | ||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Marks enterprise attestation as enabled. | ||
#[cfg(feature = "config_command")] | ||
fn enable_enterprise_attestation(&mut self) -> CtapResult<()> { | ||
// TODO | ||
// if self.attestation_store_get(&attestation_store::Id::Enterprise)?.is_none() { | ||
// return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); | ||
// } | ||
self.insert(keys::ENTERPRISE_ATTESTATION, &[]) | ||
} | ||
|
||
/// Returns whether alwaysUv is enabled. | ||
fn has_always_uv(&self) -> CtapResult<bool> { | ||
match self.find(keys::ALWAYS_UV)? { | ||
None => Ok(false), | ||
Some(value) if value.is_empty() => Ok(true), | ||
_ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), | ||
} | ||
} | ||
|
||
/// Enables alwaysUv, when disabled, and vice versa. | ||
#[cfg(feature = "config_command")] | ||
fn toggle_always_uv(&mut self) -> CtapResult<()> { | ||
if self.has_always_uv()? { | ||
Ok(self.remove(keys::ALWAYS_UV)?) | ||
} else { | ||
Ok(self.insert(keys::ALWAYS_UV, &[])?) | ||
} | ||
} | ||
} | ||
|
||
const VALUE_LENGTH: usize = 1023; | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use crate::api::customization::Customization; | ||
use crate::env::test::TestEnv; | ||
use crate::env::Env; | ||
|
||
#[test] | ||
fn test_max_large_blob_array_size() { | ||
let env = TestEnv::default(); | ||
|
||
assert!( | ||
env.customization().max_large_blob_array_size() | ||
<= VALUE_LENGTH * keys::LARGE_BLOB_SHARDS.len() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.