Skip to main content

stellar_strkey/
ed25519.rs

1use crate::{
2    convert::{binary_len, decode, decode_zeroizing, encode, encode_len, encode_zeroizing},
3    error::DecodeError,
4    unredacted::Unredacted,
5    version,
6};
7
8use core::{
9    fmt::{Debug, Display},
10    str::FromStr,
11};
12use heapless::{String, Vec};
13use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
14
15/// An Ed25519 private key (raw 32-byte seed).
16///
17/// # Zeroize
18///
19/// `PrivateKey` derives [`Zeroize`] and [`ZeroizeOnDrop`]: the 32 seed bytes
20/// are overwritten with zeroes when a value is dropped.
21/// [`from_string`](Self::from_string) and [`from_slice`](Self::from_slice)
22/// zero their intermediate scratch buffers when they return.
23/// [`Unredacted::write_string`] is the encoding path that wraps its scratch
24/// buffers in [`Zeroizing`] and writes directly into a caller-provided
25/// buffer, avoiding any return-value move.
26///
27/// [`Debug`] emits `PrivateKey([REDACTED])`. To render the encoded strkey
28/// form, serialize via `serde`, or emit the raw seed bytes in any form,
29/// wrap the value in [`Unredacted`] — see [`Unredacted`]'s doc for the full
30/// list of paths that opt-in unlocks.
31#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Zeroize, ZeroizeOnDrop)]
32#[cfg_attr(feature = "serde", derive(serde_with::DeserializeFromStr))]
33pub struct PrivateKey(pub [u8; 32]);
34
35impl Debug for PrivateKey {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        f.write_str("PrivateKey([REDACTED])")
38    }
39}
40
41impl PrivateKey {
42    pub(crate) const PAYLOAD_LEN: usize = 32;
43    pub(crate) const BINARY_LEN: usize = binary_len(Self::PAYLOAD_LEN);
44    pub const ENCODED_LEN: usize = encode_len(Self::BINARY_LEN);
45    const _ASSERTS: () = {
46        assert!(Self::BINARY_LEN == 35);
47        assert!(Self::ENCODED_LEN == 56);
48    };
49}
50
51impl Unredacted<&PrivateKey> {
52    /// Encodes this private key to its strkey string form.
53    ///
54    /// # Zeroize
55    ///
56    /// The intermediate scratch buffers used during encoding are zeroed on
57    /// drop, but the returned `String` itself is plain — its bytes are not
58    /// zeroed when the value is dropped. Use
59    /// [`write_string`](Self::write_string) for zeroizing.
60    pub fn to_string(&self) -> String<{ PrivateKey::ENCODED_LEN }> {
61        let mut zeroizing: Zeroizing<String<{ PrivateKey::ENCODED_LEN }>> =
62            Zeroizing::new(String::new());
63        self.write_string(&mut zeroizing);
64        let mut out: String<{ PrivateKey::ENCODED_LEN }> = String::new();
65        out.push_str(&zeroizing).unwrap();
66        out
67    }
68
69    /// Encodes this private key to its strkey string form, writing the
70    /// result into the caller-provided buffer.
71    ///
72    /// # Zeroize
73    ///
74    /// The intermediate scratch buffers used during encoding are wrapped in
75    /// [`Zeroizing`] and zeroed on drop, and the encoded bytes are written
76    /// directly into `out` rather than returned by value, so no copy is left
77    /// on this method's stack frame.
78    pub fn write_string(&self, out: &mut Zeroizing<String<{ PrivateKey::ENCODED_LEN }>>) {
79        encode_zeroizing::<
80            { PrivateKey::PAYLOAD_LEN },
81            { PrivateKey::BINARY_LEN },
82            { PrivateKey::ENCODED_LEN },
83        >(version::PRIVATE_KEY_ED25519, &self.0 .0, out);
84    }
85}
86
87impl PrivateKey {
88    pub fn from_payload(payload: &[u8]) -> Result<Self, DecodeError> {
89        match payload.try_into() {
90            Ok(ed25519) => Ok(Self(ed25519)),
91            Err(_) => Err(DecodeError::InvalidPayloadLength),
92        }
93    }
94
95    pub fn from_string(s: &str) -> Result<Self, DecodeError> {
96        Self::from_slice(s.as_bytes())
97    }
98
99    pub fn from_slice(s: &[u8]) -> Result<Self, DecodeError> {
100        let mut payload: Zeroizing<Vec<u8, { Self::PAYLOAD_LEN }>> = Zeroizing::new(Vec::new());
101        let ver = decode_zeroizing::<{ Self::PAYLOAD_LEN }, { Self::BINARY_LEN }>(s, &mut payload)?;
102        match ver {
103            version::PRIVATE_KEY_ED25519 => Self::from_payload(&payload),
104            _ => Err(DecodeError::UnsupportedVersion),
105        }
106    }
107
108    /// Borrows this private key as an [`Unredacted`] wrapper so it can be
109    /// rendered via [`Display`] or [`to_string`](Unredacted::to_string), or
110    /// serialized in its strkey string form.
111    pub fn as_unredacted(&self) -> Unredacted<&Self> {
112        Unredacted(self)
113    }
114}
115
116impl Display for Unredacted<&PrivateKey> {
117    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118        let mut buf: Zeroizing<String<{ PrivateKey::ENCODED_LEN }>> = Zeroizing::new(String::new());
119        self.write_string(&mut buf);
120        f.write_str(&buf)
121    }
122}
123
124impl Debug for Unredacted<&PrivateKey> {
125    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
126        write!(f, "PrivateKey(")?;
127        for b in &self.0 .0 {
128            write!(f, "{b:02x}")?;
129        }
130        write!(f, ")")
131    }
132}
133
134impl Display for Unredacted<PrivateKey> {
135    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136        Display::fmt(&Unredacted(&self.0), f)
137    }
138}
139
140impl Debug for Unredacted<PrivateKey> {
141    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
142        Debug::fmt(&Unredacted(&self.0), f)
143    }
144}
145
146impl FromStr for PrivateKey {
147    type Err = DecodeError;
148
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        PrivateKey::from_string(s)
151    }
152}
153
154#[cfg(feature = "serde-decoded")]
155mod private_key_decoded_serde_impl {
156    use super::*;
157    use crate::{decoded_json_format::Decoded, unredacted::Unredacted};
158    use serde::{Deserialize, Deserializer, Serialize, Serializer};
159    use serde_with::serde_as;
160
161    #[serde_as]
162    #[derive(Serialize)]
163    #[serde(transparent)]
164    struct DecodedBorrowed<'a>(#[serde_as(as = "serde_with::hex::Hex")] &'a [u8; 32]);
165
166    #[serde_as]
167    #[derive(Deserialize)]
168    #[serde(transparent)]
169    struct DecodedOwned(#[serde_as(as = "serde_with::hex::Hex")] [u8; 32]);
170
171    impl Serialize for Decoded<Unredacted<&PrivateKey>> {
172        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
173            let Self(Unredacted(PrivateKey(bytes))) = self;
174            DecodedBorrowed(bytes).serialize(serializer)
175        }
176    }
177
178    impl<'de> Deserialize<'de> for Decoded<Unredacted<PrivateKey>> {
179        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
180            let DecodedOwned(bytes) = DecodedOwned::deserialize(deserializer)?;
181            Ok(Decoded(Unredacted(PrivateKey(bytes))))
182        }
183    }
184}
185
186/// An ed25519 public key (`G...`).
187#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
188#[cfg_attr(
189    feature = "serde",
190    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
191)]
192pub struct PublicKey(pub [u8; 32]);
193
194impl Debug for PublicKey {
195    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
196        write!(f, "PublicKey(")?;
197        for b in &self.0 {
198            write!(f, "{b:02x}")?;
199        }
200        write!(f, ")")
201    }
202}
203
204impl PublicKey {
205    pub(crate) const PAYLOAD_LEN: usize = 32;
206    pub(crate) const BINARY_LEN: usize = binary_len(Self::PAYLOAD_LEN);
207    pub const ENCODED_LEN: usize = encode_len(Self::BINARY_LEN);
208    const _ASSERTS: () = {
209        assert!(Self::BINARY_LEN == 35);
210        assert!(Self::ENCODED_LEN == 56);
211    };
212
213    pub fn to_string(&self) -> String<{ Self::ENCODED_LEN }> {
214        encode::<{ Self::PAYLOAD_LEN }, { Self::BINARY_LEN }, { Self::ENCODED_LEN }>(
215            version::PUBLIC_KEY_ED25519,
216            &self.0,
217        )
218    }
219
220    pub fn from_payload(payload: &[u8]) -> Result<Self, DecodeError> {
221        match payload.try_into() {
222            Ok(ed25519) => Ok(Self(ed25519)),
223            Err(_) => Err(DecodeError::InvalidPayloadLength),
224        }
225    }
226
227    pub fn from_string(s: &str) -> Result<Self, DecodeError> {
228        Self::from_slice(s.as_bytes())
229    }
230
231    pub fn from_slice(s: &[u8]) -> Result<Self, DecodeError> {
232        let (ver, payload) = decode::<{ Self::PAYLOAD_LEN }, { Self::BINARY_LEN }>(s)?;
233        match ver {
234            version::PUBLIC_KEY_ED25519 => Self::from_payload(&payload),
235            _ => Err(DecodeError::UnsupportedVersion),
236        }
237    }
238}
239
240impl Display for PublicKey {
241    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
242        write!(f, "{}", self.to_string())
243    }
244}
245
246impl FromStr for PublicKey {
247    type Err = DecodeError;
248
249    fn from_str(s: &str) -> Result<Self, Self::Err> {
250        PublicKey::from_string(s)
251    }
252}
253
254#[cfg(feature = "serde-decoded")]
255mod public_key_decoded_serde_impl {
256    use super::*;
257    use crate::decoded_json_format::Decoded;
258    use serde::{Deserialize, Deserializer, Serialize, Serializer};
259    use serde_with::serde_as;
260
261    #[serde_as]
262    #[derive(Serialize)]
263    #[serde(transparent)]
264    struct DecodedBorrowed<'a>(#[serde_as(as = "serde_with::hex::Hex")] &'a [u8; 32]);
265
266    #[serde_as]
267    #[derive(Deserialize)]
268    #[serde(transparent)]
269    struct DecodedOwned(#[serde_as(as = "serde_with::hex::Hex")] [u8; 32]);
270
271    impl Serialize for Decoded<&PublicKey> {
272        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
273            let Self(PublicKey(bytes)) = self;
274            DecodedBorrowed(bytes).serialize(serializer)
275        }
276    }
277
278    impl<'de> Deserialize<'de> for Decoded<PublicKey> {
279        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
280            let DecodedOwned(bytes) = DecodedOwned::deserialize(deserializer)?;
281            Ok(Decoded(PublicKey(bytes)))
282        }
283    }
284}
285
286/// A muxed ed25519 account (`M...`).
287#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
288#[cfg_attr(
289    feature = "serde",
290    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
291)]
292pub struct MuxedAccount {
293    pub ed25519: [u8; 32],
294    pub id: u64,
295}
296
297impl Debug for MuxedAccount {
298    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
299        write!(f, "MuxedAccount(")?;
300        for b in &self.ed25519 {
301            write!(f, "{b:02x}")?;
302        }
303        write!(f, ", {}", self.id)?;
304        write!(f, ")")
305    }
306}
307
308impl MuxedAccount {
309    pub(crate) const PAYLOAD_LEN: usize = 32 + 8; // ed25519 + id
310    pub(crate) const BINARY_LEN: usize = binary_len(Self::PAYLOAD_LEN);
311    pub const ENCODED_LEN: usize = encode_len(Self::BINARY_LEN);
312    const _ASSERTS: () = {
313        assert!(Self::BINARY_LEN == 43);
314        assert!(Self::ENCODED_LEN == 69);
315    };
316
317    pub fn to_string(&self) -> String<{ Self::ENCODED_LEN }> {
318        let mut payload: [u8; Self::PAYLOAD_LEN] = [0; Self::PAYLOAD_LEN];
319        let (ed25519, id) = payload.split_at_mut(32);
320        ed25519.copy_from_slice(&self.ed25519);
321        id.copy_from_slice(&self.id.to_be_bytes());
322        encode::<{ Self::PAYLOAD_LEN }, { Self::BINARY_LEN }, { Self::ENCODED_LEN }>(
323            version::MUXED_ACCOUNT_ED25519,
324            &payload,
325        )
326    }
327
328    pub fn from_payload(payload: &[u8]) -> Result<Self, DecodeError> {
329        if payload.len() != 40 {
330            return Err(DecodeError::InvalidPayloadLength);
331        }
332        let (ed25519, id) = payload.split_at(32);
333        Ok(Self {
334            ed25519: ed25519
335                .try_into()
336                .map_err(|_| DecodeError::InvalidPayloadLength)?,
337            id: u64::from_be_bytes(
338                id.try_into()
339                    .map_err(|_| DecodeError::InvalidPayloadLength)?,
340            ),
341        })
342    }
343
344    pub fn from_string(s: &str) -> Result<Self, DecodeError> {
345        Self::from_slice(s.as_bytes())
346    }
347
348    pub fn from_slice(s: &[u8]) -> Result<Self, DecodeError> {
349        let (ver, payload) = decode::<{ Self::PAYLOAD_LEN }, { Self::BINARY_LEN }>(s)?;
350        match ver {
351            version::MUXED_ACCOUNT_ED25519 => Self::from_payload(&payload),
352            _ => Err(DecodeError::UnsupportedVersion),
353        }
354    }
355}
356
357impl Display for MuxedAccount {
358    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
359        write!(f, "{}", self.to_string())
360    }
361}
362
363impl FromStr for MuxedAccount {
364    type Err = DecodeError;
365
366    fn from_str(s: &str) -> Result<Self, Self::Err> {
367        MuxedAccount::from_string(s)
368    }
369}
370
371#[cfg(feature = "serde-decoded")]
372mod muxed_account_decoded_serde_impl {
373    use super::*;
374    use crate::decoded_json_format::Decoded;
375    use serde::{Deserialize, Deserializer, Serialize, Serializer};
376    use serde_with::serde_as;
377
378    #[serde_as]
379    #[derive(Serialize)]
380    struct DecodedBorrowed<'a> {
381        #[serde_as(as = "serde_with::hex::Hex")]
382        ed25519: &'a [u8; 32],
383        id: u64,
384    }
385
386    #[serde_as]
387    #[derive(Deserialize)]
388    struct DecodedOwned {
389        #[serde_as(as = "serde_with::hex::Hex")]
390        ed25519: [u8; 32],
391        id: u64,
392    }
393
394    impl Serialize for Decoded<&MuxedAccount> {
395        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
396            let Self(MuxedAccount { ed25519, id }) = self;
397            DecodedBorrowed { ed25519, id: *id }.serialize(serializer)
398        }
399    }
400
401    impl<'de> Deserialize<'de> for Decoded<MuxedAccount> {
402        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
403            let DecodedOwned { ed25519, id } = DecodedOwned::deserialize(deserializer)?;
404            Ok(Decoded(MuxedAccount { ed25519, id }))
405        }
406    }
407}
408
409/// Stores a signed payload ed25519 signer.
410///
411/// The inner payload must be 1..=64 bytes. Empty payloads are not valid per
412/// stellar-core (SetOptionsOpFrame rejects them with SET_OPTIONS_BAD_SIGNER).
413#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
414#[cfg_attr(
415    feature = "serde",
416    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
417)]
418pub struct SignedPayload {
419    ed25519: [u8; 32],
420    payload: Vec<u8, 64>,
421}
422
423impl Debug for SignedPayload {
424    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
425        write!(f, "SignedPayload(")?;
426        for b in &self.ed25519 {
427            write!(f, "{b:02x}")?;
428        }
429        write!(f, ", ")?;
430        for b in &self.payload {
431            write!(f, "{b:02x}")?;
432        }
433        write!(f, ")")
434    }
435}
436
437impl SignedPayload {
438    // Max payload: 32 ed25519 + 4 len + 64 inner payload = 100
439    pub(crate) const MAX_PAYLOAD_LEN: usize = 32 + 4 + 64;
440    pub(crate) const MAX_BINARY_LEN: usize = binary_len(Self::MAX_PAYLOAD_LEN);
441    pub const MAX_ENCODED_LEN: usize = encode_len(Self::MAX_BINARY_LEN);
442    const MIN_INNER_PAYLOAD_LEN: usize = 1;
443    const MAX_INNER_PAYLOAD_LEN: usize = 64;
444    const MAX_INNER_PAYLOAD_LEN_U32: u32 = 64;
445    const _ASSERTS: () = {
446        assert!(Self::MAX_PAYLOAD_LEN == 100);
447        assert!(Self::MAX_BINARY_LEN == 103);
448        assert!(Self::MAX_ENCODED_LEN == 165);
449        assert!(Self::MAX_INNER_PAYLOAD_LEN as u32 == Self::MAX_INNER_PAYLOAD_LEN_U32);
450        assert!(Self::MAX_INNER_PAYLOAD_LEN_U32 as usize == Self::MAX_INNER_PAYLOAD_LEN);
451    };
452
453    /// Constructs a SignedPayload from an ed25519 public key and inner payload.
454    ///
455    /// ### Errors
456    ///
457    /// If the inner payload is empty or larger than 64 bytes.
458    pub fn new(ed25519: [u8; 32], payload: &[u8]) -> Result<Self, DecodeError> {
459        if !(Self::MIN_INNER_PAYLOAD_LEN..=Self::MAX_INNER_PAYLOAD_LEN).contains(&payload.len()) {
460            return Err(DecodeError::InvalidPayloadLength);
461        }
462        let mut p = Vec::new();
463        p.extend_from_slice(payload)
464            .map_err(|_| DecodeError::InvalidPayloadLength)?;
465        Ok(Self {
466            ed25519,
467            payload: p,
468        })
469    }
470
471    /// Returns the ed25519 public key.
472    pub fn ed25519(&self) -> &[u8; 32] {
473        &self.ed25519
474    }
475
476    /// Returns the inner payload.
477    pub fn payload(&self) -> &[u8] {
478        &self.payload
479    }
480
481    /// Returns the strkey string for the signed payload signer.
482    pub fn to_string(&self) -> String<{ Self::MAX_ENCODED_LEN }> {
483        let inner_payload_len = self.payload.len();
484        let payload_len = 32 + 4 + inner_payload_len + (4 - inner_payload_len % 4) % 4;
485
486        let inner_payload_len_u32: u32 = inner_payload_len as u32;
487
488        // Max payload_len is 100 (32 + 4 + 64), use fixed array
489        let mut payload = [0u8; Self::MAX_PAYLOAD_LEN];
490        payload[..32].copy_from_slice(&self.ed25519);
491        payload[32..32 + 4].copy_from_slice(&(inner_payload_len_u32).to_be_bytes());
492        payload[32 + 4..32 + 4 + inner_payload_len].copy_from_slice(&self.payload);
493
494        encode::<{ Self::MAX_PAYLOAD_LEN }, { Self::MAX_BINARY_LEN }, { Self::MAX_ENCODED_LEN }>(
495            version::SIGNED_PAYLOAD_ED25519,
496            &payload[..payload_len],
497        )
498    }
499
500    /// Decodes a signed payload from raw bytes.
501    ///
502    /// ### Errors
503    ///
504    /// If the inner payload is empty or larger than 64 bytes, if the overall
505    /// layout is malformed (wrong total length, truncated fields), or if the
506    /// trailing padding bytes are not all zero.
507    pub fn from_payload(payload: &[u8]) -> Result<Self, DecodeError> {
508        // Min: 32-byte ed25519 key + 4-byte length prefix + 4 bytes (1-byte inner
509        // payload padded to 4 per XDR). Empty inner payloads are not valid per
510        // stellar-core (SetOptionsOpFrame rejects them with SET_OPTIONS_BAD_SIGNER).
511        // Max: 32-byte ed25519 key + 4-byte length prefix + 64-byte inner payload.
512        const MIN_LENGTH: usize = 32 + 4 + 4;
513        const MAX_LENGTH: usize = 32 + 4 + SignedPayload::MAX_INNER_PAYLOAD_LEN;
514        let payload_len = payload.len();
515        if !(MIN_LENGTH..=MAX_LENGTH).contains(&payload_len) {
516            return Err(DecodeError::InvalidPayloadLength);
517        }
518
519        // Decode ed25519 public key. 32 bytes.
520        let mut offset = 0;
521        let ed25519: [u8; 32] = payload
522            .get(offset..offset + 32)
523            .ok_or(DecodeError::InvalidPayloadLength)?
524            .try_into()
525            .map_err(|_| DecodeError::InvalidPayloadLength)?;
526        offset += 32;
527
528        // Decode inner payload length. 4 bytes.
529        let inner_payload_len = u32::from_be_bytes(
530            payload
531                .get(offset..offset + 4)
532                .ok_or(DecodeError::InvalidPayloadLength)?
533                .try_into()
534                .map_err(|_| DecodeError::InvalidPayloadLength)?,
535        );
536        offset += 4;
537
538        // Check inner payload length is inside accepted range.
539        if inner_payload_len > Self::MAX_INNER_PAYLOAD_LEN_U32 {
540            return Err(DecodeError::InvalidPayloadLength);
541        }
542
543        // Decode inner payload.
544        let inner_payload = payload
545            .get(offset..offset + inner_payload_len as usize)
546            .ok_or(DecodeError::InvalidPayloadLength)?;
547        offset += inner_payload_len as usize;
548
549        // Calculate padding at end of inner payload. 0-3 bytes.
550        let padding_len = (4 - inner_payload_len % 4) % 4;
551
552        // Decode padding.
553        let padding = payload
554            .get(offset..offset + padding_len as usize)
555            .ok_or(DecodeError::InvalidPayloadLength)?;
556        offset += padding_len as usize;
557
558        // Check padding is all zeros.
559        if padding.iter().any(|b| *b != 0) {
560            return Err(DecodeError::InvalidPadding);
561        }
562
563        // Check that entire payload consumed.
564        if offset != payload_len {
565            return Err(DecodeError::InvalidPayloadLength);
566        }
567
568        Self::new(ed25519, inner_payload)
569    }
570
571    pub fn from_string(s: &str) -> Result<Self, DecodeError> {
572        Self::from_slice(s.as_bytes())
573    }
574
575    pub fn from_slice(s: &[u8]) -> Result<Self, DecodeError> {
576        let (ver, payload) = decode::<{ Self::MAX_PAYLOAD_LEN }, { Self::MAX_BINARY_LEN }>(s)?;
577        match ver {
578            version::SIGNED_PAYLOAD_ED25519 => Self::from_payload(&payload),
579            _ => Err(DecodeError::UnsupportedVersion),
580        }
581    }
582}
583
584impl Display for SignedPayload {
585    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
586        write!(f, "{}", self.to_string())
587    }
588}
589
590impl FromStr for SignedPayload {
591    type Err = DecodeError;
592
593    fn from_str(s: &str) -> Result<Self, Self::Err> {
594        SignedPayload::from_string(s)
595    }
596}
597
598#[cfg(feature = "serde-decoded")]
599mod signed_payload_decoded_serde_impl {
600    use super::SignedPayload;
601    use crate::decoded_json_format::Decoded;
602    use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
603    use serde_with::serde_as;
604
605    #[serde_as]
606    #[derive(Serialize)]
607    struct DecodedBorrowed<'a> {
608        #[serde_as(as = "serde_with::hex::Hex")]
609        ed25519: &'a [u8; 32],
610        #[serde_as(as = "serde_with::hex::Hex")]
611        payload: &'a [u8],
612    }
613
614    #[serde_as]
615    #[derive(Deserialize)]
616    struct DecodedOwned {
617        #[serde_as(as = "serde_with::hex::Hex")]
618        ed25519: [u8; 32],
619        #[serde_as(as = "serde_with::hex::Hex")]
620        payload: alloc::vec::Vec<u8>,
621    }
622
623    impl Serialize for Decoded<&SignedPayload> {
624        fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
625            let Self(sp) = self;
626            DecodedBorrowed {
627                ed25519: sp.ed25519(),
628                payload: sp.payload(),
629            }
630            .serialize(serializer)
631        }
632    }
633
634    impl<'de> Deserialize<'de> for Decoded<SignedPayload> {
635        fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
636            let DecodedOwned { ed25519, payload } = DecodedOwned::deserialize(deserializer)?;
637            let sp = SignedPayload::new(ed25519, &payload)
638                .map_err(|e| de::Error::custom(format_args!("invalid signed payload: {e}")))?;
639            Ok(Decoded(sp))
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::PrivateKey;
647    use heapless::String;
648    use zeroize::Zeroizing;
649
650    /// `write_string` must produce the same strkey bytes as `to_string`.
651    /// Only the buffer-zeroization story differs.
652    #[test]
653    fn test_private_key_write_string_matches_to_string() {
654        let key = PrivateKey([
655            0x69, 0xa8, 0xc4, 0xcb, 0xb9, 0xf6, 0x4e, 0x8a, 0x07, 0x98, 0xf6, 0xe1, 0xac, 0x65,
656            0xd0, 0x6c, 0x31, 0x62, 0x92, 0x90, 0x56, 0xbc, 0xf4, 0xcd, 0xb7, 0xd3, 0x73, 0x8d,
657            0x18, 0x55, 0xf3, 0x63,
658        ]);
659        let mut buf: Zeroizing<String<{ PrivateKey::ENCODED_LEN }>> = Zeroizing::new(String::new());
660        key.as_unredacted().write_string(&mut buf);
661        assert_eq!(
662            buf.as_str(),
663            "SBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHOKR"
664        );
665        assert_eq!(buf.as_str(), key.as_unredacted().to_string().as_str());
666    }
667}