use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeriDecodeError {
#[error("Invalid KERI prefix: expected 'D' for Ed25519, got '{0}'")]
InvalidPrefix(char),
#[error("Missing KERI prefix: empty string")]
EmptyInput,
#[error("Base64url decode failed: {0}")]
DecodeError(String),
#[error("Invalid Ed25519 key length: expected 32 bytes, got {0}")]
InvalidLength(usize),
}
impl crate::AuthsErrorInfo for KeriDecodeError {
fn error_code(&self) -> &'static str {
match self {
Self::InvalidPrefix(_) => "AUTHS-E1201",
Self::EmptyInput => "AUTHS-E1202",
Self::DecodeError(_) => "AUTHS-E1203",
Self::InvalidLength(_) => "AUTHS-E1204",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::InvalidPrefix(_) => Some("KERI Ed25519 keys must start with 'D' prefix"),
Self::EmptyInput => Some("Provide a non-empty KERI-encoded key string"),
_ => None,
}
}
}
#[derive(Debug)]
pub struct KeriPublicKey([u8; 32]);
impl KeriPublicKey {
pub fn parse(encoded: &str) -> Result<Self, KeriDecodeError> {
let payload = validate_and_strip_prefix(encoded)?;
let bytes = decode_base64url(payload)?;
let array = enforce_key_length(bytes)?;
Ok(Self(array))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn into_bytes(self) -> [u8; 32] {
self.0
}
}
fn validate_and_strip_prefix(encoded: &str) -> Result<&str, KeriDecodeError> {
match encoded.strip_prefix('D') {
Some(payload) => Ok(payload),
None => match encoded.chars().next() {
Some(c) => Err(KeriDecodeError::InvalidPrefix(c)),
None => Err(KeriDecodeError::EmptyInput),
},
}
}
fn decode_base64url(payload: &str) -> Result<Vec<u8>, KeriDecodeError> {
URL_SAFE_NO_PAD
.decode(payload)
.map_err(|e| KeriDecodeError::DecodeError(e.to_string()))
}
fn enforce_key_length(bytes: Vec<u8>) -> Result<[u8; 32], KeriDecodeError> {
let len = bytes.len();
bytes
.try_into()
.map_err(|_| KeriDecodeError::InvalidLength(len))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_all_zeros() {
let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
assert_eq!(key.as_bytes(), &[0u8; 32]);
}
#[test]
fn roundtrip_into_bytes() {
let key = KeriPublicKey::parse("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap();
let bytes = key.into_bytes();
assert_eq!(bytes, [0u8; 32]);
}
#[test]
fn rejects_empty_input() {
let err = KeriPublicKey::parse("").unwrap_err();
assert_eq!(err, KeriDecodeError::EmptyInput);
}
#[test]
fn rejects_wrong_prefix() {
let err = KeriPublicKey::parse("Xsomething").unwrap_err();
assert!(matches!(err, KeriDecodeError::InvalidPrefix('X')));
}
#[test]
fn rejects_invalid_base64() {
let err = KeriPublicKey::parse("D!!!invalid!!!").unwrap_err();
assert!(matches!(err, KeriDecodeError::DecodeError(_)));
}
}