Skip to main content

codec_multikey/
parse.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use codec_multibase::multibase_to_bytes;
6use codec_multicodec::{lookup_codec_prefix, KeyMaterialKind, VARIABLE_KEY_LENGTH};
7
8use crate::encode::MAX_RSA_PUBLIC_KEY_DER_LEN;
9use crate::error::{classify_multikey_codec, MultikeyError};
10
11/// A multikey decoded into its codec metadata and raw public key bytes.
12pub struct ParsedMultikey {
13    /// Canonical multicodec name of the key type (e.g. `ed25519-pub`).
14    pub codec_name: &'static str,
15    /// Human-readable algorithm name implied by the codec (e.g. `Ed25519`).
16    pub alg: &'static str,
17    /// Raw public key bytes with the multicodec prefix stripped.
18    pub public_key: Vec<u8>,
19    /// Expected public key length for the codec.
20    pub key_length: usize,
21}
22
23/// Parses a multibase-encoded multikey string into its codec and key bytes.
24///
25/// Fails closed: returns an error on non-canonical multibase, unknown codec
26/// prefix, or a key length that does not match the codec.
27pub fn parse_multikey(multibase_key: &str) -> Result<ParsedMultikey, MultikeyError> {
28    if multibase_key.len() < 2 {
29        return Err(MultikeyError::InvalidMultibase);
30    }
31    // Multikey has a single canonical string form: base58btc (`z`).
32    // Rejecting alternate multibase alphabets prevents duplicate encodings of
33    // the same key from bypassing string-based blocklists or deduplication.
34    if !multibase_key.starts_with('z') {
35        return Err(MultikeyError::InvalidMultibase);
36    }
37
38    // 1) multibase decode
39    let raw = multibase_to_bytes(multibase_key).map_err(|_| MultikeyError::InvalidMultibase)?;
40
41    if raw.len() < 2 {
42        return Err(MultikeyError::DecodedTooShort(raw.len()));
43    }
44
45    // 2) multicodec prefix lookup
46    let found = lookup_codec_prefix(&raw).ok_or(MultikeyError::UnknownCodecPrefix)?;
47
48    if found.key_material != KeyMaterialKind::PublicKey {
49        return Err(MultikeyError::UnknownCodecPrefix);
50    }
51
52    let public_key = raw[found.codec.len()..].to_vec();
53
54    // 3) key length validation
55    if found.key_length == VARIABLE_KEY_LENGTH && public_key.is_empty() {
56        return Err(MultikeyError::KeyLengthMismatch {
57            codec: classify_multikey_codec(found.name),
58            expected: found.key_length,
59            actual: public_key.len(),
60        });
61    }
62
63    if found.name == "rsa-pub" && public_key.len() > MAX_RSA_PUBLIC_KEY_DER_LEN {
64        return Err(MultikeyError::KeyTooLarge {
65            codec: classify_multikey_codec(found.name),
66            max: MAX_RSA_PUBLIC_KEY_DER_LEN,
67            actual: public_key.len(),
68        });
69    }
70
71    if found.key_length != VARIABLE_KEY_LENGTH && public_key.len() != found.key_length {
72        return Err(MultikeyError::KeyLengthMismatch {
73            codec: classify_multikey_codec(found.name),
74            expected: found.key_length,
75            actual: public_key.len(),
76        });
77    }
78
79    Ok(ParsedMultikey {
80        codec_name: found.name,
81        alg: found.alg,
82        public_key,
83        key_length: found.key_length,
84    })
85}