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 from_source(source: &crate::config::KeySource) -> Result<Self, Error> {
32 match source {
33 crate::config::KeySource::File(path) => Self::load(path),
34 crate::config::KeySource::Command(hook) => Self::exec(hook),
35 }
36 }
37
38 pub fn load(path: &std::path::Path) -> Result<Self, Error> {
39 let contents = std::fs::read_to_string(path).map_err(|error| {
40 Error::Storage(std::io::Error::other(format!(
41 "the encryption key file at {} could not be read: {error}",
42 path.display()
43 )))
44 })?;
45
46 Self::parse(&contents)
47 }
48
49 fn exec(hook: &str) -> Result<Self, Error> {
56 let output = shell(hook).output().map_err(|error| {
57 Error::Storage(std::io::Error::other(format!(
58 "the encryption key command could not be run: {error}"
59 )))
60 })?;
61
62 if !output.status.success() {
63 return Err(Error::Storage(std::io::Error::other(format!(
64 "the encryption key command failed ({}): {}",
65 output.status,
66 String::from_utf8_lossy(&output.stderr).trim()
67 ))));
68 }
69
70 Self::parse(&String::from_utf8_lossy(&output.stdout))
71 }
72
73 pub(super) fn parse(contents: &str) -> Result<Self, Error> {
74 let mut keys: Vec<([u8; KEY], KeyId)> = Vec::new();
75
76 for line in contents.lines() {
77 let line = line.trim();
78 if line.is_empty() || line.starts_with('#') {
79 continue;
80 }
81
82 let raw = hex::decode(line)
83 .ok()
84 .filter(|raw| raw.len() == KEY)
85 .ok_or(Error::Misconfigured(
86 "an encryption key must be 32 bytes as 64 hex characters, one key per line",
87 ))?;
88
89 let mut key = [0u8; KEY];
90 key.copy_from_slice(&raw);
91 let id = identify(&key);
92
93 if keys.iter().any(|(_, known)| *known == id) {
98 return Err(Error::Misconfigured(
99 "two encryption keys hash to the same id: the same key is probably listed twice",
100 ));
101 }
102
103 keys.push((key, id));
104 }
105
106 if keys.is_empty() {
107 return Err(Error::Misconfigured(
108 "the encryption key file holds no keys",
109 ));
110 }
111
112 Ok(Self { keys })
113 }
114
115 pub fn writing(&self) -> ObjectKey {
116 let (key, id) = &self.keys[0];
117
118 ObjectKey::derive(key, *id, random_salt())
119 }
120
121 pub fn reading(&self, id: KeyId, salt: [u8; SALT]) -> Result<ObjectKey, Error> {
122 self.keys
123 .iter()
124 .find(|(_, known)| *known == id)
125 .map(|(key, id)| ObjectKey::derive(key, *id, salt))
126 .ok_or(Error::UnknownKey)
127 }
128}
129
130fn identify(key: &[u8; KEY]) -> KeyId {
131 let mut id = [0u8; ID];
132 id.copy_from_slice(&blake3::hash(key).as_bytes()[..ID]);
133 id
134}
135
136fn random_salt() -> [u8; SALT] {
137 let mut salt = [0u8; SALT];
138 getrandom::fill(&mut salt).expect("the operating system has a random number generator");
139 salt
140}
141
142pub struct ObjectKey {
148 cipher: ChaCha20Poly1305,
149 id: KeyId,
150 salt: [u8; SALT],
151}
152
153const CONTEXT: &str = "LFSX 2026-08-16 object encryption key";
154
155impl ObjectKey {
156 fn derive(master: &[u8; KEY], id: KeyId, salt: [u8; SALT]) -> Self {
157 let mut hasher = blake3::Hasher::new_derive_key(CONTEXT);
158 hasher.update(master);
159 hasher.update(&salt);
160 let derived = hasher.finalize();
161
162 Self {
163 cipher: ChaCha20Poly1305::new(&Key::from(*derived.as_bytes())),
164 id,
165 salt,
166 }
167 }
168
169 pub fn id(&self) -> KeyId {
170 self.id
171 }
172
173 pub fn salt(&self) -> [u8; SALT] {
174 self.salt
175 }
176
177 pub fn seal(&self, frame: u32, last: bool, oid: &str, plain: &[u8]) -> Result<Vec<u8>, Error> {
178 self.cipher
179 .encrypt(
180 &nonce(frame),
181 Payload {
182 msg: plain,
183 aad: &associated(frame, last, oid),
184 },
185 )
186 .map_err(|_| Error::Storage(std::io::Error::other("a frame could not be encrypted")))
187 }
188
189 pub fn open(&self, frame: u32, last: bool, oid: &str, sealed: &[u8]) -> Result<Vec<u8>, Error> {
190 self.cipher
191 .decrypt(
192 &nonce(frame),
193 Payload {
194 msg: sealed,
195 aad: &associated(frame, last, oid),
196 },
197 )
198 .map_err(|_| Error::Tampered)
199 }
200}
201
202fn nonce(frame: u32) -> Nonce {
203 let mut bytes = [0u8; 12];
204 bytes[8..].copy_from_slice(&frame.to_be_bytes());
205
206 Nonce::from(bytes)
207}
208
209fn associated(frame: u32, last: bool, oid: &str) -> Vec<u8> {
216 let mut aad = Vec::with_capacity(oid.len() + 5);
217 aad.extend_from_slice(oid.as_bytes());
218 aad.extend_from_slice(&frame.to_le_bytes());
219 aad.push(u8::from(last));
220 aad
221}
222
223#[cfg(test)]
224mod tests;
225
226#[cfg(unix)]
227fn shell(hook: &str) -> std::process::Command {
228 let mut command = std::process::Command::new("sh");
229 command.arg("-c").arg(hook);
230 command
231}
232
233#[cfg(windows)]
234fn shell(hook: &str) -> std::process::Command {
235 let mut command = std::process::Command::new("cmd");
236 command.arg("/C").arg(hook);
237 command
238}