1use hkdf::Hkdf;
8use rand::RngCore;
9use sha2::Sha256;
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12use crate::{CryptoError, CryptoResult};
13
14const SALT: &[u8] = b"void-v1";
15
16macro_rules! define_secret_key_newtype {
28 ($(#[$meta:meta])* $name:ident, $size:literal) => {
29 $(#[$meta])*
30 #[derive(Clone, Zeroize, ZeroizeOnDrop)]
31 pub struct $name([u8; $size]);
32
33 impl $name {
34 pub fn from_bytes(bytes: [u8; $size]) -> Self {
36 Self(bytes)
37 }
38
39 pub fn as_bytes(&self) -> &[u8; $size] {
41 &self.0
42 }
43 }
44
45 impl std::fmt::Debug for $name {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}([REDACTED])", stringify!($name))
48 }
49 }
50 };
51}
52
53define_secret_key_newtype!(
58 ShareKey, 32
65);
66
67define_secret_key_newtype!(
72 SigningSecretKey, 32
74);
75
76define_secret_key_newtype!(
77 RecipientSecretKey, 32
79);
80
81define_secret_key_newtype!(
82 IdentitySeed, 64
84);
85
86define_secret_key_newtype!(
87 NostrSecretKey, 32
89);
90
91#[derive(Clone, Zeroize, ZeroizeOnDrop)]
103pub struct SecretKey([u8; 32]);
104
105impl SecretKey {
106 pub fn new(bytes: [u8; 32]) -> Self {
108 Self(bytes)
109 }
110
111 pub fn as_bytes(&self) -> &[u8; 32] {
119 &self.0
120 }
121
122 pub fn generate() -> Self {
124 let mut bytes = [0u8; 32];
125 rand::thread_rng().fill_bytes(&mut bytes);
126 Self(bytes)
127 }
128}
129
130impl AsRef<[u8]> for SecretKey {
131 fn as_ref(&self) -> &[u8] {
132 &self.0
133 }
134}
135
136#[derive(Clone, Copy, PartialEq, Eq, Zeroize, serde::Serialize, serde::Deserialize)]
148pub struct ContentKey([u8; 32]);
149
150impl ContentKey {
151 pub(crate) fn new(bytes: [u8; 32]) -> Self {
155 Self(bytes)
156 }
157
158 pub(crate) fn from_raw(bytes: [u8; 32]) -> Self {
160 Self(bytes)
161 }
162
163 pub fn from_hex(hex: &str) -> CryptoResult<Self> {
169 let bytes = hex::decode(hex.trim())
170 .map_err(|e| CryptoError::InvalidKey(format!("invalid content key hex: {e}")))?;
171 let arr: [u8; 32] = bytes
172 .try_into()
173 .map_err(|_| CryptoError::InvalidKey("content key must be 32 bytes".into()))?;
174 Ok(Self(arr))
175 }
176
177 pub fn as_bytes(&self) -> &[u8; 32] {
184 &self.0
185 }
186}
187
188impl std::fmt::Debug for ContentKey {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(f, "ContentKey({}…)", hex::encode(&self.0[..4]))
191 }
192}
193
194#[derive(Clone, Copy, PartialEq, Eq)]
207pub struct Nonce<const N: usize>([u8; N]);
208
209impl<const N: usize> Nonce<N> {
210 pub const SIZE: usize = N;
212
213 pub fn generate() -> Self {
215 let mut bytes = [0u8; N];
216 rand::thread_rng().fill_bytes(&mut bytes);
217 Self(bytes)
218 }
219
220 pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
224 let arr: [u8; N] = bytes.try_into().ok()?;
225 Some(Self(arr))
226 }
227
228 pub fn as_bytes(&self) -> &[u8; N] {
230 &self.0
231 }
232}
233
234impl<const N: usize> AsRef<[u8]> for Nonce<N> {
235 fn as_ref(&self) -> &[u8] {
236 &self.0
237 }
238}
239
240impl<const N: usize> std::fmt::Debug for Nonce<N> {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 let preview_len = N.min(4);
243 write!(f, "Nonce<{}>({}…)", N, hex::encode(&self.0[..preview_len]))
244 }
245}
246
247impl<const N: usize> std::fmt::Display for Nonce<N> {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 write!(f, "{}", hex::encode(&self.0))
250 }
251}
252
253pub type KeyNonce = Nonce<16>;
258
259pub type AeadNonce = Nonce<12>;
261
262#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
273pub struct RepoSecret([u8; 32]);
274
275impl RepoSecret {
276 pub fn new(bytes: [u8; 32]) -> Self {
278 Self(bytes)
279 }
280
281 pub fn as_bytes(&self) -> &[u8; 32] {
283 &self.0
284 }
285
286 pub fn generate() -> Self {
288 let mut bytes = [0u8; 32];
289 rand::thread_rng().fill_bytes(&mut bytes);
290 Self(bytes)
291 }
292}
293
294impl std::fmt::Debug for RepoSecret {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 write!(f, "RepoSecret([REDACTED])")
297 }
298}
299
300#[derive(Clone, Debug, PartialEq, Eq)]
309pub enum KeyPurpose {
310 Commits,
312 Metadata,
314 Content,
316 Index,
318 Stash,
320 Staged,
322 ScopedRead(String),
324}
325
326impl KeyPurpose {
327 fn info(&self) -> Vec<u8> {
329 match self {
330 Self::Commits => b"void-v1-commits".to_vec(),
331 Self::Metadata => b"void-v1-metadata".to_vec(),
332 Self::Content => b"void-v1-content".to_vec(),
333 Self::Index => b"void-v1-index".to_vec(),
334 Self::Stash => b"void-v1-stash".to_vec(),
335 Self::Staged => b"void-v1-staged".to_vec(),
336 Self::ScopedRead(scope) => format!("void-v1-read:{}", scope).into_bytes(),
337 }
338 }
339}
340
341#[derive(Zeroize, ZeroizeOnDrop)]
352pub struct KeyRing {
353 pub(crate) commits: SecretKey,
355 pub(crate) metadata: SecretKey,
357 pub(crate) content: SecretKey,
359 pub(crate) index: SecretKey,
361 pub(crate) stash: SecretKey,
363 pub(crate) staged: SecretKey,
365}
366
367impl KeyRing {
368 pub fn from_root(root_key: &[u8; 32]) -> CryptoResult<Self> {
370 Ok(Self {
371 commits: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Commits)?),
372 metadata: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Metadata)?),
373 content: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Content)?),
374 index: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Index)?),
375 stash: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Stash)?),
376 staged: SecretKey::new(derive_key_for_purpose(root_key, KeyPurpose::Staged)?),
377 })
378 }
379}
380
381pub fn derive_key_for_purpose(root_key: &[u8; 32], purpose: KeyPurpose) -> CryptoResult<[u8; 32]> {
387 let hk = Hkdf::<Sha256>::new(Some(SALT), root_key);
388 let mut output = [0u8; 32];
389 hk.expand(&purpose.info(), &mut output)
390 .map_err(|e| CryptoError::InvalidKey(e.to_string()))?;
391 Ok(output)
392}
393
394pub fn derive_scoped_key(root_key: &[u8; 32], scope: &str) -> CryptoResult<[u8; 32]> {
400 derive_key_for_purpose(root_key, KeyPurpose::ScopedRead(scope.to_string()))
401}
402
403pub fn generate_key() -> [u8; 32] {
405 let mut key = [0u8; 32];
406 rand::thread_rng().fill_bytes(&mut key);
407 key
408}
409
410pub fn derive_key(root_key: &[u8; 32], info: &str) -> CryptoResult<[u8; 32]> {
416 let hk = Hkdf::<Sha256>::new(Some(SALT), root_key);
417
418 let mut output = [0u8; 32];
419 hk.expand(info.as_bytes(), &mut output)
420 .map_err(|e| CryptoError::InvalidKey(e.to_string()))?;
421
422 Ok(output)
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn derive_key_deterministic() {
431 let root = [0x42u8; 32];
432 let key1 = derive_key(&root, "encryption").unwrap();
433 let key2 = derive_key(&root, "encryption").unwrap();
434 assert_eq!(key1, key2);
435 }
436
437 #[test]
438 fn derive_key_different_info() {
439 let root = [0x42u8; 32];
440 let key1 = derive_key(&root, "encryption").unwrap();
441 let key2 = derive_key(&root, "signing").unwrap();
442 assert_ne!(key1, key2);
443 }
444
445 #[test]
446 fn derive_key_different_root() {
447 let root1 = [0x42u8; 32];
448 let root2 = [0x43u8; 32];
449 let key1 = derive_key(&root1, "encryption").unwrap();
450 let key2 = derive_key(&root2, "encryption").unwrap();
451 assert_ne!(key1, key2);
452 }
453
454 #[test]
455 fn generate_key_unique() {
456 let key1 = generate_key();
457 let key2 = generate_key();
458 assert_ne!(key1, key2);
459 }
460
461 #[test]
462 fn key_separation_produces_different_keys() {
463 let root = generate_key();
464 let ring = KeyRing::from_root(&root).unwrap();
465
466 assert_ne!(ring.commits.as_bytes(), ring.metadata.as_bytes());
467 assert_ne!(ring.metadata.as_bytes(), ring.content.as_bytes());
468 assert_ne!(ring.content.as_bytes(), ring.index.as_bytes());
469 assert_ne!(ring.index.as_bytes(), ring.stash.as_bytes());
470 assert_ne!(ring.stash.as_bytes(), ring.staged.as_bytes());
471 }
472
473 #[test]
474 fn stash_and_staged_keys_are_distinct() {
475 let root = generate_key();
476 let ring = KeyRing::from_root(&root).unwrap();
477
478 assert_ne!(ring.stash.as_bytes(), ring.index.as_bytes());
479 assert_ne!(ring.staged.as_bytes(), ring.index.as_bytes());
480 assert_ne!(ring.stash.as_bytes(), ring.staged.as_bytes());
481 }
482
483 #[test]
484 fn derive_key_for_purpose_deterministic() {
485 let root = [0x42u8; 32];
486 let key1 = derive_key_for_purpose(&root, KeyPurpose::Commits).unwrap();
487 let key2 = derive_key_for_purpose(&root, KeyPurpose::Commits).unwrap();
488 assert_eq!(key1, key2);
489 }
490
491 #[test]
492 fn derive_key_for_purpose_different_purposes() {
493 let root = [0x42u8; 32];
494
495 let commits = derive_key_for_purpose(&root, KeyPurpose::Commits).unwrap();
496 let metadata = derive_key_for_purpose(&root, KeyPurpose::Metadata).unwrap();
497 let content = derive_key_for_purpose(&root, KeyPurpose::Content).unwrap();
498 let index = derive_key_for_purpose(&root, KeyPurpose::Index).unwrap();
499 let stash = derive_key_for_purpose(&root, KeyPurpose::Stash).unwrap();
500 let staged = derive_key_for_purpose(&root, KeyPurpose::Staged).unwrap();
501
502 let keys = [commits, metadata, content, index, stash, staged];
503 for i in 0..keys.len() {
504 for j in (i + 1)..keys.len() {
505 assert_ne!(keys[i], keys[j], "Keys at {} and {} should differ", i, j);
506 }
507 }
508 }
509
510 #[test]
511 fn secret_key_generate_unique() {
512 let key1 = SecretKey::generate();
513 let key2 = SecretKey::generate();
514 assert_ne!(key1.as_bytes(), key2.as_bytes());
515 }
516
517 #[test]
518 fn secret_key_as_ref() {
519 let bytes = [0x42u8; 32];
520 let key = SecretKey::new(bytes);
521 let slice: &[u8] = key.as_ref();
522 assert_eq!(slice, &bytes);
523 }
524}