fido_authenticator/ctap2/
large_blobs.rs

1use 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;
19// empty CBOR array (0x80) + hash
20const 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    /// The location for storing the large-blob array.
32    pub location: Location,
33    /// The maximum size for the large-blob array including metadata.
34    ///
35    /// This value must be at least 1024 according to the CTAP2.1 spec.  Without the chunking
36    /// extension, it cannot be larger than 1024 because the large-blob array must fit into a
37    /// Trussed message.  Therefore, this setting is only available if the chunked feature is
38    /// enabled.
39    #[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            // If the data is shorter than MIN_SIZE, it is missing or corrupted and we fall back to
65            // an empty array which has exactly MIN_SIZE
66            .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    // sanity checks
102    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// Basic implementation using a file in the volatile storage as a buffer based on the core Trussed
159// API.  Maximum size for the entire large blob array: 1024 bytes.
160#[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        // sanity checks
199        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            // The stored file is missing or too short, so we fall back to an empty array.
265            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}