Skip to main content

affinidi_encoding/
multicodec.rs

1//! Multicodec encoding/decoding
2//!
3//! Multicodec is a self-describing format that prefixes data with a varint
4//! indicating the type of data that follows.
5//!
6//! See: <https://github.com/multiformats/multicodec>
7
8use crate::EncodingError;
9use serde::{Deserialize, Serialize};
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12// ****************************************************************************
13// Codec Magic Numbers
14// See: https://github.com/multiformats/multicodec/blob/master/table.csv
15// ****************************************************************************
16pub const ED25519_PUB: u64 = 0xed;
17pub const ED25519_PRIV: u64 = 0x1300;
18pub const X25519_PUB: u64 = 0xec;
19pub const X25519_PRIV: u64 = 0x1302;
20pub const SECP256K1_PUB: u64 = 0xe7;
21pub const SECP256K1_PRIV: u64 = 0x1301;
22pub const P256_PUB: u64 = 0x1200;
23pub const P256_PRIV: u64 = 0x1306;
24pub const P384_PUB: u64 = 0x1201;
25pub const P384_PRIV: u64 = 0x1307;
26pub const P521_PUB: u64 = 0x1202;
27pub const P521_PRIV: u64 = 0x1308;
28
29// Post-quantum codecs — draft entries from the official multicodec table.
30// We store ML-DSA private keys as the 32-byte seed, so we use the
31// `-priv-seed` codes (0x131a–0x131c), not the 2560/4032/4896-byte
32// expanded-private codes (0x1317–0x1319).
33//
34// SLH-DSA has no private-key codec registered; `Secret::from_multibase`
35// and `get_private_keymultibase` return an error for SLH-DSA keys.
36pub const ML_DSA_44_PUB: u64 = 0x1210;
37pub const ML_DSA_44_PRIV_SEED: u64 = 0x131a;
38pub const ML_DSA_65_PUB: u64 = 0x1211;
39pub const ML_DSA_65_PRIV_SEED: u64 = 0x131b;
40pub const ML_DSA_87_PUB: u64 = 0x1212;
41pub const ML_DSA_87_PRIV_SEED: u64 = 0x131c;
42pub const SLH_DSA_SHA2_128S_PUB: u64 = 0x1220;
43
44/// Known codec types
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46pub enum Codec {
47    Ed25519Pub,
48    Ed25519Priv,
49    X25519Pub,
50    X25519Priv,
51    Secp256k1Pub,
52    Secp256k1Priv,
53    P256Pub,
54    P256Priv,
55    P384Pub,
56    P384Priv,
57    P521Pub,
58    P521Priv,
59    MlDsa44Pub,
60    MlDsa44PrivSeed,
61    MlDsa65Pub,
62    MlDsa65PrivSeed,
63    MlDsa87Pub,
64    MlDsa87PrivSeed,
65    SlhDsaSha2_128sPub,
66    Unknown(u64),
67}
68
69impl Codec {
70    /// Convert a raw codec value to a Codec enum
71    pub fn from_u64(value: u64) -> Self {
72        match value {
73            ED25519_PUB => Codec::Ed25519Pub,
74            ED25519_PRIV => Codec::Ed25519Priv,
75            X25519_PUB => Codec::X25519Pub,
76            X25519_PRIV => Codec::X25519Priv,
77            SECP256K1_PUB => Codec::Secp256k1Pub,
78            SECP256K1_PRIV => Codec::Secp256k1Priv,
79            P256_PUB => Codec::P256Pub,
80            P256_PRIV => Codec::P256Priv,
81            P384_PUB => Codec::P384Pub,
82            P384_PRIV => Codec::P384Priv,
83            P521_PUB => Codec::P521Pub,
84            P521_PRIV => Codec::P521Priv,
85            ML_DSA_44_PUB => Codec::MlDsa44Pub,
86            ML_DSA_44_PRIV_SEED => Codec::MlDsa44PrivSeed,
87            ML_DSA_65_PUB => Codec::MlDsa65Pub,
88            ML_DSA_65_PRIV_SEED => Codec::MlDsa65PrivSeed,
89            ML_DSA_87_PUB => Codec::MlDsa87Pub,
90            ML_DSA_87_PRIV_SEED => Codec::MlDsa87PrivSeed,
91            SLH_DSA_SHA2_128S_PUB => Codec::SlhDsaSha2_128sPub,
92            other => Codec::Unknown(other),
93        }
94    }
95
96    /// Convert to raw u64 value
97    pub fn to_u64(self) -> u64 {
98        match self {
99            Codec::Ed25519Pub => ED25519_PUB,
100            Codec::Ed25519Priv => ED25519_PRIV,
101            Codec::X25519Pub => X25519_PUB,
102            Codec::X25519Priv => X25519_PRIV,
103            Codec::Secp256k1Pub => SECP256K1_PUB,
104            Codec::Secp256k1Priv => SECP256K1_PRIV,
105            Codec::P256Pub => P256_PUB,
106            Codec::P256Priv => P256_PRIV,
107            Codec::P384Pub => P384_PUB,
108            Codec::P384Priv => P384_PRIV,
109            Codec::P521Pub => P521_PUB,
110            Codec::P521Priv => P521_PRIV,
111            Codec::MlDsa44Pub => ML_DSA_44_PUB,
112            Codec::MlDsa44PrivSeed => ML_DSA_44_PRIV_SEED,
113            Codec::MlDsa65Pub => ML_DSA_65_PUB,
114            Codec::MlDsa65PrivSeed => ML_DSA_65_PRIV_SEED,
115            Codec::MlDsa87Pub => ML_DSA_87_PUB,
116            Codec::MlDsa87PrivSeed => ML_DSA_87_PRIV_SEED,
117            Codec::SlhDsaSha2_128sPub => SLH_DSA_SHA2_128S_PUB,
118            Codec::Unknown(v) => v,
119        }
120    }
121
122    /// Returns true if this is a public key codec
123    pub fn is_public(&self) -> bool {
124        matches!(
125            self,
126            Codec::Ed25519Pub
127                | Codec::X25519Pub
128                | Codec::Secp256k1Pub
129                | Codec::P256Pub
130                | Codec::P384Pub
131                | Codec::P521Pub
132                | Codec::MlDsa44Pub
133                | Codec::MlDsa65Pub
134                | Codec::MlDsa87Pub
135                | Codec::SlhDsaSha2_128sPub // SLH-DSA has no private codec registered
136        )
137    }
138
139    /// Returns the expected key length for this codec, if known
140    pub fn expected_key_length(&self) -> Option<usize> {
141        match self {
142            Codec::Ed25519Pub | Codec::Ed25519Priv => Some(32),
143            Codec::X25519Pub | Codec::X25519Priv => Some(32),
144            Codec::Secp256k1Pub => Some(33), // compressed
145            Codec::P256Pub => Some(33),      // compressed
146            Codec::P384Pub => Some(49),      // compressed
147            Codec::P521Pub => Some(67),      // compressed
148            // ML-DSA public keys: FIPS 204 fixed sizes
149            Codec::MlDsa44Pub => Some(1312),
150            Codec::MlDsa65Pub => Some(1952),
151            Codec::MlDsa87Pub => Some(2592),
152            // ML-DSA priv-seed codec (0x131a–0x131c): 32-byte seed (xi)
153            Codec::MlDsa44PrivSeed | Codec::MlDsa65PrivSeed | Codec::MlDsa87PrivSeed => Some(32),
154            // SLH-DSA-SHA2-128s public key: FIPS 205 (32 bytes)
155            Codec::SlhDsaSha2_128sPub => Some(32),
156            _ => None,
157        }
158    }
159}
160
161/// A multicodec-encoded byte slice (borrowed).
162///
163/// `#[repr(transparent)]` guarantees the same memory layout as `[u8]`,
164/// which `MultiEncoded::new` relies on when reinterpreting a borrowed
165/// byte slice as this DST.
166#[derive(Zeroize, ZeroizeOnDrop)]
167#[repr(transparent)]
168pub struct MultiEncoded([u8]);
169
170impl MultiEncoded {
171    /// Create a new multiencoded byte slice, validating the varint
172    /// prefix.
173    pub fn new(bytes: &[u8]) -> Result<&Self, EncodingError> {
174        unsigned_varint::decode::u64(bytes)
175            .map_err(|e| EncodingError::InvalidMulticodec(format!("varint decode: {e}")))?;
176
177        // SAFETY: `MultiEncoded` is a `#[repr(transparent)]` wrapper
178        // around `[u8]`, so `&[u8]` and `&MultiEncoded` have identical
179        // memory layout (including DST metadata). The varint prefix has
180        // been validated above, so every subsequent call into `parts()`
181        // will see well-formed bytes.
182        Ok(unsafe { &*(bytes as *const [u8] as *const MultiEncoded) })
183    }
184
185    /// Size of the byte array (including codec prefix)
186    pub fn len(&self) -> usize {
187        self.0.len()
188    }
189
190    /// Returns true if empty
191    pub fn is_empty(&self) -> bool {
192        self.0.is_empty()
193    }
194
195    /// Separates the codec and the data
196    pub fn parts(&self) -> (u64, &[u8]) {
197        unsigned_varint::decode::u64(&self.0).unwrap()
198    }
199
200    /// Raw codec value (u64)
201    pub fn codec(&self) -> u64 {
202        self.parts().0
203    }
204
205    /// Codec as typed enum
206    pub fn codec_type(&self) -> Codec {
207        Codec::from_u64(self.codec())
208    }
209
210    /// Data bytes (without codec prefix)
211    pub fn data(&self) -> &[u8] {
212        self.parts().1
213    }
214
215    /// Returns the raw bytes, including the codec prefix
216    pub fn as_bytes(&self) -> &[u8] {
217        &self.0
218    }
219}
220
221/// A multicodec-encoded byte buffer (owned)
222#[derive(Clone, Zeroize, ZeroizeOnDrop)]
223pub struct MultiEncodedBuf(Vec<u8>);
224
225impl MultiEncodedBuf {
226    /// Parse an existing multicodec-encoded buffer
227    pub fn new(bytes: Vec<u8>) -> Result<Self, EncodingError> {
228        unsigned_varint::decode::u64(&bytes)
229            .map_err(|e| EncodingError::InvalidMulticodec(format!("varint decode: {e}")))?;
230        Ok(Self(bytes))
231    }
232
233    /// Encode bytes with the given codec
234    pub fn encode(codec: Codec, bytes: &[u8]) -> Self {
235        Self::encode_raw(codec.to_u64(), bytes)
236    }
237
238    /// Encode bytes with a raw codec value (backwards-compatible alias)
239    pub fn encode_bytes(codec: u64, bytes: &[u8]) -> Self {
240        Self::encode_raw(codec, bytes)
241    }
242
243    /// Encode bytes with a raw codec value
244    pub fn encode_raw(codec: u64, bytes: &[u8]) -> Self {
245        let mut codec_buffer = [0u8; 10];
246        let encoded_codec = unsigned_varint::encode::u64(codec, &mut codec_buffer);
247        let mut result = Vec::with_capacity(encoded_codec.len() + bytes.len());
248        result.extend(encoded_codec);
249        result.extend(bytes);
250        Self(result)
251    }
252
253    /// Returns the raw bytes, including the codec prefix
254    /// Note: clones due to ZeroizeOnDrop
255    pub fn into_bytes(self) -> Vec<u8> {
256        self.0.clone()
257    }
258
259    /// Returns a reference to the raw bytes
260    pub fn as_bytes(&self) -> &[u8] {
261        &self.0
262    }
263
264    /// Borrow as MultiEncoded slice.
265    pub fn as_multi_encoded(&self) -> &MultiEncoded {
266        // SAFETY: `MultiEncoded` is `#[repr(transparent)]` over `[u8]`.
267        // The varint prefix was validated when this `MultiEncodedBuf`
268        // was constructed (see `MultiEncodedBuf::new` / `encode_raw`).
269        unsafe { &*(self.0.as_slice() as *const [u8] as *const MultiEncoded) }
270    }
271}
272
273impl AsRef<MultiEncoded> for MultiEncodedBuf {
274    fn as_ref(&self) -> &MultiEncoded {
275        self.as_multi_encoded()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_encode_decode_ed25519() {
285        let key_bytes = [0u8; 32];
286        let encoded = MultiEncodedBuf::encode(Codec::Ed25519Pub, &key_bytes);
287
288        let decoded = MultiEncoded::new(encoded.as_bytes()).unwrap();
289        assert_eq!(decoded.codec(), ED25519_PUB);
290        assert_eq!(decoded.codec_type(), Codec::Ed25519Pub);
291        assert_eq!(decoded.data(), &key_bytes);
292    }
293
294    #[test]
295    fn test_codec_roundtrip() {
296        for codec in [
297            Codec::Ed25519Pub,
298            Codec::X25519Pub,
299            Codec::P256Pub,
300            Codec::P384Pub,
301        ] {
302            let raw = codec.to_u64();
303            assert_eq!(Codec::from_u64(raw), codec);
304        }
305    }
306}