1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7
8#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum KeriDecodeError {
12 #[error("Invalid KERI prefix: expected 'D' for Ed25519, got '{0}'")]
13 InvalidPrefix(char),
14 #[error("Missing KERI prefix: empty string")]
15 EmptyInput,
16 #[error("Base64url decode failed: {0}")]
17 DecodeError(String),
18 #[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
19 InvalidLength(usize),
20}
21
22impl crate::AuthsErrorInfo for KeriDecodeError {
23 fn error_code(&self) -> &'static str {
24 match self {
25 Self::InvalidPrefix(_) => "AUTHS-E1201",
26 Self::EmptyInput => "AUTHS-E1202",
27 Self::DecodeError(_) => "AUTHS-E1203",
28 Self::InvalidLength(_) => "AUTHS-E1204",
29 }
30 }
31
32 fn suggestion(&self) -> Option<&'static str> {
33 match self {
34 Self::InvalidPrefix(_) => Some("KERI Ed25519 keys must start with 'D' prefix"),
35 Self::EmptyInput => Some("Provide a non-empty KERI-encoded key string"),
36 _ => None,
37 }
38 }
39}
40
41#[derive(Debug)]
55pub struct KeriPublicKey([u8; 32]);
56
57impl KeriPublicKey {
58 pub fn parse(encoded: &str) -> Result<Self, KeriDecodeError> {
74 let payload = validate_and_strip_prefix(encoded)?;
75 let bytes = decode_base64url(payload)?;
76 let array = enforce_key_length(bytes)?;
77 Ok(Self(array))
78 }
79
80 pub fn as_bytes(&self) -> &[u8; 32] {
82 &self.0
83 }
84
85 pub fn into_bytes(self) -> [u8; 32] {
87 self.0
88 }
89}
90
91fn validate_and_strip_prefix(encoded: &str) -> Result<&str, KeriDecodeError> {
92 match encoded.strip_prefix('D') {
93 Some(payload) => Ok(payload),
94 None => match encoded.chars().next() {
95 Some(c) => Err(KeriDecodeError::InvalidPrefix(c)),
96 None => Err(KeriDecodeError::EmptyInput),
97 },
98 }
99}
100
101fn decode_base64url(payload: &str) -> Result<Vec<u8>, KeriDecodeError> {
102 URL_SAFE_NO_PAD
103 .decode(payload)
104 .map_err(|e| KeriDecodeError::DecodeError(e.to_string()))
105}
106
107fn enforce_key_length(bytes: Vec<u8>) -> Result<[u8; 32], KeriDecodeError> {
108 let len = bytes.len();
109 bytes
110 .try_into()
111 .map_err(|_| KeriDecodeError::InvalidLength(len))
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn parse_all_zeros() {
120 let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
121 assert_eq!(key.as_bytes(), &[0u8; 32]);
122 }
123
124 #[test]
125 fn roundtrip_into_bytes() {
126 let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
127 let bytes = key.into_bytes();
128 assert_eq!(bytes, [0u8; 32]);
129 }
130
131 #[test]
132 fn rejects_empty_input() {
133 let err = KeriPublicKey::parse("").unwrap_err();
134 assert_eq!(err, KeriDecodeError::EmptyInput);
135 }
136
137 #[test]
138 fn rejects_wrong_prefix() {
139 let err = KeriPublicKey::parse("Xsomething").unwrap_err();
140 assert!(matches!(err, KeriDecodeError::InvalidPrefix('X')));
141 }
142
143 #[test]
144 fn rejects_invalid_base64() {
145 let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
146 assert!(matches!(err, KeriDecodeError::DecodeError(_)));
147 }
148}