1const ED25519_MULTICODEC: [u8; 2] = [0xED, 0x01];
9
10#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum DidKeyError {
14 #[error("DID must start with 'did:key:z', got: {0}")]
15 InvalidPrefix(String),
16
17 #[error("Base58 decoding failed: {0}")]
18 Base58DecodeFailed(String),
19
20 #[error("Unsupported or malformed multicodec: expected Ed25519 [0xED, 0x01]")]
21 UnsupportedMulticodec,
22
23 #[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
24 InvalidKeyLength(usize),
25}
26
27impl crate::AuthsErrorInfo for DidKeyError {
28 fn error_code(&self) -> &'static str {
29 match self {
30 Self::InvalidPrefix(_) => "AUTHS-E1101",
31 Self::Base58DecodeFailed(_) => "AUTHS-E1102",
32 Self::UnsupportedMulticodec => "AUTHS-E1103",
33 Self::InvalidKeyLength(_) => "AUTHS-E1104",
34 }
35 }
36
37 fn suggestion(&self) -> Option<&'static str> {
38 match self {
39 Self::InvalidPrefix(_) => Some("DID must start with 'did:key:z'"),
40 Self::UnsupportedMulticodec => Some("Only Ed25519 keys are supported"),
41 _ => None,
42 }
43 }
44}
45
46pub fn did_key_to_ed25519(did: &str) -> Result<[u8; 32], DidKeyError> {
56 let encoded = strip_did_key_prefix(did)?;
57 let decoded = decode_base58(encoded)?;
58 validate_multicodec_and_extract(&decoded)
59}
60
61pub fn ed25519_pubkey_to_did_key(public_key: &[u8; 32]) -> String {
72 let mut prefixed = vec![0xED, 0x01];
73 prefixed.extend_from_slice(public_key);
74 let encoded = bs58::encode(prefixed).into_string();
75 format!("did:key:z{encoded}")
76}
77
78pub fn ed25519_pubkey_to_did_keri(pk: &[u8]) -> String {
83 format!("did:keri:{}", bs58::encode(pk).into_string())
84}
85
86fn strip_did_key_prefix(did: &str) -> Result<&str, DidKeyError> {
87 did.strip_prefix("did:key:z")
88 .ok_or_else(|| DidKeyError::InvalidPrefix(did.to_string()))
89}
90
91fn decode_base58(encoded: &str) -> Result<Vec<u8>, DidKeyError> {
92 bs58::decode(encoded)
93 .into_vec()
94 .map_err(|e| DidKeyError::Base58DecodeFailed(e.to_string()))
95}
96
97fn validate_multicodec_and_extract(decoded: &[u8]) -> Result<[u8; 32], DidKeyError> {
98 if decoded.len() != 34
99 || decoded[0] != ED25519_MULTICODEC[0]
100 || decoded[1] != ED25519_MULTICODEC[1]
101 {
102 if decoded.len() != 34 {
103 return Err(DidKeyError::InvalidKeyLength(
104 decoded.len().saturating_sub(2),
105 ));
106 }
107 return Err(DidKeyError::UnsupportedMulticodec);
108 }
109
110 let mut key = [0u8; 32];
111 key.copy_from_slice(&decoded[2..]);
112 Ok(key)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn roundtrip_encode_decode() {
121 let original = [42u8; 32];
122 let did = ed25519_pubkey_to_did_key(&original);
123 assert!(did.starts_with("did:key:z"));
124 let decoded = did_key_to_ed25519(&did).unwrap();
125 assert_eq!(decoded, original);
126 }
127
128 #[test]
129 fn rejects_invalid_prefix() {
130 let err = did_key_to_ed25519("did:web:example.com").unwrap_err();
131 assert!(matches!(err, DidKeyError::InvalidPrefix(_)));
132 }
133
134 #[test]
135 fn rejects_invalid_base58() {
136 let err = did_key_to_ed25519("did:key:z0OOO").unwrap_err();
137 assert!(matches!(err, DidKeyError::Base58DecodeFailed(_)));
138 }
139}