1use anyhow::{anyhow, Result};
2
3use super::AlgorithmBytes;
4
5const KEY_SEPARATOR: char = '.';
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum KeyAlgorithm {
9 Ed25519,
10}
11
12pub fn parse_key(value: &str) -> Result<AlgorithmBytes<KeyAlgorithm>> {
13 let (algorithm_str, value_b58) = value
14 .split_once(KEY_SEPARATOR)
15 .ok_or_else(|| anyhow!("Key must use '{{algorithm}}.{{base58}}' format"))?;
16 let algorithm = match algorithm_str {
17 "ed25519" => KeyAlgorithm::Ed25519,
18 other => return Err(anyhow!("Unsupported key algorithm: '{}'", other)),
19 };
20 if value_b58.is_empty() {
21 return Err(anyhow!("Key payload must not be empty"));
22 }
23
24 let bytes = bs58::decode(value_b58)
25 .into_vec()
26 .map_err(|e| anyhow!("Invalid key base58 payload: {}", e))?;
27 if bytes.len() != 32 {
28 return Err(anyhow!(
29 "Invalid key length for {}: expected 32 bytes, got {}",
30 algorithm.prefix(),
31 bytes.len()
32 ));
33 }
34
35 Ok(AlgorithmBytes { algorithm, bytes })
36}
37
38pub fn format_key(algorithm: KeyAlgorithm, bytes: &[u8]) -> String {
39 format!(
40 "{}.{}",
41 algorithm.prefix(),
42 bs58::encode(bytes).into_string()
43 )
44}
45
46impl KeyAlgorithm {
47 fn prefix(self) -> &'static str {
48 match self {
49 Self::Ed25519 => "ed25519",
50 }
51 }
52}
53
54#[cfg(test)]
55#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
56mod tests {
57
58 use super::*;
59
60 #[test]
61 fn test_parse_key_roundtrip() {
62 let input = "ed25519.2p3NPZceQ6njbPg8aMFsEynX3Cmv6uCt1XMGHhPcL4AT";
63 let parsed = parse_key(input).unwrap();
64 assert_eq!(parsed.algorithm, KeyAlgorithm::Ed25519);
65 assert_eq!(format_key(parsed.algorithm, &parsed.bytes), input);
66 }
67
68 #[test]
69 fn test_parse_key_rejects_wrong_length() {
70 assert!(parse_key("ed25519.5Hue").is_err());
71 }
72}