Skip to main content

auths_crypto/
keri.rs

1//! KERI CESR Ed25519 key parsing.
2//!
3//! Decodes KERI-encoded public keys: 'D' derivation code prefix + base64url-no-pad
4//! encoded 32-byte Ed25519 key, as defined by the KERI CESR specification.
5
6use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
7
8/// Errors from decoding a KERI-encoded Ed25519 public key.
9#[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/// A validated KERI Ed25519 public key (32 bytes).
22///
23/// Args:
24/// * The inner `[u8; 32]` is the raw Ed25519 public key bytes, decoded from
25///   a KERI CESR-encoded string with 'D' derivation code prefix.
26///
27/// Usage:
28/// ```
29/// use auths_crypto::KeriPublicKey;
30///
31/// let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
32/// assert_eq!(key.as_bytes(), &[0u8; 32]);
33/// ```
34#[derive(Debug)]
35pub struct KeriPublicKey([u8; 32]);
36
37impl KeriPublicKey {
38    /// Parses a KERI-encoded Ed25519 public key string into a validated type.
39    ///
40    /// The input must be a 'D'-prefixed base64url-no-pad encoded 32-byte Ed25519 key,
41    /// as defined by the KERI CESR specification.
42    ///
43    /// Args:
44    /// * `encoded`: The KERI-encoded string (e.g., `"D<base64url>"`).
45    ///
46    /// Usage:
47    /// ```
48    /// use auths_crypto::KeriPublicKey;
49    ///
50    /// let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
51    /// let raw = key.as_bytes();
52    /// ```
53    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    /// Returns the raw 32-byte Ed25519 public key.
61    pub fn as_bytes(&self) -> &[u8; 32] {
62        &self.0
63    }
64
65    /// Consumes self and returns the inner 32-byte array.
66    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}