lfsx_server/storage/
crypt.rs1use chacha20poly1305::aead::{Aead, KeyInit, Payload};
2use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
3
4use crate::error::Error;
5
6pub const KEY: usize = 32;
13pub const SALT: usize = 16;
14pub const ID: usize = 4;
15pub const TAG: u64 = 16;
16
17pub type KeyId = [u8; ID];
22
23pub struct Keyring {
24 keys: Vec<([u8; KEY], KeyId)>,
28}
29
30impl Keyring {
31 pub fn load(path: &std::path::Path) -> Result<Self, Error> {
32 let contents = std::fs::read_to_string(path).map_err(|error| {
33 Error::Storage(std::io::Error::other(format!(
34 "the encryption key file at {} could not be read: {error}",
35 path.display()
36 )))
37 })?;
38
39 Self::parse(&contents)
40 }
41
42 pub(super) fn parse(contents: &str) -> Result<Self, Error> {
43 let mut keys: Vec<([u8; KEY], KeyId)> = Vec::new();
44
45 for line in contents.lines() {
46 let line = line.trim();
47 if line.is_empty() || line.starts_with('#') {
48 continue;
49 }
50
51 let raw = hex::decode(line)
52 .ok()
53 .filter(|raw| raw.len() == KEY)
54 .ok_or(Error::Misconfigured(
55 "an encryption key must be 32 bytes as 64 hex characters, one key per line",
56 ))?;
57
58 let mut key = [0u8; KEY];
59 key.copy_from_slice(&raw);
60 let id = identify(&key);
61
62 if keys.iter().any(|(_, known)| *known == id) {
67 return Err(Error::Misconfigured(
68 "two encryption keys hash to the same id — the same key is probably listed twice",
69 ));
70 }
71
72 keys.push((key, id));
73 }
74
75 if keys.is_empty() {
76 return Err(Error::Misconfigured(
77 "the encryption key file holds no keys",
78 ));
79 }
80
81 Ok(Self { keys })
82 }
83
84 pub fn writing(&self) -> ObjectKey {
85 let (key, id) = &self.keys[0];
86
87 ObjectKey::derive(key, *id, random_salt())
88 }
89
90 pub fn reading(&self, id: KeyId, salt: [u8; SALT]) -> Result<ObjectKey, Error> {
91 self.keys
92 .iter()
93 .find(|(_, known)| *known == id)
94 .map(|(key, id)| ObjectKey::derive(key, *id, salt))
95 .ok_or(Error::UnknownKey)
96 }
97}
98
99fn identify(key: &[u8; KEY]) -> KeyId {
100 let mut id = [0u8; ID];
101 id.copy_from_slice(&blake3::hash(key).as_bytes()[..ID]);
102 id
103}
104
105fn random_salt() -> [u8; SALT] {
106 let mut salt = [0u8; SALT];
107 getrandom::fill(&mut salt).expect("the operating system has a random number generator");
108 salt
109}
110
111pub struct ObjectKey {
117 cipher: ChaCha20Poly1305,
118 id: KeyId,
119 salt: [u8; SALT],
120}
121
122const CONTEXT: &str = "LFSX 2026-08-16 object encryption key";
123
124impl ObjectKey {
125 fn derive(master: &[u8; KEY], id: KeyId, salt: [u8; SALT]) -> Self {
126 let mut hasher = blake3::Hasher::new_derive_key(CONTEXT);
127 hasher.update(master);
128 hasher.update(&salt);
129 let derived = hasher.finalize();
130
131 Self {
132 cipher: ChaCha20Poly1305::new(&Key::from(*derived.as_bytes())),
133 id,
134 salt,
135 }
136 }
137
138 pub fn id(&self) -> KeyId {
139 self.id
140 }
141
142 pub fn salt(&self) -> [u8; SALT] {
143 self.salt
144 }
145
146 pub fn seal(&self, frame: u32, last: bool, oid: &str, plain: &[u8]) -> Result<Vec<u8>, Error> {
147 self.cipher
148 .encrypt(
149 &nonce(frame),
150 Payload {
151 msg: plain,
152 aad: &associated(frame, last, oid),
153 },
154 )
155 .map_err(|_| Error::Storage(std::io::Error::other("a frame could not be encrypted")))
156 }
157
158 pub fn open(&self, frame: u32, last: bool, oid: &str, sealed: &[u8]) -> Result<Vec<u8>, Error> {
159 self.cipher
160 .decrypt(
161 &nonce(frame),
162 Payload {
163 msg: sealed,
164 aad: &associated(frame, last, oid),
165 },
166 )
167 .map_err(|_| Error::Tampered)
168 }
169}
170
171fn nonce(frame: u32) -> Nonce {
172 let mut bytes = [0u8; 12];
173 bytes[8..].copy_from_slice(&frame.to_be_bytes());
174
175 Nonce::from(bytes)
176}
177
178fn associated(frame: u32, last: bool, oid: &str) -> Vec<u8> {
185 let mut aad = Vec::with_capacity(oid.len() + 5);
186 aad.extend_from_slice(oid.as_bytes());
187 aad.extend_from_slice(&frame.to_le_bytes());
188 aad.push(u8::from(last));
189 aad
190}
191
192#[cfg(test)]
193mod tests;