1use hkdf::Hkdf;
12use sha2::Sha256;
13
14use crate::kdf::{IdentitySeed, NostrSecretKey, RecipientSecretKey, SigningSecretKey};
15
16const IDENTITY_SALT: &[u8] = b"void-identity-v1";
18
19const SIGNING_INFO: &[u8] = b"void-identity-signing";
21
22const RECIPIENT_INFO: &[u8] = b"void-identity-recipient";
24
25const NOSTR_INFO: &[u8] = b"void-identity-nostr";
27
28const REPO_OWNER_SALT: &[u8] = b"void-repo-owner-v1";
30
31#[derive(Debug, thiserror::Error)]
33pub enum SeedError {
34 #[error("invalid mnemonic phrase: {0}")]
35 InvalidMnemonic(String),
36
37 #[error("key derivation failed")]
38 DerivationFailed,
39}
40
41pub fn generate_mnemonic() -> String {
45 bip39::Mnemonic::generate(24)
46 .expect("24 is a valid word count")
47 .to_string()
48}
49
50pub fn mnemonic_to_seed(mnemonic: &str) -> Result<IdentitySeed, SeedError> {
55 let parsed = bip39::Mnemonic::parse(mnemonic)
56 .map_err(|e| SeedError::InvalidMnemonic(e.to_string()))?;
57
58 let seed_bytes: [u8; 64] = parsed.to_seed("");
59 Ok(IdentitySeed::from_bytes(seed_bytes))
60}
61
62pub fn derive_signing_key(seed: &IdentitySeed) -> Result<SigningSecretKey, SeedError> {
67 let hk = Hkdf::<Sha256>::new(Some(IDENTITY_SALT), seed.as_bytes());
68 let mut output = [0u8; 32];
69 hk.expand(SIGNING_INFO, &mut output)
70 .map_err(|_| SeedError::DerivationFailed)?;
71 Ok(SigningSecretKey::from_bytes(output))
72}
73
74pub fn derive_recipient_key(seed: &IdentitySeed) -> Result<RecipientSecretKey, SeedError> {
80 let hk = Hkdf::<Sha256>::new(Some(IDENTITY_SALT), seed.as_bytes());
81 let mut output = [0u8; 32];
82 hk.expand(RECIPIENT_INFO, &mut output)
83 .map_err(|_| SeedError::DerivationFailed)?;
84 Ok(RecipientSecretKey::from_bytes(output))
85}
86
87pub fn derive_nostr_key(seed: &IdentitySeed) -> Result<NostrSecretKey, SeedError> {
93 let hk = Hkdf::<Sha256>::new(Some(IDENTITY_SALT), seed.as_bytes());
94 let mut output = [0u8; 32];
95 hk.expand(NOSTR_INFO, &mut output)
96 .map_err(|_| SeedError::DerivationFailed)?;
97 Ok(NostrSecretKey::from_bytes(output))
98}
99
100pub fn derive_repo_owner_key(seed: &IdentitySeed, repo_id: &str) -> Result<SigningSecretKey, SeedError> {
110 let hk = Hkdf::<Sha256>::new(Some(REPO_OWNER_SALT), seed.as_bytes());
111 let mut output = [0u8; 32];
112 hk.expand(repo_id.as_bytes(), &mut output)
113 .map_err(|_| SeedError::DerivationFailed)?;
114 Ok(SigningSecretKey::from_bytes(output))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn generate_mnemonic_is_24_words() {
123 let mnemonic = generate_mnemonic();
124 let words: Vec<&str> = mnemonic.split_whitespace().collect();
125 assert_eq!(words.len(), 24);
126 }
127
128 #[test]
129 fn mnemonic_to_seed_deterministic() {
130 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
131 let seed1 = mnemonic_to_seed(mnemonic).unwrap();
132 let seed2 = mnemonic_to_seed(mnemonic).unwrap();
133 assert_eq!(seed1.as_bytes(), seed2.as_bytes());
134 }
135
136 #[test]
137 fn derive_signing_key_deterministic() {
138 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
139 let seed = mnemonic_to_seed(mnemonic).unwrap();
140 let key1 = derive_signing_key(&seed).unwrap();
141 let key2 = derive_signing_key(&seed).unwrap();
142 assert_eq!(key1.as_bytes(), key2.as_bytes());
143 }
144
145 #[test]
146 fn derive_recipient_key_deterministic() {
147 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
148 let seed = mnemonic_to_seed(mnemonic).unwrap();
149 let key1 = derive_recipient_key(&seed).unwrap();
150 let key2 = derive_recipient_key(&seed).unwrap();
151 assert_eq!(key1.as_bytes(), key2.as_bytes());
152 }
153
154 #[test]
155 fn signing_and_recipient_keys_differ() {
156 let mnemonic = generate_mnemonic();
157 let seed = mnemonic_to_seed(&mnemonic).unwrap();
158 let signing = derive_signing_key(&seed).unwrap();
159 let recipient = derive_recipient_key(&seed).unwrap();
160 assert_ne!(signing.as_bytes(), recipient.as_bytes());
161 }
162
163 #[test]
164 fn invalid_mnemonic_rejected() {
165 assert!(mnemonic_to_seed("not a valid mnemonic").is_err());
166 assert!(mnemonic_to_seed("").is_err());
167 assert!(mnemonic_to_seed("abandon").is_err());
168 }
169
170 #[test]
171 fn derive_repo_owner_key_deterministic() {
172 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
173 let seed = mnemonic_to_seed(mnemonic).unwrap();
174 let key1 = derive_repo_owner_key(&seed, "repo-123").unwrap();
175 let key2 = derive_repo_owner_key(&seed, "repo-123").unwrap();
176 assert_eq!(key1.as_bytes(), key2.as_bytes());
177 }
178
179 #[test]
180 fn derive_repo_owner_key_different_repos() {
181 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
182 let seed = mnemonic_to_seed(mnemonic).unwrap();
183 let key_a = derive_repo_owner_key(&seed, "repo-a").unwrap();
184 let key_b = derive_repo_owner_key(&seed, "repo-b").unwrap();
185 assert_ne!(key_a.as_bytes(), key_b.as_bytes());
186 }
187
188 #[test]
189 fn repo_owner_key_differs_from_identity_keys() {
190 let mnemonic = generate_mnemonic();
191 let seed = mnemonic_to_seed(&mnemonic).unwrap();
192 let signing = derive_signing_key(&seed).unwrap();
193 let recipient = derive_recipient_key(&seed).unwrap();
194 let repo_owner = derive_repo_owner_key(&seed, "test-repo").unwrap();
195 assert_ne!(repo_owner.as_bytes(), signing.as_bytes());
196 assert_ne!(repo_owner.as_bytes(), recipient.as_bytes());
197 }
198
199 #[test]
200 fn derive_nostr_key_deterministic() {
201 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
202 let seed = mnemonic_to_seed(mnemonic).unwrap();
203 let key1 = derive_nostr_key(&seed).unwrap();
204 let key2 = derive_nostr_key(&seed).unwrap();
205 assert_eq!(key1.as_bytes(), key2.as_bytes());
206 }
207
208 #[test]
209 fn nostr_key_differs_from_other_keys() {
210 let mnemonic = generate_mnemonic();
211 let seed = mnemonic_to_seed(&mnemonic).unwrap();
212 let signing = derive_signing_key(&seed).unwrap();
213 let recipient = derive_recipient_key(&seed).unwrap();
214 let nostr = derive_nostr_key(&seed).unwrap();
215 assert_ne!(nostr.as_bytes(), signing.as_bytes());
216 assert_ne!(nostr.as_bytes(), recipient.as_bytes());
217 }
218
219 #[test]
220 fn different_mnemonics_different_keys() {
221 let mnemonic1 = generate_mnemonic();
222 let mnemonic2 = generate_mnemonic();
223
224 let seed1 = mnemonic_to_seed(&mnemonic1).unwrap();
225 let seed2 = mnemonic_to_seed(&mnemonic2).unwrap();
226
227 let signing1 = derive_signing_key(&seed1).unwrap();
228 let signing2 = derive_signing_key(&seed2).unwrap();
229
230 assert_ne!(signing1.as_bytes(), signing2.as_bytes());
231 }
232}