Skip to main content

coven_format/
file_reference.rs

1//! The fixed file identity carried by a synced file row (format.md D12).
2
3use crate::{error::Rule, Error};
4use coven_crypto::{FileKey, SecretText};
5use coven_foundation::id_source::{DeviceId, ObjectId};
6use zeroize::Zeroizing;
7
8/// A file's fixed uploader, random storage name and decryption key.
9/// This identity carries neither upload status nor a device-local path.
10#[derive(Debug)]
11pub struct FileReference {
12    /// Device responsible for uploading the object.
13    pub device: DeviceId,
14    /// The stored object's random id, its name in storage (ยง17.5).
15    pub object: ObjectId,
16    /// Key carried only inside encrypted rows and writes.
17    pub key: FileKey,
18}
19
20#[cfg(test)]
21#[path = "file_reference_tests.rs"]
22mod tests;
23
24impl FileReference {
25    /// Decode canonical decimal device, UUID and lowercase hexadecimal key.
26    #[expect(clippy::map_err_ignore, reason = "a malformed reference is invalid as a whole")]
27    pub fn decode(text: &str) -> Result<Self, Error> {
28        let invalid = || Error::Invalid { field: "file reference", rule: Rule::KeyEncoding };
29        let rest = text.strip_prefix("file ").ok_or_else(invalid)?;
30        let (device, rest) = rest.split_once(' ').ok_or_else(invalid)?;
31        let (object, key) = rest.split_once(' ').ok_or_else(invalid)?;
32        let number = crate::path::canonical_decimal(device).ok_or_else(invalid)?;
33        let uuid = coven_foundation::id_source::canonical_uuid(object).map_err(|_| invalid())?;
34        let mut bytes = Zeroizing::new([0; 32]);
35        hex::decode_to_slice(key, bytes.as_mut()).map_err(|_| invalid())?;
36        if key.bytes().any(|b| b.is_ascii_uppercase()) {
37            return Err(invalid());
38        }
39        Ok(Self {
40            device: DeviceId(number),
41            object: ObjectId(uuid),
42            key: FileKey::from_bytes(*bytes),
43        })
44    }
45
46    /// Encode into secret text so diagnostics cannot reveal the file key.
47    pub fn encode(&self) -> SecretText {
48        let mut text = format!("file {} {} ", self.device.0, self.object);
49        let mut key = Zeroizing::new([0; 64]);
50        hex::encode_to_slice(self.key.to_secret_bytes().as_bytes(), key.as_mut())
51            .expect("64 hex digits for a 32-byte key");
52        text.push_str(std::str::from_utf8(key.as_ref()).expect("hex is ASCII"));
53        SecretText::new(text)
54    }
55}