1pub fn ed25519_multibase_pubkey(public_key_bytes: &[u8; 32]) -> String {
3 let mut buf = Vec::with_capacity(34);
4 buf.extend_from_slice(&[0xed, 0x01]);
5 buf.extend_from_slice(public_key_bytes);
6 multibase::encode(multibase::Base::Base58Btc, &buf)
7}
8
9const ED25519_PUB_CODEC: [u8; 2] = [0xed, 0x01];
11
12pub fn decode_ed25519_public_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
19 let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
20 let key_bytes = if raw.len() == PREFIXED_KEY_LEN && [raw[0], raw[1]] == ED25519_PUB_CODEC {
21 &raw[2..]
22 } else {
23 &raw[..]
24 };
25 key_bytes
26 .try_into()
27 .map_err(|_| DidKeyError::InvalidSeedLength)
28}
29
30const PREFIXED_KEY_LEN: usize = 34;
39
40const ED25519_PRIV_CODEC: [u8; 2] = [0x80, 0x26]; const X25519_PRIV_CODEC: [u8; 2] = [0x82, 0x26]; const P256_PRIV_CODEC: [u8; 2] = [0x86, 0x26]; pub fn decode_private_key_multibase(mb: &str) -> Result<[u8; 32], DidKeyError> {
54 let (_, raw) = multibase::decode(mb).map_err(|e| DidKeyError::Multibase(e.to_string()))?;
55 let key_bytes = if raw.len() == PREFIXED_KEY_LEN
56 && matches!(
57 [raw[0], raw[1]],
58 ED25519_PRIV_CODEC | X25519_PRIV_CODEC | P256_PRIV_CODEC
59 ) {
60 &raw[2..]
61 } else {
62 &raw[..]
63 };
64 key_bytes
65 .try_into()
66 .map_err(|_| DidKeyError::InvalidSeedLength)
67}
68
69#[cfg(feature = "didcomm")]
71pub struct DidKeySecrets {
72 pub signing: affinidi_tdk::secrets_resolver::secrets::Secret,
73 pub key_agreement: affinidi_tdk::secrets_resolver::secrets::Secret,
74}
75
76#[cfg(feature = "didcomm")]
81pub fn secrets_from_did_key(did: &str, seed: &[u8; 32]) -> Result<DidKeySecrets, DidKeyError> {
82 use affinidi_tdk::secrets_resolver::secrets::Secret;
83
84 let ed_pub_mb = did
85 .strip_prefix("did:key:")
86 .ok_or(DidKeyError::InvalidDidKey)?;
87
88 let mut signing = Secret::generate_ed25519(None, Some(seed));
90 signing.id = format!("{did}#{ed_pub_mb}");
91
92 let mut key_agreement = signing
94 .to_x25519()
95 .map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
96 let x_pub_mb = key_agreement
97 .get_public_keymultibase()
98 .map_err(|e| DidKeyError::X25519Conversion(e.to_string()))?;
99 key_agreement.id = format!("{did}#{x_pub_mb}");
100
101 Ok(DidKeySecrets {
102 signing,
103 key_agreement,
104 })
105}
106
107#[cfg(feature = "didcomm")]
134pub fn secrets_from_bundle(
135 bundle: &crate::did_secrets::DidSecretsBundle,
136) -> Result<Vec<affinidi_tdk::secrets_resolver::secrets::Secret>, DidKeyError> {
137 use affinidi_tdk::secrets_resolver::secrets::Secret;
138
139 let mut secrets = Vec::with_capacity(bundle.secrets.len());
140 for entry in &bundle.secrets {
141 decode_private_key_multibase(&entry.private_key_multibase)?;
145
146 let secret = Secret::from_multibase(&entry.private_key_multibase, Some(&entry.key_id))
151 .map_err(|e| DidKeyError::SecretConstruction(e.to_string()))?;
152 secrets.push(secret);
153 }
154 Ok(secrets)
155}
156
157#[derive(Debug)]
158pub enum DidKeyError {
159 Multibase(String),
160 InvalidSeedLength,
161 InvalidDidKey,
162 #[cfg(feature = "didcomm")]
163 X25519Conversion(String),
164 #[cfg(feature = "didcomm")]
165 SecretConstruction(String),
166}
167
168impl std::fmt::Display for DidKeyError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::Multibase(e) => write!(f, "invalid private key multibase: {e}"),
172 Self::InvalidSeedLength => write!(f, "private key seed must be 32 bytes"),
173 Self::InvalidDidKey => write!(f, "invalid did:key format"),
174 #[cfg(feature = "didcomm")]
175 Self::X25519Conversion(e) => write!(f, "X25519 conversion failed: {e}"),
176 #[cfg(feature = "didcomm")]
177 Self::SecretConstruction(e) => write!(f, "failed to construct secret from bundle: {e}"),
178 }
179 }
180}
181
182impl std::error::Error for DidKeyError {}
183
184#[cfg(feature = "client")]
191pub fn secret_from_key_response(
192 resp: &crate::client::GetKeySecretResponse,
193) -> Result<affinidi_tdk::secrets_resolver::secrets::Secret, DidKeyError> {
194 affinidi_tdk::secrets_resolver::secrets::Secret::from_multibase(
195 &resp.private_key_multibase,
196 None,
197 )
198 .map_err(|e| DidKeyError::Multibase(e.to_string()))
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_ed25519_multibase_pubkey_format() {
207 let key = [0u8; 32];
208 let result = ed25519_multibase_pubkey(&key);
209 assert!(result.starts_with('z'));
211
212 let (_, decoded) = multibase::decode(&result).unwrap();
213 assert_eq!(decoded.len(), 34);
214 assert_eq!(decoded[0], 0xed);
215 assert_eq!(decoded[1], 0x01);
216 assert_eq!(&decoded[2..], &key);
217 }
218
219 #[test]
220 fn test_decode_private_key_multibase_roundtrip() {
221 let seed = [42u8; 32];
222 let encoded = multibase::encode(multibase::Base::Base58Btc, seed);
223 let decoded = decode_private_key_multibase(&encoded).unwrap();
224 assert_eq!(decoded, seed);
225 }
226
227 #[test]
228 fn test_decode_private_key_multibase_with_codec_prefix() {
229 let seed = [42u8; 32];
230 let mut prefixed = Vec::with_capacity(34);
231 prefixed.extend_from_slice(&ED25519_PRIV_CODEC);
232 prefixed.extend_from_slice(&seed);
233 let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed);
234 let decoded = decode_private_key_multibase(&encoded).unwrap();
235 assert_eq!(decoded, seed);
236 }
237
238 #[test]
239 fn test_decode_private_key_multibase_invalid() {
240 let result = decode_private_key_multibase("!!!bad!!!");
241 assert!(result.is_err());
242 }
243
244 #[test]
245 fn test_decode_private_key_multibase_wrong_length() {
246 let encoded = multibase::encode(multibase::Base::Base58Btc, [1u8; 16]);
247 let result = decode_private_key_multibase(&encoded);
248 assert!(matches!(result, Err(DidKeyError::InvalidSeedLength)));
249 }
250
251 #[cfg(feature = "didcomm")]
262 #[test]
263 fn test_secrets_from_did_key_uses_multibase_fragment_ids() {
264 use affinidi_tdk::secrets_resolver::secrets::Secret;
265
266 let seed = [42u8; 32];
267 let ed_secret = Secret::generate_ed25519(None, Some(&seed));
268 let ed_pub_mb = ed_secret.get_public_keymultibase().unwrap();
269 let did = format!("did:key:{ed_pub_mb}");
270
271 let secrets = secrets_from_did_key(&did, &seed).expect("did:key secrets");
272
273 assert_eq!(secrets.signing.id, format!("{did}#{ed_pub_mb}"));
276 assert_ne!(secrets.signing.id, format!("{did}#key-0"));
277
278 assert!(
282 secrets.key_agreement.id.starts_with(&format!("{did}#z")),
283 "key_agreement.id should start with `{did}#z`, got: {}",
284 secrets.key_agreement.id
285 );
286 assert_ne!(secrets.key_agreement.id, format!("{did}#key-1"));
287 assert_ne!(secrets.key_agreement.id, secrets.signing.id);
288 }
289
290 #[cfg(feature = "didcomm")]
295 #[test]
296 fn test_secrets_from_did_key_is_deterministic() {
297 use affinidi_tdk::secrets_resolver::secrets::Secret;
298
299 let seed = [7u8; 32];
300 let ed_secret = Secret::generate_ed25519(None, Some(&seed));
301 let did = format!("did:key:{}", ed_secret.get_public_keymultibase().unwrap());
302
303 let a = secrets_from_did_key(&did, &seed).unwrap();
304 let b = secrets_from_did_key(&did, &seed).unwrap();
305 assert_eq!(a.signing.id, b.signing.id);
306 assert_eq!(a.key_agreement.id, b.key_agreement.id);
307 }
308
309 #[cfg(feature = "didcomm")]
310 #[test]
311 fn test_secrets_from_did_key_rejects_non_did_key() {
312 let seed = [1u8; 32];
313 let result = secrets_from_did_key("did:webvh:abc:example.com:vta", &seed);
314 assert!(matches!(result, Err(DidKeyError::InvalidDidKey)));
315 }
316
317 #[cfg(feature = "didcomm")]
324 #[test]
325 fn test_secrets_from_bundle_ed25519_and_x25519() {
326 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
327 use crate::keys::KeyType;
328 use affinidi_tdk::secrets_resolver::secrets::{KeyType as ResolverKeyType, Secret};
329
330 let did = "did:webvh:QmAbc:example.com:agent";
331
332 let ed_seed = [11u8; 32];
334 let mut ed_prefixed = Vec::with_capacity(34);
335 ed_prefixed.extend_from_slice(&ED25519_PRIV_CODEC);
336 ed_prefixed.extend_from_slice(&ed_seed);
337 let signing_mb = multibase::encode(multibase::Base::Base58Btc, &ed_prefixed);
338
339 let x_scalar = [22u8; 32];
341 let mut x_prefixed = Vec::with_capacity(34);
342 x_prefixed.extend_from_slice(&X25519_PRIV_CODEC);
343 x_prefixed.extend_from_slice(&x_scalar);
344 let ka_mb = multibase::encode(multibase::Base::Base58Btc, &x_prefixed);
345
346 let bundle = DidSecretsBundle {
347 did: did.to_string(),
348 secrets: vec![
349 SecretEntry {
350 key_id: format!("{did}#key-0"),
351 key_type: KeyType::Ed25519,
352 private_key_multibase: signing_mb.clone(),
353 },
354 SecretEntry {
355 key_id: format!("{did}#key-1"),
356 key_type: KeyType::X25519,
357 private_key_multibase: ka_mb,
358 },
359 ],
360 };
361
362 let secrets = secrets_from_bundle(&bundle).expect("bundle secrets");
363 assert_eq!(secrets.len(), 2, "signing + key-agreement");
364
365 assert_eq!(secrets[0].id, format!("{did}#key-0"));
367 assert_eq!(secrets[1].id, format!("{did}#key-1"));
368
369 assert_eq!(secrets[0].get_key_type(), ResolverKeyType::Ed25519);
371 assert_eq!(secrets[1].get_key_type(), ResolverKeyType::X25519);
372
373 let mut x_prefixed2 = Vec::with_capacity(34);
378 x_prefixed2.extend_from_slice(&X25519_PRIV_CODEC);
379 x_prefixed2.extend_from_slice(&x_scalar);
380 let same = Secret::from_multibase(
381 &multibase::encode(multibase::Base::Base58Btc, &x_prefixed2),
382 None,
383 )
384 .unwrap();
385 assert_eq!(
386 secrets[1].get_public_keymultibase().unwrap(),
387 same.get_public_keymultibase().unwrap(),
388 );
389 }
390
391 #[cfg(feature = "didcomm")]
395 #[test]
396 fn test_secrets_from_bundle_roundtrips_multibase() {
397 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
398 use crate::keys::KeyType;
399
400 let scalar = [7u8; 32];
401 let mut prefixed = Vec::with_capacity(34);
402 prefixed.extend_from_slice(&X25519_PRIV_CODEC);
403 prefixed.extend_from_slice(&scalar);
404 let mb = multibase::encode(multibase::Base::Base58Btc, &prefixed);
405
406 assert_eq!(decode_private_key_multibase(&mb).unwrap(), scalar);
408
409 let bundle = DidSecretsBundle {
410 did: "did:webvh:x:example.com:a".to_string(),
411 secrets: vec![SecretEntry {
412 key_id: "did:webvh:x:example.com:a#key-1".to_string(),
413 key_type: KeyType::X25519,
414 private_key_multibase: mb,
415 }],
416 };
417 let secrets = secrets_from_bundle(&bundle).unwrap();
418 assert_eq!(secrets.len(), 1);
419 assert_eq!(secrets[0].id, "did:webvh:x:example.com:a#key-1");
420 }
421
422 #[cfg(feature = "didcomm")]
425 #[test]
426 fn test_secrets_from_bundle_rejects_bad_multibase() {
427 use crate::did_secrets::{DidSecretsBundle, SecretEntry};
428 use crate::keys::KeyType;
429
430 let bundle = DidSecretsBundle {
431 did: "did:webvh:x:example.com:a".to_string(),
432 secrets: vec![SecretEntry {
433 key_id: "did:webvh:x:example.com:a#key-0".to_string(),
434 key_type: KeyType::Ed25519,
435 private_key_multibase: "!!!bad!!!".to_string(),
436 }],
437 };
438 assert!(matches!(
439 secrets_from_bundle(&bundle),
440 Err(DidKeyError::Multibase(_))
441 ));
442 }
443}