defuse_crypto/
public_key.rs

1use core::{
2    fmt::{self, Debug, Display},
3    str::FromStr,
4};
5
6use near_sdk::{AccountId, AccountIdRef, bs58, env, near};
7
8use crate::{
9    Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1, parse::checked_base58_decode_array,
10};
11
12#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
13#[near(serializers = [borsh])]
14#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
15#[cfg_attr(
16    feature = "serde",
17    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
18)]
19pub enum PublicKey {
20    Ed25519(<Ed25519 as Curve>::PublicKey),
21    Secp256k1(<Secp256k1 as Curve>::PublicKey),
22    P256(<P256 as Curve>::PublicKey),
23}
24
25impl PublicKey {
26    #[inline]
27    pub const fn curve_type(&self) -> CurveType {
28        match self {
29            Self::Ed25519(_) => CurveType::Ed25519,
30            Self::Secp256k1(_) => CurveType::Secp256k1,
31            Self::P256(_) => CurveType::P256,
32        }
33    }
34
35    #[inline]
36    const fn data(&self) -> &[u8] {
37        #[allow(clippy::match_same_arms)]
38        match self {
39            Self::Ed25519(data) => data,
40            Self::Secp256k1(data) => data,
41            Self::P256(data) => data,
42        }
43    }
44
45    #[inline]
46    pub fn to_implicit_account_id(&self) -> AccountId {
47        match self {
48            Self::Ed25519(pk) => {
49                // https://docs.near.org/concepts/protocol/account-id#implicit-address
50                hex::encode(pk)
51            }
52            Self::Secp256k1(pk) => {
53                // https://ethereum.org/en/developers/docs/accounts/#account-creation
54                format!("0x{}", hex::encode(&env::keccak256_array(pk)[12..32]))
55            }
56            Self::P256(pk) => {
57                // In order to keep compatibility with all existing standards
58                // within Near ecosystem (e.g. NEP-245), we need our implicit
59                // account_ids to be fully backwards-compatible with Near's
60                // implicit AccountId.
61                //
62                // To avoid introducing new implicit account id types, we
63                // reuse existing Eth Implicit schema with same hash func.
64                // To avoid collisions between addresses for different curves,
65                // we add "p256" ("\x70\x32\x35\x36") prefix to the public key
66                // before hashing.
67                //
68                // So, the final schema looks like:
69                // "0x" .. hex(keccak256("p256" .. pk)[12..32])
70                format!(
71                    "0x{}",
72                    hex::encode(&env::keccak256_array(&[b"p256".as_slice(), pk].concat())[12..32])
73                )
74            }
75        }
76        .try_into()
77        .unwrap_or_else(|_| unreachable!())
78    }
79
80    #[inline]
81    pub fn from_implicit_account_id(account_id: &AccountIdRef) -> Option<Self> {
82        let mut pk = [0; 32];
83        // Only NearImplicitAccount can be reversed
84        hex::decode_to_slice(account_id.as_str(), &mut pk).ok()?;
85        Some(Self::Ed25519(pk))
86    }
87}
88
89impl Debug for PublicKey {
90    #[inline]
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(
93            f,
94            "{}:{}",
95            self.curve_type(),
96            bs58::encode(self.data()).into_string()
97        )
98    }
99}
100
101impl Display for PublicKey {
102    #[inline]
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        fmt::Debug::fmt(self, f)
105    }
106}
107
108impl FromStr for PublicKey {
109    type Err = ParseCurveError;
110
111    fn from_str(s: &str) -> Result<Self, Self::Err> {
112        let (curve, data) = if let Some((curve, data)) = s.split_once(':') {
113            (
114                curve.parse().map_err(|_| ParseCurveError::WrongCurveType)?,
115                data,
116            )
117        } else {
118            (CurveType::Ed25519, s)
119        };
120
121        match curve {
122            CurveType::Ed25519 => checked_base58_decode_array(data).map(Self::Ed25519),
123            CurveType::Secp256k1 => checked_base58_decode_array(data).map(Self::Secp256k1),
124            CurveType::P256 => checked_base58_decode_array(data).map(Self::P256),
125        }
126    }
127}
128
129#[cfg(all(feature = "abi", not(target_arch = "wasm32")))]
130mod abi {
131    use super::*;
132
133    use near_sdk::{
134        schemars::{
135            JsonSchema,
136            r#gen::SchemaGenerator,
137            schema::{InstanceType, Metadata, Schema, SchemaObject},
138        },
139        serde_json,
140    };
141
142    impl JsonSchema for PublicKey {
143        fn schema_name() -> String {
144            String::schema_name()
145        }
146
147        fn is_referenceable() -> bool {
148            false
149        }
150
151        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
152            SchemaObject {
153                instance_type: Some(InstanceType::String.into()),
154                extensions: [("contentEncoding", "base58".into())]
155                    .into_iter()
156                    .map(|(k, v)| (k.to_string(), v))
157                    .collect(),
158                metadata: Some(
159                    Metadata {
160                        examples: [Self::example_ed25519(), Self::example_secp256k1()]
161                            .map(serde_json::to_value)
162                            .map(Result::unwrap)
163                            .into(),
164                        ..Default::default()
165                    }
166                    .into(),
167                ),
168                ..Default::default()
169            }
170            .into()
171        }
172    }
173
174    impl PublicKey {
175        pub(super) fn example_ed25519() -> Self {
176            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm"
177                .parse()
178                .unwrap()
179        }
180
181        pub(super) fn example_secp256k1() -> Self {
182            "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3"
183                .parse()
184                .unwrap()
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use rstest::rstest;
192
193    use super::*;
194
195    #[rstest]
196    #[case(
197        "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm",
198        "423df0a6640e9467769c55a573f15b9ee999dc8970048959c72890abf5cc3a8e"
199    )]
200    #[case(
201        "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
202        "0xbff77166b39599e54e391156eef7b8191e02be92"
203    )]
204    #[case(
205        "p256:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
206        "0x7edf07ede58238026db3f90fc8032633b69b8de5"
207    )]
208    fn to_implicit_account_id(#[case] pk: &str, #[case] expected: &str) {
209        assert_eq!(
210            pk.parse::<PublicKey>().unwrap().to_implicit_account_id(),
211            AccountIdRef::new_or_panic(expected)
212        );
213    }
214
215    #[rstest]
216    fn parse_invalid_length(
217        #[values(
218            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJ",
219            "ed25519:",
220            "secp256k1:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs",
221            "secp256k1:",
222            "p256:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs",
223            "p256:"
224        )]
225        pk: &str,
226    ) {
227        assert_eq!(pk.parse::<PublicKey>(), Err(ParseCurveError::InvalidLength));
228    }
229}