Skip to main content

sui_crypto/
suipriv.rs

1//! Encoding and decoding helpers for the serialized Sui private key formats.
2//!
3//! Both formats carry the same payload of `flag || private_key`, where the
4//! leading flag byte is the `SignatureScheme` flag for the contained key
5//! (`0x00` for Ed25519, `0x01` for Secp256k1, `0x02` for Secp256r1). The
6//! payload has two wire encodings, mirroring the Sui CLI and the `sui-types`
7//! crate:
8//!
9//! - `bech32`: a Bech32 (BIP-173) string with the human-readable part
10//!   `suiprivkey`, produced by `sui keytool generate` and `sui keytool
11//!   export`.
12//! - `base64`: a plain Base64 string, the legacy encoding used for entries
13//!   of the Sui CLI's `sui.keystore` file and by older versions of `sui
14//!   keytool`.
15//!
16//! These helpers are kept `pub(crate)` on purpose. Callers reach the formats
17//! through strongly-typed wrappers like `Ed25519PrivateKey::from_suiprivkey`
18//! or `SimpleKeypair::from_base64` to avoid handing raw key bytes back as a
19//! `Vec<u8>` at the public boundary.
20
21use sui_sdk_types::SignatureScheme;
22
23use crate::SignatureError;
24
25/// Join a scheme flag and key bytes into a `flag || private_key` payload.
26fn join_payload(scheme: SignatureScheme, key: &[u8]) -> Vec<u8> {
27    let mut payload = Vec::with_capacity(1 + key.len());
28    payload.push(scheme.to_u8());
29    payload.extend_from_slice(key);
30    payload
31}
32
33/// Split a `flag || private_key` payload into its scheme flag and key bytes.
34///
35/// The returned key bytes are not validated against the scheme — the caller
36/// is responsible for verifying the length and constructing a scheme-specific
37/// key from them.
38fn split_payload(bytes: &[u8]) -> Result<(SignatureScheme, Vec<u8>), SignatureError> {
39    let (flag, key) = bytes
40        .split_first()
41        .ok_or_else(|| SignatureError::from_source("private key payload is empty"))?;
42    let scheme = SignatureScheme::from_byte(*flag).map_err(|e| {
43        SignatureError::from_source(format!("invalid private key scheme flag: {e}"))
44    })?;
45    Ok((scheme, key.to_vec()))
46}
47
48/// The human-readable part of the `suiprivkey` Bech32 encoding.
49#[cfg(feature = "bech32")]
50const HRP: &str = "suiprivkey";
51
52#[cfg(feature = "bech32")]
53fn hrp() -> bech32::Hrp {
54    // "suiprivkey" is a valid Bech32 HRP (lowercase ASCII, length 10).
55    bech32::Hrp::parse(HRP).expect("`suiprivkey` is a valid Bech32 HRP")
56}
57
58/// Encode a `flag || private_key` payload as a Bech32 `suiprivkey` string.
59#[cfg(feature = "bech32")]
60pub(crate) fn encode(scheme: SignatureScheme, key: &[u8]) -> Result<String, SignatureError> {
61    bech32::encode::<bech32::Bech32>(hrp(), &join_payload(scheme, key))
62        .map_err(|e| SignatureError::from_source(format!("bech32 encode failed: {e}")))
63}
64
65/// Decode a Bech32 `suiprivkey` string into its scheme flag and key bytes.
66///
67/// The BIP-173 checksum is validated strictly; Bech32m-checksummed strings are
68/// rejected.
69#[cfg(feature = "bech32")]
70pub(crate) fn decode(s: &str) -> Result<(SignatureScheme, Vec<u8>), SignatureError> {
71    let parsed = bech32::primitives::decode::CheckedHrpstring::new::<bech32::Bech32>(s)
72        .map_err(|e| SignatureError::from_source(format!("invalid suiprivkey string: {e}")))?;
73
74    if parsed.hrp() != hrp() {
75        return Err(SignatureError::from_source(format!(
76            "expected `{HRP}` human-readable part",
77        )));
78    }
79
80    let bytes: Vec<u8> = parsed.byte_iter().collect();
81    split_payload(&bytes)
82}
83
84/// Encode a `flag || private_key` payload as a legacy Base64 keystore string.
85pub(crate) fn encode_base64(scheme: SignatureScheme, key: &[u8]) -> String {
86    use base64ct::Encoding;
87
88    base64ct::Base64::encode_string(&join_payload(scheme, key))
89}
90
91/// Decode a legacy Base64 keystore string into its scheme flag and key bytes.
92pub(crate) fn decode_base64(s: &str) -> Result<(SignatureScheme, Vec<u8>), SignatureError> {
93    use base64ct::Encoding;
94
95    let bytes = base64ct::Base64::decode_vec(s).map_err(|e| {
96        SignatureError::from_source(format!("invalid base64 private key string: {e}"))
97    })?;
98    split_payload(&bytes)
99}