use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error};
use littlefs2_core::{path, Path, PathBuf};
use trussed_core::{
config::MAX_MESSAGE_LENGTH,
try_syscall,
types::{Bytes, Location, Message},
FilesystemClient,
};
#[cfg(feature = "chunked")]
use trussed_chunked::ChunkedClient;
#[cfg(not(feature = "chunked"))]
use trussed_core::{mechanisms::Sha256, syscall, types::Mechanism, CryptoClient};
use crate::{Result, TrussedRequirements};
const HASH_SIZE: usize = 16;
pub const MIN_SIZE: usize = HASH_SIZE + 1;
const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[
0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d,
0x3c,
];
const FILENAME: &Path = path!("large-blob-array");
const FILENAME_TMP: &Path = path!(".large-blob-array");
pub type Chunk = Bytes<LARGE_BLOB_MAX_FRAGMENT_LENGTH>;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Config {
pub location: Location,
#[cfg(feature = "chunked")]
pub max_size: usize,
}
impl Config {
pub fn max_size(&self) -> usize {
#[cfg(feature = "chunked")]
{
self.max_size
}
#[cfg(not(feature = "chunked"))]
{
MAX_MESSAGE_LENGTH
}
}
}
pub fn size<C: FilesystemClient>(client: &mut C, location: Location) -> Result<usize> {
Ok(
try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
.map_err(|_| Error::Other)?
.metadata
.map(|metadata| metadata.len())
.unwrap_or_default()
.max(MIN_SIZE),
)
}
pub fn read_chunk<C: TrussedRequirements>(
client: &mut C,
location: Location,
offset: usize,
length: usize,
) -> Result<Chunk> {
SelectedStorage::read(client, location, offset, length)
}
pub fn write_chunk<C: TrussedRequirements>(
client: &mut C,
state: &mut State,
location: Location,
data: &[u8],
) -> Result<()> {
write_impl::<_, SelectedStorage>(client, state, location, data)
}
pub fn reset<C: FilesystemClient>(client: &mut C) {
for location in [Location::Internal, Location::External, Location::Volatile] {
try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok();
}
try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok();
}
fn write_impl<C, S: Storage<C>>(
client: &mut C,
state: &mut State,
location: Location,
data: &[u8],
) -> Result<()> {
if state.expected_next_offset + data.len() > state.expected_length {
return Err(Error::InvalidParameter);
}
let mut writer = S::start_write(
client,
location,
state.expected_next_offset,
state.expected_length,
)?;
state.expected_next_offset = writer.extend_buffer(client, data)?;
if state.expected_next_offset == state.expected_length {
if writer.validate_checksum(client)? {
writer.commit(client)
} else {
writer.abort(client)?;
Err(Error::IntegrityFailure)
}
} else {
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct State {
pub expected_length: usize,
pub expected_next_offset: usize,
}
trait Storage<C>: Sized {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk>;
fn start_write(
client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self>;
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize>;
fn validate_checksum(&mut self, client: &mut C) -> Result<bool>;
fn commit(&mut self, client: &mut C) -> Result<()>;
fn abort(&mut self, client: &mut C) -> Result<()> {
let _ = client;
Ok(())
}
}
#[cfg(not(feature = "chunked"))]
type SelectedStorage = SimpleStorage;
#[cfg(feature = "chunked")]
type SelectedStorage = ChunkedStorage;
#[cfg(not(feature = "chunked"))]
struct SimpleStorage {
location: Location,
buffer: Message,
}
#[cfg(not(feature = "chunked"))]
impl<C: CryptoClient + FilesystemClient + Sha256> Storage<C> for SimpleStorage {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME)));
let data = if let Ok(reply) = &result {
reply.data.as_slice()
} else {
EMPTY_ARRAY.as_slice()
};
let Some(max_length) = data.len().checked_sub(offset) else {
return Err(Error::InvalidParameter);
};
let length = length.min(max_length);
let mut buffer = Chunk::new();
buffer.extend_from_slice(&data[offset..][..length]).unwrap();
Ok(buffer)
}
fn start_write(
client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self> {
let buffer = if offset == 0 {
Message::new()
} else {
try_syscall!(client.read_file(Location::Volatile, PathBuf::from(FILENAME_TMP)))
.map_err(|_| Error::Other)?
.data
};
if expected_length > buffer.capacity() {
return Err(Error::InvalidLength);
}
if buffer.len() != offset {
return Err(Error::Other);
}
Ok(Self { buffer, location })
}
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
self.buffer
.extend_from_slice(data)
.map_err(|_| Error::InvalidParameter)?;
try_syscall!(client.write_file(
Location::Volatile,
PathBuf::from(FILENAME_TMP),
self.buffer.clone(),
None
))
.map_err(|_| Error::Other)?;
Ok(self.buffer.len())
}
fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else {
return Ok(false);
};
let mut message = Message::new();
message.extend_from_slice(&self.buffer[..n]).unwrap();
let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash;
Ok(checksum[..HASH_SIZE] == self.buffer[n..])
}
fn commit(&mut self, client: &mut C) -> Result<()> {
try_syscall!(client.write_file(
self.location,
PathBuf::from(FILENAME),
self.buffer.clone(),
None
))
.map_err(|_| Error::Other)?;
try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok();
Ok(())
}
}
#[cfg(feature = "chunked")]
struct ChunkedStorage {
location: Location,
expected_length: usize,
create_file: bool,
}
#[cfg(feature = "chunked")]
impl<C: ChunkedClient + FilesystemClient> Storage<C> for ChunkedStorage {
fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
debug!("ChunkedStorage::read: offset = {offset}, length = {length}");
let mut chunk = Chunk::new();
let file_size = try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
.map_err(|_| Error::Other)?
.metadata
.map(|metadata| metadata.len())
.unwrap_or_default();
if file_size < MIN_SIZE {
trace!("Sending empty array instead of missing or corrupted file");
let start = offset.min(MIN_SIZE);
let end = (offset + length).min(MIN_SIZE);
chunk.extend_from_slice(&EMPTY_ARRAY[start..end]).unwrap();
return Ok(chunk);
}
while offset + chunk.len() < offset + length {
let n = MAX_MESSAGE_LENGTH.min(length - chunk.len());
let reply = try_syscall!(client.partial_read_file(
location,
PathBuf::from(FILENAME),
offset + chunk.len(),
n
))
.map_err(|_| Error::Other)?;
chunk
.extend_from_slice(&reply.data)
.map_err(|_| Error::Other)?;
if offset + chunk.len() >= reply.file_length {
break;
}
}
trace!("Read chunk with {} bytes", chunk.len());
Ok(chunk)
}
fn start_write(
_client: &mut C,
location: Location,
offset: usize,
expected_length: usize,
) -> Result<Self> {
debug!(
"ChunkedStorage::start_write: offset = {offset}, expected_length = {expected_length}"
);
let create_file = offset == 0;
Ok(ChunkedStorage {
location,
create_file,
expected_length,
})
}
fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len());
let mut n = 0;
for chunk in data.chunks(trussed_core::config::MAX_MESSAGE_LENGTH) {
trace!("Writing {} bytes", chunk.len());
let path = PathBuf::from(FILENAME_TMP);
let mut message = Message::new();
message.extend_from_slice(chunk).unwrap();
if self.create_file {
try_syscall!(client.write_file(self.location, path, message, None)).map_err(
|_err| {
error!("failed to write initial chunk: {_err:?}");
Error::Other
},
)?;
self.create_file = false;
n = data.len();
} else {
n = try_syscall!(client.append_file(self.location, path, message))
.map(|reply| reply.file_length)
.map_err(|_err| {
error!("failed to append chunk: {_err:?}");
Error::Other
})?;
}
}
Ok(n)
}
fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
use sha2::{digest::Digest as _, Sha256};
debug!("ChunkedStorage::validate_checksum");
let mut digest = Sha256::new();
let mut received_hash: Bytes<HASH_SIZE> = Bytes::new();
let mut bytes_read = 0;
let (mut chunk, mut len) =
try_syscall!(client.start_chunked_read(self.location, PathBuf::from(FILENAME_TMP)))
.map(|reply| (reply.data, reply.len))
.map_err(|_err| {
error!("Failed to read file: {:?}", _err);
Error::Other
})?;
loop {
trace!("read chunk: {}", chunk.len());
let remaining_data = self
.expected_length
.saturating_sub(bytes_read)
.saturating_sub(HASH_SIZE);
let data_end = remaining_data.min(chunk.len());
digest.update(&chunk[..data_end]);
if received_hash
.extend_from_slice(&chunk[data_end..chunk.len()])
.is_err()
{
return Ok(false);
}
bytes_read += chunk.len();
if bytes_read >= len {
break;
}
(chunk, len) = try_syscall!(client.read_file_chunk())
.map(|reply| (reply.data, reply.len))
.map_err(|_err| {
error!("Failed to read chunk: {:?}", _err);
Error::Other
})?;
}
let actual_hash = digest.finalize();
Ok(bytes_read == self.expected_length
&& received_hash.as_slice() == &actual_hash[..HASH_SIZE])
}
fn commit(&mut self, client: &mut C) -> Result<()> {
debug!("ChunkedStorage::commit");
try_syscall!(client.rename(
self.location,
PathBuf::from(FILENAME_TMP),
PathBuf::from(FILENAME)
))
.map_err(|_| Error::Other)?;
Ok(())
}
fn abort(&mut self, client: &mut C) -> Result<()> {
debug!("ChunkedStorage::abort");
try_syscall!(client.remove_file(self.location, PathBuf::from(FILENAME_TMP)))
.map_err(|_| Error::Other)?;
Ok(())
}
}