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