fido_authenticator/ctap2/
large_blobs.rs1use ctap_types::{sizes::LARGE_BLOB_MAX_FRAGMENT_LENGTH, Error};
2use littlefs2_core::{path, Path, PathBuf};
3use trussed_core::{
4 config::MAX_MESSAGE_LENGTH,
5 try_syscall,
6 types::{Bytes, Location, Message},
7 FilesystemClient,
8};
9
10#[cfg(feature = "chunked")]
11use trussed_chunked::ChunkedClient;
12#[cfg(not(feature = "chunked"))]
13use trussed_core::{mechanisms::Sha256, syscall, types::Mechanism, CryptoClient};
14
15use crate::{Result, TrussedRequirements};
16
17const HASH_SIZE: usize = 16;
18pub const MIN_SIZE: usize = HASH_SIZE + 1;
19const EMPTY_ARRAY: &[u8; MIN_SIZE] = &[
21 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d,
22 0x3c,
23];
24const FILENAME: &Path = path!("large-blob-array");
25const FILENAME_TMP: &Path = path!(".large-blob-array");
26
27pub type Chunk = Bytes<LARGE_BLOB_MAX_FRAGMENT_LENGTH>;
28
29#[derive(Copy, Clone, Debug, Eq, PartialEq)]
30pub struct Config {
31 pub location: Location,
33 #[cfg(feature = "chunked")]
40 pub max_size: usize,
41}
42
43impl Config {
44 pub fn max_size(&self) -> usize {
45 #[cfg(feature = "chunked")]
46 {
47 self.max_size
48 }
49
50 #[cfg(not(feature = "chunked"))]
51 {
52 MAX_MESSAGE_LENGTH
53 }
54 }
55}
56
57pub fn size<C: FilesystemClient>(client: &mut C, location: Location) -> Result<usize> {
58 Ok(
59 try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
60 .map_err(|_| Error::Other)?
61 .metadata
62 .map(|metadata| metadata.len())
63 .unwrap_or_default()
64 .max(MIN_SIZE),
67 )
68}
69
70pub fn read_chunk<C: TrussedRequirements>(
71 client: &mut C,
72 location: Location,
73 offset: usize,
74 length: usize,
75) -> Result<Chunk> {
76 SelectedStorage::read(client, location, offset, length)
77}
78
79pub fn write_chunk<C: TrussedRequirements>(
80 client: &mut C,
81 state: &mut State,
82 location: Location,
83 data: &[u8],
84) -> Result<()> {
85 write_impl::<_, SelectedStorage>(client, state, location, data)
86}
87
88pub fn reset<C: FilesystemClient>(client: &mut C) {
89 for location in [Location::Internal, Location::External, Location::Volatile] {
90 try_syscall!(client.remove_file(location, PathBuf::from(FILENAME))).ok();
91 }
92 try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok();
93}
94
95fn write_impl<C, S: Storage<C>>(
96 client: &mut C,
97 state: &mut State,
98 location: Location,
99 data: &[u8],
100) -> Result<()> {
101 if state.expected_next_offset + data.len() > state.expected_length {
103 return Err(Error::InvalidParameter);
104 }
105
106 let mut writer = S::start_write(
107 client,
108 location,
109 state.expected_next_offset,
110 state.expected_length,
111 )?;
112 state.expected_next_offset = writer.extend_buffer(client, data)?;
113 if state.expected_next_offset == state.expected_length {
114 if writer.validate_checksum(client)? {
115 writer.commit(client)
116 } else {
117 writer.abort(client)?;
118 Err(Error::IntegrityFailure)
119 }
120 } else {
121 Ok(())
122 }
123}
124
125#[derive(Clone, Debug, Default)]
126pub struct State {
127 pub expected_length: usize,
128 pub expected_next_offset: usize,
129}
130
131trait Storage<C>: Sized {
132 fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk>;
133
134 fn start_write(
135 client: &mut C,
136 location: Location,
137 offset: usize,
138 expected_length: usize,
139 ) -> Result<Self>;
140
141 fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize>;
142
143 fn validate_checksum(&mut self, client: &mut C) -> Result<bool>;
144
145 fn commit(&mut self, client: &mut C) -> Result<()>;
146
147 fn abort(&mut self, client: &mut C) -> Result<()> {
148 let _ = client;
149 Ok(())
150 }
151}
152
153#[cfg(not(feature = "chunked"))]
154type SelectedStorage = SimpleStorage;
155#[cfg(feature = "chunked")]
156type SelectedStorage = ChunkedStorage;
157
158#[cfg(not(feature = "chunked"))]
161struct SimpleStorage {
162 location: Location,
163 buffer: Message,
164}
165
166#[cfg(not(feature = "chunked"))]
167impl<C: CryptoClient + FilesystemClient + Sha256> Storage<C> for SimpleStorage {
168 fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
169 let result = try_syscall!(client.read_file(location, PathBuf::from(FILENAME)));
170 let data = if let Ok(reply) = &result {
171 reply.data.as_slice()
172 } else {
173 EMPTY_ARRAY.as_slice()
174 };
175 let Some(max_length) = data.len().checked_sub(offset) else {
176 return Err(Error::InvalidParameter);
177 };
178 let length = length.min(max_length);
179 let mut buffer = Chunk::new();
180 buffer.extend_from_slice(&data[offset..][..length]).unwrap();
181 Ok(buffer)
182 }
183
184 fn start_write(
185 client: &mut C,
186 location: Location,
187 offset: usize,
188 expected_length: usize,
189 ) -> Result<Self> {
190 let buffer = if offset == 0 {
191 Message::new()
192 } else {
193 try_syscall!(client.read_file(Location::Volatile, PathBuf::from(FILENAME_TMP)))
194 .map_err(|_| Error::Other)?
195 .data
196 };
197
198 if expected_length > buffer.capacity() {
200 return Err(Error::InvalidLength);
201 }
202 if buffer.len() != offset {
203 return Err(Error::Other);
204 }
205
206 Ok(Self { buffer, location })
207 }
208
209 fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
210 self.buffer
211 .extend_from_slice(data)
212 .map_err(|_| Error::InvalidParameter)?;
213 try_syscall!(client.write_file(
214 Location::Volatile,
215 PathBuf::from(FILENAME_TMP),
216 self.buffer.clone(),
217 None
218 ))
219 .map_err(|_| Error::Other)?;
220 Ok(self.buffer.len())
221 }
222
223 fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
224 let Some(n) = self.buffer.len().checked_sub(HASH_SIZE) else {
225 return Ok(false);
226 };
227 let mut message = Message::new();
228 message.extend_from_slice(&self.buffer[..n]).unwrap();
229 let checksum = syscall!(client.hash(Mechanism::Sha256, message)).hash;
230 Ok(checksum[..HASH_SIZE] == self.buffer[n..])
231 }
232
233 fn commit(&mut self, client: &mut C) -> Result<()> {
234 try_syscall!(client.write_file(
235 self.location,
236 PathBuf::from(FILENAME),
237 self.buffer.clone(),
238 None
239 ))
240 .map_err(|_| Error::Other)?;
241 try_syscall!(client.remove_file(Location::Volatile, PathBuf::from(FILENAME_TMP))).ok();
242 Ok(())
243 }
244}
245
246#[cfg(feature = "chunked")]
247struct ChunkedStorage {
248 location: Location,
249 expected_length: usize,
250 create_file: bool,
251}
252
253#[cfg(feature = "chunked")]
254impl<C: ChunkedClient + FilesystemClient> Storage<C> for ChunkedStorage {
255 fn read(client: &mut C, location: Location, offset: usize, length: usize) -> Result<Chunk> {
256 debug!("ChunkedStorage::read: offset = {offset}, length = {length}");
257 let mut chunk = Chunk::new();
258 let file_size = try_syscall!(client.entry_metadata(location, PathBuf::from(FILENAME)))
259 .map_err(|_| Error::Other)?
260 .metadata
261 .map(|metadata| metadata.len())
262 .unwrap_or_default();
263 if file_size < MIN_SIZE {
264 trace!("Sending empty array instead of missing or corrupted file");
266 let start = offset.min(MIN_SIZE);
267 let end = (offset + length).min(MIN_SIZE);
268 chunk.extend_from_slice(&EMPTY_ARRAY[start..end]).unwrap();
269 return Ok(chunk);
270 }
271
272 while offset + chunk.len() < offset + length {
273 let n = MAX_MESSAGE_LENGTH.min(length - chunk.len());
274 let reply = try_syscall!(client.partial_read_file(
275 location,
276 PathBuf::from(FILENAME),
277 offset + chunk.len(),
278 n
279 ))
280 .map_err(|_| Error::Other)?;
281 chunk
282 .extend_from_slice(&reply.data)
283 .map_err(|_| Error::Other)?;
284 if offset + chunk.len() >= reply.file_length {
285 break;
286 }
287 }
288
289 trace!("Read chunk with {} bytes", chunk.len());
290 Ok(chunk)
291 }
292
293 fn start_write(
294 _client: &mut C,
295 location: Location,
296 offset: usize,
297 expected_length: usize,
298 ) -> Result<Self> {
299 debug!(
300 "ChunkedStorage::start_write: offset = {offset}, expected_length = {expected_length}"
301 );
302 let create_file = offset == 0;
303 Ok(ChunkedStorage {
304 location,
305 create_file,
306 expected_length,
307 })
308 }
309
310 fn extend_buffer(&mut self, client: &mut C, data: &[u8]) -> Result<usize> {
311 debug!("ChunkedStorage::extend_buffer: |data| = {}", data.len());
312 let mut n = 0;
313 for chunk in data.chunks(trussed_core::config::MAX_MESSAGE_LENGTH) {
314 trace!("Writing {} bytes", chunk.len());
315 let path = PathBuf::from(FILENAME_TMP);
316 let mut message = Message::new();
317 message.extend_from_slice(chunk).unwrap();
318 if self.create_file {
319 try_syscall!(client.write_file(self.location, path, message, None)).map_err(
320 |_err| {
321 error!("failed to write initial chunk: {_err:?}");
322 Error::Other
323 },
324 )?;
325 self.create_file = false;
326 n = data.len();
327 } else {
328 n = try_syscall!(client.append_file(self.location, path, message))
329 .map(|reply| reply.file_length)
330 .map_err(|_err| {
331 error!("failed to append chunk: {_err:?}");
332 Error::Other
333 })?;
334 }
335 }
336 Ok(n)
337 }
338
339 fn validate_checksum(&mut self, client: &mut C) -> Result<bool> {
340 use sha2::{digest::Digest as _, Sha256};
341
342 debug!("ChunkedStorage::validate_checksum");
343
344 let mut digest = Sha256::new();
345 let mut received_hash: Bytes<HASH_SIZE> = Bytes::new();
346 let mut bytes_read = 0;
347
348 let (mut chunk, mut len) =
349 try_syscall!(client.start_chunked_read(self.location, PathBuf::from(FILENAME_TMP)))
350 .map(|reply| (reply.data, reply.len))
351 .map_err(|_err| {
352 error!("Failed to read file: {:?}", _err);
353 Error::Other
354 })?;
355 loop {
356 trace!("read chunk: {}", chunk.len());
357
358 let remaining_data = self
359 .expected_length
360 .saturating_sub(bytes_read)
361 .saturating_sub(HASH_SIZE);
362 let data_end = remaining_data.min(chunk.len());
363 digest.update(&chunk[..data_end]);
364 if received_hash
365 .extend_from_slice(&chunk[data_end..chunk.len()])
366 .is_err()
367 {
368 return Ok(false);
369 }
370
371 bytes_read += chunk.len();
372 if bytes_read >= len {
373 break;
374 }
375
376 (chunk, len) = try_syscall!(client.read_file_chunk())
377 .map(|reply| (reply.data, reply.len))
378 .map_err(|_err| {
379 error!("Failed to read chunk: {:?}", _err);
380 Error::Other
381 })?;
382 }
383
384 let actual_hash = digest.finalize();
385 Ok(bytes_read == self.expected_length
386 && received_hash.as_slice() == &actual_hash[..HASH_SIZE])
387 }
388
389 fn commit(&mut self, client: &mut C) -> Result<()> {
390 debug!("ChunkedStorage::commit");
391 try_syscall!(client.rename(
392 self.location,
393 PathBuf::from(FILENAME_TMP),
394 PathBuf::from(FILENAME)
395 ))
396 .map_err(|_| Error::Other)?;
397 Ok(())
398 }
399
400 fn abort(&mut self, client: &mut C) -> Result<()> {
401 debug!("ChunkedStorage::abort");
402 try_syscall!(client.remove_file(self.location, PathBuf::from(FILENAME_TMP)))
403 .map_err(|_| Error::Other)?;
404 Ok(())
405 }
406}