1use std::fs;
15use std::path::Path;
16
17use ring::aead::{
18 Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, UnboundKey, AES_256_GCM,
19};
20use ring::error::Unspecified;
21use ring::rand::{SecureRandom, SystemRandom};
22
23use crate::error::{Result, StoreError};
24
25fn ensure_default_keyring_store() -> Result<()> {
31 use std::sync::OnceLock;
32 static INIT: OnceLock<std::result::Result<(), String>> = OnceLock::new();
33 let r = INIT.get_or_init(install_default_store);
34 match r {
35 Ok(()) => Ok(()),
36 Err(msg) => Err(StoreError::Keyring(msg.clone())),
37 }
38}
39
40#[cfg(target_os = "macos")]
41fn install_default_store() -> std::result::Result<(), String> {
42 let store = apple_native_keyring_store::keychain::Store::new().map_err(|e| e.to_string())?;
43 keyring_core::set_default_store(store);
44 Ok(())
45}
46
47#[cfg(target_os = "linux")]
48fn install_default_store() -> std::result::Result<(), String> {
49 let store = linux_keyutils_keyring_store::Store::new().map_err(|e| e.to_string())?;
50 keyring_core::set_default_store(store);
51 Ok(())
52}
53
54#[cfg(target_os = "windows")]
55fn install_default_store() -> std::result::Result<(), String> {
56 let store = windows_native_keyring_store::Store::new().map_err(|e| e.to_string())?;
57 keyring_core::set_default_store(store);
58 Ok(())
59}
60
61#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
62fn install_default_store() -> std::result::Result<(), String> {
63 Err("no native keyring store available for this target".into())
64}
65
66pub const KEYRING_SERVICE: &str = "vibesurfer";
68pub const KEYRING_ACCOUNT: &str = "default";
70
71const KEY_LEN: usize = 32; const NONCE_LEN: usize = 12; #[derive(Clone)]
80pub struct MasterKey([u8; KEY_LEN]);
81
82impl std::fmt::Debug for MasterKey {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.write_str("MasterKey([REDACTED])")
87 }
88}
89
90impl MasterKey {
91 #[must_use]
95 pub const fn from_bytes(bytes: [u8; KEY_LEN]) -> Self {
96 Self(bytes)
97 }
98
99 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
101 let bytes = fs::read(path.as_ref())?;
102 if bytes.len() != KEY_LEN {
103 return Err(StoreError::KeyFileSize(bytes.len()));
104 }
105 let mut buf = [0u8; KEY_LEN];
106 buf.copy_from_slice(&bytes);
107 Ok(Self(buf))
108 }
109
110 pub fn from_keyring() -> Result<Self> {
115 ensure_default_keyring_store()?;
116 let entry = keyring_core::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
117 .map_err(|e| StoreError::Keyring(e.to_string()))?;
118 let hex = entry
119 .get_password()
120 .map_err(|e| StoreError::Keyring(e.to_string()))?;
121 decode_hex_key(&hex)
122 }
123
124 pub fn resolve(fallback_path: impl AsRef<Path>) -> Result<Self> {
127 match Self::from_keyring() {
128 Ok(k) => Ok(k),
129 Err(_) => Self::from_file(fallback_path),
130 }
131 }
132
133 pub fn generate() -> Result<Self> {
135 let mut buf = [0u8; KEY_LEN];
136 SystemRandom::new()
137 .fill(&mut buf)
138 .map_err(|_| StoreError::Crypto("rng"))?;
139 Ok(Self(buf))
140 }
141
142 pub fn write_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
146 fs::write(path.as_ref(), self.0)?;
147 Ok(())
148 }
149
150 pub fn write_to_keyring(&self) -> Result<()> {
152 use std::fmt::Write as _;
153 let mut hex = String::with_capacity(KEY_LEN * 2);
154 for b in &self.0 {
155 write!(hex, "{b:02x}").expect("write to String never fails");
156 }
157 ensure_default_keyring_store()?;
158 let entry = keyring_core::Entry::new(KEYRING_SERVICE, KEYRING_ACCOUNT)
159 .map_err(|e| StoreError::Keyring(e.to_string()))?;
160 entry
161 .set_password(&hex)
162 .map_err(|e| StoreError::Keyring(e.to_string()))?;
163 Ok(())
164 }
165
166 fn raw(&self) -> &[u8; KEY_LEN] {
167 &self.0
168 }
169}
170
171fn decode_hex_key(s: &str) -> Result<MasterKey> {
172 if s.len() != KEY_LEN * 2 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
173 return Err(StoreError::Crypto("hex key shape"));
174 }
175 let mut buf = [0u8; KEY_LEN];
176 for (i, chunk) in s.as_bytes().chunks_exact(2).enumerate() {
177 let pair = std::str::from_utf8(chunk).map_err(|_| StoreError::Crypto("hex key utf8"))?;
178 buf[i] = u8::from_str_radix(pair, 16).map_err(|_| StoreError::Crypto("hex key digit"))?;
179 }
180 Ok(MasterKey::from_bytes(buf))
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct EncryptedBlob {
186 pub ciphertext: Vec<u8>,
187 pub nonce: [u8; NONCE_LEN],
188}
189
190pub fn encrypt(key: &MasterKey, plaintext: &[u8]) -> Result<EncryptedBlob> {
194 let mut nonce = [0u8; NONCE_LEN];
195 SystemRandom::new()
196 .fill(&mut nonce)
197 .map_err(|_| StoreError::Crypto("rng"))?;
198
199 let unbound =
200 UnboundKey::new(&AES_256_GCM, key.raw()).map_err(|_| StoreError::Crypto("key"))?;
201 let mut sealing = SealingKey::new(unbound, OneNonce(Some(nonce)));
202
203 let mut buf = plaintext.to_vec();
204 sealing
205 .seal_in_place_append_tag(Aad::empty(), &mut buf)
206 .map_err(|_| StoreError::Crypto("seal"))?;
207 Ok(EncryptedBlob {
208 ciphertext: buf,
209 nonce,
210 })
211}
212
213pub fn decrypt(key: &MasterKey, blob: &EncryptedBlob) -> Result<Vec<u8>> {
215 let unbound =
216 UnboundKey::new(&AES_256_GCM, key.raw()).map_err(|_| StoreError::Crypto("key"))?;
217 let mut opening = OpeningKey::new(unbound, OneNonce(Some(blob.nonce)));
218 let mut buf = blob.ciphertext.clone();
219 let plaintext = opening
220 .open_in_place(Aad::empty(), &mut buf)
221 .map_err(|_| StoreError::Crypto("open (likely wrong key or tampered blob)"))?;
222 Ok(plaintext.to_vec())
223}
224
225struct OneNonce(Option<[u8; NONCE_LEN]>);
229
230impl NonceSequence for OneNonce {
231 fn advance(&mut self) -> std::result::Result<Nonce, Unspecified> {
232 let bytes = self.0.take().ok_or(Unspecified)?;
233 Ok(Nonce::assume_unique_for_key(bytes))
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn fixed_key() -> MasterKey {
242 MasterKey::from_bytes([7u8; KEY_LEN])
243 }
244
245 #[test]
246 fn round_trip_empty() {
247 let k = fixed_key();
248 let blob = encrypt(&k, &[]).unwrap();
249 assert_eq!(decrypt(&k, &blob).unwrap(), Vec::<u8>::new());
250 }
251
252 #[test]
253 fn round_trip_payload() {
254 let k = fixed_key();
255 let plain = b"cookies=session=abc123; theme=dark";
256 let blob = encrypt(&k, plain).unwrap();
257 assert_ne!(blob.ciphertext, plain);
258 assert_eq!(decrypt(&k, &blob).unwrap(), plain.to_vec());
259 }
260
261 #[test]
262 fn nonces_are_unique() {
263 let k = fixed_key();
264 let a = encrypt(&k, b"hello").unwrap();
265 let b = encrypt(&k, b"hello").unwrap();
266 assert_ne!(a.nonce, b.nonce);
267 assert_ne!(a.ciphertext, b.ciphertext);
268 }
269
270 #[test]
271 fn wrong_key_rejected() {
272 let k1 = MasterKey::from_bytes([1u8; KEY_LEN]);
273 let k2 = MasterKey::from_bytes([2u8; KEY_LEN]);
274 let blob = encrypt(&k1, b"secret").unwrap();
275 assert!(decrypt(&k2, &blob).is_err());
276 }
277
278 #[test]
279 fn tamper_rejected() {
280 let k = fixed_key();
281 let mut blob = encrypt(&k, b"secret").unwrap();
282 blob.ciphertext[0] ^= 0xff;
284 assert!(decrypt(&k, &blob).is_err());
285 }
286
287 #[test]
288 fn key_file_round_trip() {
289 let dir = tempfile::tempdir().unwrap();
290 let path = dir.path().join("key");
291 let k = MasterKey::generate().unwrap();
292 k.write_to_file(&path).unwrap();
293 let loaded = MasterKey::from_file(&path).unwrap();
294 assert_eq!(k.0, loaded.0);
295 }
296
297 #[test]
298 fn key_file_wrong_size_rejected() {
299 let dir = tempfile::tempdir().unwrap();
300 let path = dir.path().join("key");
301 std::fs::write(&path, b"too short").unwrap();
302 let err = MasterKey::from_file(&path).unwrap_err();
303 matches!(err, StoreError::KeyFileSize(_));
304 }
305}