defuse_crypto/
public_key.rs

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