1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7
8#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
10pub enum KeriDecodeError {
11 #[error("Invalid KERI prefix: expected 'D' for Ed25519, got '{0}'")]
12 InvalidPrefix(char),
13 #[error("Missing KERI prefix: empty string")]
14 EmptyInput,
15 #[error("Base64url decode failed: {0}")]
16 DecodeError(String),
17 #[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
18 InvalidLength(usize),
19}
20
21#[derive(Debug)]
35pub struct KeriPublicKey([u8; 32]);
36
37impl KeriPublicKey {
38 pub fn parse(encoded: &str) -> Result<Self, KeriDecodeError> {
54 let payload = validate_and_strip_prefix(encoded)?;
55 let bytes = decode_base64url(payload)?;
56 let array = enforce_key_length(bytes)?;
57 Ok(Self(array))
58 }
59
60 pub fn as_bytes(&self) -> &[u8; 32] {
62 &self.0
63 }
64
65 pub fn into_bytes(self) -> [u8; 32] {
67 self.0
68 }
69}
70
71fn validate_and_strip_prefix(encoded: &str) -> Result<&str, KeriDecodeError> {
72 match encoded.strip_prefix('D') {
73 Some(payload) => Ok(payload),
74 None => match encoded.chars().next() {
75 Some(c) => Err(KeriDecodeError::InvalidPrefix(c)),
76 None => Err(KeriDecodeError::EmptyInput),
77 },
78 }
79}
80
81fn decode_base64url(payload: &str) -> Result<Vec<u8>, KeriDecodeError> {
82 URL_SAFE_NO_PAD
83 .decode(payload)
84 .map_err(|e| KeriDecodeError::DecodeError(e.to_string()))
85}
86
87fn enforce_key_length(bytes: Vec<u8>) -> Result<[u8; 32], KeriDecodeError> {
88 let len = bytes.len();
89 bytes
90 .try_into()
91 .map_err(|_| KeriDecodeError::InvalidLength(len))
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn parse_all_zeros() {
100 let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
101 assert_eq!(key.as_bytes(), &[0u8; 32]);
102 }
103
104 #[test]
105 fn roundtrip_into_bytes() {
106 let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
107 let bytes = key.into_bytes();
108 assert_eq!(bytes, [0u8; 32]);
109 }
110
111 #[test]
112 fn rejects_empty_input() {
113 let err = KeriPublicKey::parse("").unwrap_err();
114 assert_eq!(err, KeriDecodeError::EmptyInput);
115 }
116
117 #[test]
118 fn rejects_wrong_prefix() {
119 let err = KeriPublicKey::parse("Xsomething").unwrap_err();
120 assert!(matches!(err, KeriDecodeError::InvalidPrefix('X')));
121 }
122
123 #[test]
124 fn rejects_invalid_base64() {
125 let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
126 assert!(matches!(err, KeriDecodeError::DecodeError(_)));
127 }
128}