1use aes_gcm::{
8 Aes256Gcm, KeyInit, Nonce,
9 aead::{Aead, AeadCore, OsRng, Payload},
10};
11use serde::{Deserialize, Serialize};
12use zeroize::Zeroizing;
13
14use super::{AccountError, crypto};
15use crate::{
16 ID, Result,
17 auth::crypto::{PrivateKey, PublicKey},
18};
19
20const VERSION: u8 = 1;
25const AAD: &[u8] = b"eidetica:local-credential-bundle:v1";
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct CredentialBundle {
36 version: u8,
37 salt: String,
38 nonce: Vec<u8>,
39 ciphertext: Vec<u8>,
40}
41
42pub struct UnlockedCredentials {
48 account_id: ID,
49 keys: Vec<PrivateKey>,
50 wrapping_key: Option<Zeroizing<Vec<u8>>>,
53 salt: Option<String>,
54}
55
56#[derive(Serialize)]
57struct BorrowedPayload<'a> {
58 account_id: &'a ID,
59 keys: &'a [PrivateKey],
60}
61
62#[derive(Deserialize)]
63#[serde(deny_unknown_fields)]
64struct PayloadData {
65 account_id: ID,
66 keys: Vec<PrivateKey>,
67}
68
69fn invalid_bundle() -> crate::Error {
70 AccountError::DecryptionFailed {
71 reason: "Invalid local credential bundle".to_owned(),
72 }
73 .into()
74}
75
76fn validate_keys(keys: &[PrivateKey]) -> Result<()> {
77 if keys.is_empty() {
78 return Err(AccountError::NoKeysAvailable.into());
79 }
80 let mut seen = std::collections::HashSet::new();
81 for key in keys {
82 if !seen.insert(key.public_key()) {
83 return Err(AccountError::KeyAlreadyExists {
84 key_id: key.public_key().to_string(),
85 }
86 .into());
87 }
88 }
89 Ok(())
90}
91
92impl CredentialBundle {
93 pub fn seal(account_id: &ID, keys: &[PrivateKey], password: &str) -> Result<Self> {
114 validate_keys(keys)?;
115 let salt = crypto::generate_salt();
116 let wrapping_key = Zeroizing::new(crypto::derive_encryption_key(password, &salt)?);
117 Self::encrypt(account_id, keys, &salt, &wrapping_key)
118 }
119
120 fn encrypt(account_id: &ID, keys: &[PrivateKey], salt: &str, key: &[u8]) -> Result<Self> {
121 let payload = BorrowedPayload { account_id, keys };
125 let mut counter = ByteCount(0);
126 serde_json::to_writer(&mut counter, &payload).map_err(|_| invalid_bundle())?;
127 let mut plaintext = Zeroizing::new(Vec::with_capacity(counter.0));
128 serde_json::to_writer(&mut *plaintext, &payload).map_err(|_| invalid_bundle())?;
129 let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| invalid_bundle())?;
130 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
131 let ciphertext = cipher
132 .encrypt(
133 &nonce,
134 Payload {
135 msg: &plaintext,
136 aad: AAD,
137 },
138 )
139 .map_err(|_| invalid_bundle())?;
140 Ok(Self {
141 version: VERSION,
142 salt: salt.to_owned(),
143 nonce: nonce.to_vec(),
144 ciphertext,
145 })
146 }
147
148 pub fn unlock(&self, password: &str) -> Result<UnlockedCredentials> {
152 if self.version != VERSION || self.nonce.len() != crypto::NONCE_LENGTH {
153 return Err(invalid_bundle());
154 }
155 let wrapping_key = Zeroizing::new(crypto::derive_encryption_key(password, &self.salt)?);
156 self.unlock_with_key(wrapping_key)
157 }
158
159 fn unlock_with_key(&self, wrapping_key: Zeroizing<Vec<u8>>) -> Result<UnlockedCredentials> {
160 if self.version != VERSION || self.nonce.len() != crypto::NONCE_LENGTH {
161 return Err(invalid_bundle());
162 }
163 let cipher = Aes256Gcm::new_from_slice(&wrapping_key).map_err(|_| invalid_bundle())?;
164 let plaintext = Zeroizing::new(
165 cipher
166 .decrypt(
167 Nonce::from_slice(&self.nonce),
168 Payload {
169 msg: &self.ciphertext,
170 aad: AAD,
171 },
172 )
173 .map_err(|_| invalid_bundle())?,
174 );
175 let payload: PayloadData =
176 serde_json::from_slice(&plaintext).map_err(|_| invalid_bundle())?;
177 validate_keys(&payload.keys)?;
178 Ok(UnlockedCredentials {
179 account_id: payload.account_id,
180 keys: payload.keys,
181 wrapping_key: Some(wrapping_key),
182 salt: Some(self.salt.clone()),
183 })
184 }
185
186 pub fn unlock_for(&self, account_id: &ID, password: &str) -> Result<UnlockedCredentials> {
189 let credentials = self.unlock(password)?;
190 if credentials.account_id() != account_id {
191 return Err(invalid_bundle());
192 }
193 Ok(credentials)
194 }
195}
196
197impl UnlockedCredentials {
198 pub(crate) fn from_signer(account_id: ID, keys: Vec<PrivateKey>) -> Result<Self> {
199 validate_keys(&keys)?;
200 Ok(Self {
201 account_id,
202 keys,
203 wrapping_key: None,
204 salt: None,
205 })
206 }
207
208 pub(crate) fn get_signer(&self, key_id: &PublicKey) -> Option<&PrivateKey> {
209 self.keys.iter().find(|key| &key.public_key() == key_id)
210 }
211
212 pub(crate) fn clear(&mut self) {
213 self.keys.clear();
214 self.wrapping_key = None;
215 }
216
217 pub(crate) fn storage(&self) -> Result<super::KeyStorage> {
218 Ok(super::KeyStorage::Encrypted {
219 bundle: self.seal()?,
220 })
221 }
222
223 pub(crate) fn unlock_storage(&self, storage: &super::KeyStorage) -> Result<Self> {
226 let super::KeyStorage::Encrypted { bundle } = storage;
227 if Some(&bundle.salt) != self.salt.as_ref() {
228 return Err(invalid_bundle());
229 }
230 let key = self.wrapping_key.as_ref().ok_or_else(invalid_bundle)?;
231 let unlocked = bundle.unlock_with_key(key.clone())?;
232 if unlocked.account_id != self.account_id {
233 return Err(invalid_bundle());
234 }
235 Ok(unlocked)
236 }
237
238 pub fn account_id(&self) -> &ID {
240 &self.account_id
241 }
242
243 pub fn key_ids(&self) -> Vec<PublicKey> {
245 self.keys.iter().map(PrivateKey::public_key).collect()
246 }
247
248 pub fn signer(&self, key_id: &PublicKey) -> Result<PrivateKey> {
251 self.keys
252 .iter()
253 .find(|key| &key.public_key() == key_id)
254 .cloned()
255 .ok_or_else(|| {
256 AccountError::KeyNotFound {
257 key_id: key_id.to_string(),
258 }
259 .into()
260 })
261 }
262
263 pub fn add_key(&mut self, key: PrivateKey) -> Result<()> {
266 let key_id = key.public_key();
267 if self.keys.iter().any(|key| key.public_key() == key_id) {
268 return Err(AccountError::KeyAlreadyExists {
269 key_id: key_id.to_string(),
270 }
271 .into());
272 }
273 self.keys.push(key);
274 Ok(())
275 }
276
277 pub fn seal(&self) -> Result<CredentialBundle> {
280 CredentialBundle::encrypt(
281 &self.account_id,
282 &self.keys,
283 self.salt.as_deref().ok_or_else(invalid_bundle)?,
284 self.wrapping_key.as_deref().ok_or_else(invalid_bundle)?,
285 )
286 }
287
288 pub fn rewrap(&self, password: &str) -> Result<CredentialBundle> {
292 CredentialBundle::seal(&self.account_id, &self.keys, password)
293 }
294}
295
296struct ByteCount(usize);
297impl std::io::Write for ByteCount {
298 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
299 self.0 += bytes.len();
300 Ok(bytes.len())
301 }
302 fn flush(&mut self) -> std::io::Result<()> {
303 Ok(())
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::auth::crypto::generate_keypair;
311
312 #[test]
313 fn credential_bundle_requires_its_domain_aad() {
314 let account = ID::from_bytes("account");
315 let bundle = CredentialBundle::seal(&account, &[generate_keypair().0], "password").unwrap();
316 assert!(bundle.unlock_for(&account, "password").is_ok());
317 let key = Zeroizing::new(crypto::derive_encryption_key("password", &bundle.salt).unwrap());
318 let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
319 assert!(
320 cipher
321 .decrypt(
322 Nonce::from_slice(&bundle.nonce),
323 bundle.ciphertext.as_slice()
324 )
325 .is_err()
326 );
327 assert!(
328 cipher
329 .decrypt(
330 Nonce::from_slice(&bundle.nonce),
331 Payload {
332 msg: &bundle.ciphertext,
333 aad: b"another-format"
334 }
335 )
336 .is_err()
337 );
338 }
339}