Skip to main content

coven_format/
file.rs

1//! The cleartext file header, indexed ciphertext chunks and authenticated ranges.
2
3use crate::chunks::{chunk_length, chunk_start, CHUNK_SIZE};
4use crate::error::{exact_length, require, Error, Rule};
5use coven_crypto::{CryptoError, FileKey, FILE_CHUNK_TAG_LEN};
6
7/// Bytes before the first file chunk: kind, version and file size.
8pub const FILE_HEADER_LEN: usize = 11;
9
10/// Validated cleartext header. It is authenticated by every file chunk.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct FileHeader {
13    size: u64,
14}
15
16impl FileHeader {
17    /// Describe a file of this plaintext size, in fixed 64-KiB chunks.
18    pub fn new(size: u64) -> Self {
19        Self { size }
20    }
21
22    /// Decode exactly the 11-byte header, without reading any ciphertext.
23    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
24        crate::sealed::prefix(bytes, crate::kinds::FILE)?;
25        exact_length(bytes.len(), FILE_HEADER_LEN)?;
26        Ok(Self::new(u64::from_be_bytes(bytes[3..11].try_into().expect("eight bytes"))))
27    }
28
29    /// Encode kind, version and size, all integers big-endian.
30    pub fn encode(&self) -> [u8; FILE_HEADER_LEN] {
31        let mut bytes = [0; FILE_HEADER_LEN];
32        bytes[..3].copy_from_slice(&crate::FormatVersion::CURRENT.prefix(crate::kinds::FILE));
33        bytes[3..].copy_from_slice(&self.size.to_be_bytes());
34        bytes
35    }
36
37    /// The file's complete plaintext size.
38    pub fn size(&self) -> u64 {
39        self.size
40    }
41
42    /// How many chunks follow this header; an empty file has none.
43    pub fn chunk_count(&self) -> u64 {
44        crate::chunks::chunk_count(self.size)
45    }
46
47    /// Full stored size, refusing an offset that cannot be represented in u64.
48    pub fn encrypted_size(&self) -> Result<u64, Error> {
49        self.chunk_count()
50            .checked_mul(FILE_CHUNK_TAG_LEN as u64)
51            .and_then(|tags| tags.checked_add(self.size))
52            .and_then(|body| body.checked_add(FILE_HEADER_LEN as u64))
53            .ok_or_else(offset_error)
54    }
55
56    /// The exact storage offset and plaintext length of a particular chunk.
57    pub fn chunk(&self, index: u64) -> Result<FileChunk, Error> {
58        require(index < self.chunk_count(), "file chunk index", Rule::Chunk)?;
59        let offset = index
60            .checked_mul((CHUNK_SIZE + FILE_CHUNK_TAG_LEN) as u64)
61            .and_then(|n| n.checked_add(FILE_HEADER_LEN as u64))
62            .ok_or_else(offset_error)?;
63        let chunk = FileChunk { index, offset, plaintext_length: chunk_length(self.size, index) };
64        offset.checked_add(chunk.stored_length() as u64).ok_or_else(offset_error)?;
65        Ok(chunk)
66    }
67
68    /// The chunk whose stored bytes hold the object's byte `position`; a
69    /// position in the header or past the last chunk has none.
70    pub fn chunk_at(&self, position: u64) -> Result<FileChunk, Error> {
71        let past_header = position.checked_sub(FILE_HEADER_LEN as u64).ok_or_else(offset_error)?;
72        let chunk = self.chunk(past_header / (CHUNK_SIZE + FILE_CHUNK_TAG_LEN) as u64)?;
73        require(position < chunk.end(), "file chunk position", Rule::Chunk)?;
74        Ok(chunk)
75    }
76
77    /// Validate a chunk's plaintext length, then seal it under the file's key.
78    pub fn seal_chunk(
79        &self,
80        key: &FileKey,
81        path: &str,
82        index: u64,
83        plaintext: &[u8],
84    ) -> Result<Vec<u8>, FileError> {
85        let chunk = self.chunk(index)?;
86        require(plaintext.len() == chunk.plaintext_length, "file chunk length", Rule::Chunk)?;
87        Ok(key.seal_chunk(path, &self.encode(), index, plaintext))
88    }
89
90    /// Validate a chunk's stored length before authenticating or allocating.
91    pub fn open_chunk(
92        &self,
93        key: &FileKey,
94        path: &str,
95        index: u64,
96        bytes: &[u8],
97    ) -> Result<Vec<u8>, FileError> {
98        let chunk = self.chunk(index)?;
99        exact_length(bytes.len(), chunk.stored_length())?;
100        Ok(key.open_chunk(path, &self.encode(), index, bytes)?)
101    }
102}
103
104/// One file chunk's coordinates, computed from its header.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct FileChunk {
107    /// Zero-based index; also the chunk's nonce.
108    pub index: u64,
109    /// Byte offset in the stored object, including its 11-byte header.
110    pub offset: u64,
111    /// Exact plaintext length; the stored length adds a 16-byte tag.
112    pub plaintext_length: usize,
113}
114impl FileChunk {
115    /// Where the chunk's plaintext begins in the file.
116    pub fn plaintext_start(&self) -> u64 {
117        chunk_start(self.index)
118    }
119
120    /// Its stored length: the plaintext and the 16-byte tag.
121    pub fn stored_length(&self) -> usize {
122        self.plaintext_length + FILE_CHUNK_TAG_LEN
123    }
124
125    /// The stored object's byte just past this chunk.
126    pub fn end(&self) -> u64 {
127        self.offset + self.stored_length() as u64
128    }
129}
130
131fn offset_error() -> Error {
132    Error::Invalid { field: "file offset", rule: Rule::Chunk }
133}
134
135/// A malformed file layout or a chunk that failed authentication.
136#[derive(Debug)]
137pub enum FileError {
138    /// The range, header or chunk length is invalid.
139    Format(Error),
140    /// The key, path, header, index or ciphertext did not authenticate.
141    Crypto(CryptoError),
142}
143impl From<Error> for FileError {
144    fn from(error: Error) -> Self {
145        Self::Format(error)
146    }
147}
148impl From<CryptoError> for FileError {
149    fn from(error: CryptoError) -> Self {
150        Self::Crypto(error)
151    }
152}
153impl std::fmt::Display for FileError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            Self::Format(e) => e.fmt(f),
157            Self::Crypto(e) => e.fmt(f),
158        }
159    }
160}
161impl std::error::Error for FileError {
162    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
163        match self {
164            Self::Format(e) => Some(e),
165            Self::Crypto(e) => Some(e),
166        }
167    }
168}
169
170#[cfg(test)]
171#[path = "file_tests.rs"]
172mod tests;