tessarikv_format/seal/
mod.rs1mod header;
17#[cfg(test)]
18mod tests;
19
20pub use header::SealHeader;
21
22use aes_gcm::Aes256Gcm;
23use aes_gcm::aead::{Aead, AeadInOut, KeyInit, Payload};
24use chacha20poly1305::ChaCha20Poly1305;
25use hkdf::Hkdf;
26use sha2::Sha256;
27use zeroize::Zeroizing;
28
29use crate::{Error, Result};
30
31pub const HEADER_BYTES: usize = 42;
33pub const TAG_BYTES: usize = 16;
35pub const ROOT_KEY_BYTES: usize = 32;
37
38const MAX_UNIT_BYTES: u64 = (1 << 36) - 32;
40
41const LABEL: &[u8] = b"tessarikv/sealed-file/v1/";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum Cipher {
47 Aes256Gcm,
49 ChaCha20Poly1305,
51}
52
53impl Cipher {
54 #[must_use]
56 pub fn preferred() -> Self {
57 if aes_instructions() {
58 Self::Aes256Gcm
59 } else {
60 Self::ChaCha20Poly1305
61 }
62 }
63
64 const fn code(self) -> u8 {
65 match self {
66 Self::Aes256Gcm => 1,
67 Self::ChaCha20Poly1305 => 2,
68 }
69 }
70
71 const fn from_code(code: u8) -> Option<Self> {
72 match code {
73 1 => Some(Self::Aes256Gcm),
74 2 => Some(Self::ChaCha20Poly1305),
75 _ => None,
76 }
77 }
78}
79
80#[cfg(target_arch = "x86_64")]
81fn aes_instructions() -> bool {
82 std::arch::is_x86_feature_detected!("aes")
83}
84
85#[cfg(target_arch = "aarch64")]
86fn aes_instructions() -> bool {
87 std::arch::is_aarch64_feature_detected!("aes")
88}
89
90#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
91fn aes_instructions() -> bool {
92 false
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[non_exhaustive]
98pub enum FileKind {
99 Table,
101 Log,
103 Manifest,
105 Options,
107 KeyCheck,
109}
110
111impl FileKind {
112 const fn label(self) -> &'static [u8] {
113 match self {
114 Self::Table => b"table",
115 Self::Log => b"log",
116 Self::Manifest => b"manifest",
117 Self::Options => b"options",
118 Self::KeyCheck => b"key-check",
119 }
120 }
121}
122
123pub struct RootKey(Zeroizing<[u8; ROOT_KEY_BYTES]>);
125
126impl RootKey {
127 #[must_use]
129 pub fn new(bytes: [u8; ROOT_KEY_BYTES]) -> Self {
130 Self(Zeroizing::new(bytes))
131 }
132}
133
134impl std::fmt::Debug for RootKey {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.write_str("RootKey(..)")
137 }
138}
139
140pub struct FileSealer {
142 aead: SealingAead,
143 context: Vec<u8>,
145}
146
147enum SealingAead {
148 Aes(Box<Aes256Gcm>),
149 ChaCha(Box<ChaCha20Poly1305>),
150}
151
152impl FileSealer {
153 #[must_use]
155 pub fn new(root: &RootKey, header: &SealHeader, kind: FileKind) -> Self {
156 let encoded = header.encode();
157 let mut label = Vec::with_capacity(LABEL.len().saturating_add(kind.label().len()));
158 label.extend_from_slice(LABEL);
159 label.extend_from_slice(kind.label());
160 let mut key = Zeroizing::new([0_u8; 32]);
161 let _ = Hkdf::<Sha256>::new(Some(&encoded), root.0.as_slice())
163 .expand(&label, key.as_mut_slice());
164 let aead = match header.cipher {
165 Cipher::Aes256Gcm => SealingAead::Aes(Box::new(Aes256Gcm::new((&*key).into()))),
166 Cipher::ChaCha20Poly1305 => {
167 SealingAead::ChaCha(Box::new(ChaCha20Poly1305::new((&*key).into())))
168 }
169 };
170 let mut context = Vec::with_capacity(HEADER_BYTES.saturating_add(label.len()));
171 context.extend_from_slice(&encoded);
172 context.extend_from_slice(&label);
173 Self { aead, context }
174 }
175
176 pub fn seal(&self, position: u64, plaintext: &[u8]) -> Result<Vec<u8>> {
185 let aad = self.aad(position);
186 let nonce = nonce(position);
187 let payload = Payload {
188 msg: plaintext,
189 aad: &aad,
190 };
191 match &self.aead {
192 SealingAead::Aes(aead) => aead.encrypt((&nonce).into(), payload),
193 SealingAead::ChaCha(aead) => aead.encrypt((&nonce).into(), payload),
194 }
195 .map_err(|_| too_large(plaintext.len()))
196 }
197
198 pub fn seal_in_place(&self, position: u64, buffer: &mut [u8]) -> Result<[u8; TAG_BYTES]> {
206 let aad = self.aad(position);
207 let nonce = nonce(position);
208 let len = buffer.len();
209 let tag = match &self.aead {
210 SealingAead::Aes(aead) => {
211 aead.encrypt_inout_detached((&nonce).into(), &aad, buffer.into())
212 }
213 SealingAead::ChaCha(aead) => {
214 aead.encrypt_inout_detached((&nonce).into(), &aad, buffer.into())
215 }
216 }
217 .map_err(|_| too_large(len))?;
218 let mut bytes = [0; TAG_BYTES];
219 bytes.copy_from_slice(&tag);
220 Ok(bytes)
221 }
222
223 pub fn open(&self, position: u64, sealed: &[u8]) -> Result<Vec<u8>> {
230 let aad = self.aad(position);
231 let nonce = nonce(position);
232 let payload = Payload {
233 msg: sealed,
234 aad: &aad,
235 };
236 match &self.aead {
237 SealingAead::Aes(aead) => aead.decrypt((&nonce).into(), payload),
238 SealingAead::ChaCha(aead) => aead.decrypt((&nonce).into(), payload),
239 }
240 .map_err(|_| Error::Unauthenticated {
241 what: "sealed unit",
242 })
243 }
244
245 fn aad(&self, position: u64) -> Vec<u8> {
246 let mut aad = Vec::with_capacity(self.context.len().saturating_add(8));
247 aad.extend_from_slice(&self.context);
248 aad.extend_from_slice(&position.to_le_bytes());
249 aad
250 }
251}
252
253impl std::fmt::Debug for FileSealer {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.write_str("FileSealer(..)")
256 }
257}
258
259fn too_large(len: usize) -> Error {
260 Error::TooLarge {
261 what: "sealed unit",
262 size: u64::try_from(len).unwrap_or(u64::MAX),
263 limit: MAX_UNIT_BYTES,
264 }
265}
266
267fn nonce(position: u64) -> [u8; 12] {
269 let mut nonce = [0; 12];
270 if let Some(start) = nonce.get_mut(..8) {
271 start.copy_from_slice(&position.to_le_bytes());
272 }
273 nonce
274}