1use std::path::Path;
11
12use base64::Engine as _;
13
14use async_trait::async_trait;
15use chacha20poly1305::aead::{Aead, KeyInit, Payload};
16use zeroize::Zeroize;
17
18use crate::{DEK_LEN, Dek, Kms, KmsError};
19
20const WRAP_AAD: &[u8] = b"FZ-KMS-LOCAL-WRAP-v1";
23const NONCE_LEN: usize = 24;
24
25pub struct LocalFileKms {
28 kek: Dek,
29 key_ref: String,
30}
31
32impl std::fmt::Debug for LocalFileKms {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("LocalFileKms")
38 .field("provider", &"local-file")
39 .field("key_ref", &self.key_ref)
40 .finish_non_exhaustive()
41 }
42}
43
44impl LocalFileKms {
45 pub fn open(path: impl AsRef<Path>, env: &str) -> Result<Self, KmsError> {
64 if env.eq_ignore_ascii_case("production") {
65 return Err(KmsError::Refused(
66 "LocalFileKms holds its master key on local disk, where anything that can read \
67 the file can read the key. It is for development and tests. Configure a managed \
68 KMS provider for production"
69 .to_owned(),
70 ));
71 }
72 let path = path.as_ref();
73 let raw = std::fs::read(path).map_err(|err| {
74 KmsError::Unavailable(format!(
75 "cannot read the master key at {}: {err}",
76 path.display()
77 ))
78 })?;
79 let kek = parse_key(raw).map_err(|err| {
80 KmsError::Invalid(format!("the master key at {}: {err}", path.display()))
81 })?;
82 Ok(Self {
83 kek,
84 key_ref: path.display().to_string(),
85 })
86 }
87
88 pub fn from_key(kek: Dek, key_ref: impl Into<String>, env: &str) -> Result<Self, KmsError> {
95 if env.eq_ignore_ascii_case("production") {
96 return Err(KmsError::Refused(
97 "LocalFileKms is for development and tests".to_owned(),
98 ));
99 }
100 Ok(Self {
101 kek,
102 key_ref: key_ref.into(),
103 })
104 }
105
106 pub fn create(path: impl AsRef<Path>) -> Result<(), KmsError> {
116 let path = path.as_ref();
117 if path.exists() {
118 return Err(KmsError::Refused(format!(
119 "{} exists; every secret wrapped under it would become unreadable. Delete it \
120 deliberately if that is what you mean",
121 path.display()
122 )));
123 }
124 let dek = Dek::generate()?;
125 let mut hex = String::with_capacity(DEK_LEN * 2);
126 for byte in dek.expose() {
127 use std::fmt::Write as _;
128 let _ = write!(hex, "{byte:02x}");
129 }
130 let result = std::fs::write(path, format!("{hex}\n")).map_err(|err| {
131 KmsError::Unavailable(format!("cannot write {}: {err}", path.display()))
132 });
133 hex.zeroize();
134 result
135 }
136}
137
138fn parse_key(raw: Vec<u8>) -> Result<Dek, String> {
140 if raw.len() == DEK_LEN {
141 return Dek::from_bytes(raw).map_err(|err| err.to_string());
142 }
143 let text = String::from_utf8(raw)
144 .map_err(|_| format!("is not {DEK_LEN} raw bytes and not valid text"))?;
145 let text = text.trim();
146 if text.len() == DEK_LEN * 2
147 && let Ok(bytes) = decode_hex(text)
148 {
149 return Dek::from_bytes(bytes).map_err(|err| err.to_string());
150 }
151 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(text.as_bytes())
152 && bytes.len() == DEK_LEN
153 {
154 return Dek::from_bytes(bytes).map_err(|err| err.to_string());
155 }
156 Err(format!(
157 "is not a {DEK_LEN}-byte key: expected {DEK_LEN} raw bytes, {} hex characters, or base64",
158 DEK_LEN * 2
159 ))
160}
161
162fn decode_hex(text: &str) -> Result<Vec<u8>, ()> {
163 let (pairs, rest) = text.as_bytes().as_chunks::<2>();
164 if !rest.is_empty() {
165 return Err(());
166 }
167 let mut out = Vec::with_capacity(pairs.len());
168 for pair in pairs {
169 let hi = (pair[0] as char).to_digit(16).ok_or(())?;
170 let lo = (pair[1] as char).to_digit(16).ok_or(())?;
171 out.push(u8::try_from(hi * 16 + lo).map_err(|_| ())?);
172 }
173 Ok(out)
174}
175
176#[async_trait]
177impl Kms for LocalFileKms {
178 fn provider(&self) -> &'static str {
179 "local-file"
180 }
181
182 fn key_ref(&self) -> &str {
183 &self.key_ref
184 }
185
186 async fn wrap(&self, dek: &Dek) -> Result<Vec<u8>, KmsError> {
187 let cipher = self.cipher()?;
188 let mut nonce = [0_u8; NONCE_LEN];
189 getrandom::fill(&mut nonce)
190 .map_err(|err| KmsError::Unavailable(format!("the OS random source failed: {err}")))?;
191 let sealed = cipher
192 .encrypt(
193 (&nonce).into(),
194 Payload {
195 msg: dek.expose(),
196 aad: WRAP_AAD,
197 },
198 )
199 .map_err(|_| KmsError::Invalid("the data key could not be wrapped".to_owned()))?;
200 let mut out = Vec::with_capacity(NONCE_LEN + sealed.len());
201 out.extend_from_slice(&nonce);
202 out.extend_from_slice(&sealed);
203 Ok(out)
204 }
205
206 async fn unwrap(&self, wrapped: &[u8]) -> Result<Dek, KmsError> {
207 if wrapped.len() <= NONCE_LEN {
208 return Err(KmsError::Tampered(
209 "the wrapped key is too short to contain a nonce and a tag".to_owned(),
210 ));
211 }
212 let (nonce, sealed) = wrapped.split_at(NONCE_LEN);
213 let nonce: &[u8; NONCE_LEN] = nonce.try_into().expect("split at NONCE_LEN");
214 let opened = self
215 .cipher()?
216 .decrypt(
217 nonce.into(),
218 Payload {
219 msg: sealed,
220 aad: WRAP_AAD,
221 },
222 )
223 .map_err(|_| {
224 KmsError::Tampered(
225 "the wrapped key did not authenticate under this master key".to_owned(),
226 )
227 })?;
228 Dek::from_bytes(opened)
229 }
230}
231
232impl LocalFileKms {
233 fn cipher(&self) -> Result<chacha20poly1305::XChaCha20Poly1305, KmsError> {
234 chacha20poly1305::XChaCha20Poly1305::new_from_slice(self.kek.expose())
235 .map_err(|_| KmsError::Invalid("the master key is not 32 bytes".to_owned()))
236 }
237}