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: std::iter::once(("contentEncoding", "base58".into()))
182                    .map(|(k, v)| (k.to_string(), v))
183                    .collect(),
184                metadata: Some(
185                    Metadata {
186                        examples: [
187                            #[cfg(feature = "ed25519")]
188                            Self::example_ed25519(),
189                            #[cfg(feature = "secp256k1")]
190                            Self::example_secp256k1(),
191                            #[cfg(feature = "p256")]
192                            Self::example_p256(),
193                        ]
194                        .map(serde_json::to_value)
195                        .map(Result::unwrap)
196                        .into(),
197                        ..Default::default()
198                    }
199                    .into(),
200                ),
201                ..Default::default()
202            }
203            .into()
204        }
205    }
206
207    impl PublicKey {
208        #[cfg(feature = "ed25519")]
209        pub(super) fn example_ed25519() -> Self {
210            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm"
211                .parse()
212                .unwrap()
213        }
214
215        #[cfg(feature = "secp256k1")]
216        pub(super) fn example_secp256k1() -> Self {
217            "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3"
218                .parse()
219                .unwrap()
220        }
221
222        #[cfg(feature = "p256")]
223        pub(super) fn example_p256() -> Self {
224            "p256:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3"
225                .parse()
226                .unwrap()
227        }
228    }
229};
230
231#[cfg(feature = "near-api-types")]
232const _: () = {
233    use near_api_types::crypto::public_key::{
234        ED25519PublicKey, PublicKey as NearPublicKey, Secp256K1PublicKey,
235    };
236
237    impl From<NearPublicKey> for PublicKey {
238        fn from(pk: NearPublicKey) -> Self {
239            match pk {
240                #[cfg(feature = "ed25519")]
241                NearPublicKey::ED25519(pk) => pk.into(),
242                #[cfg(feature = "secp256k1")]
243                NearPublicKey::SECP256K1(pk) => pk.into(),
244            }
245        }
246    }
247
248    impl From<ED25519PublicKey> for PublicKey {
249        fn from(pk: ED25519PublicKey) -> Self {
250            Self::Ed25519(pk.0)
251        }
252    }
253
254    impl From<Secp256K1PublicKey> for PublicKey {
255        fn from(pk: Secp256K1PublicKey) -> Self {
256            Self::Secp256k1(pk.0)
257        }
258    }
259};
260
261#[cfg(test)]
262mod tests {
263    use near_sdk::AccountIdRef;
264    use rstest::rstest;
265
266    use super::*;
267
268    #[rstest]
269    #[cfg_attr(
270        feature = "ed25519",
271        case(
272            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm",
273            "423df0a6640e9467769c55a573f15b9ee999dc8970048959c72890abf5cc3a8e"
274        )
275    )]
276    #[cfg_attr(
277        feature = "secp256k1",
278        case(
279            "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
280            "0xbff77166b39599e54e391156eef7b8191e02be92"
281        )
282    )]
283    #[cfg_attr(
284        feature = "p256",
285        case(
286            "p256:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
287            "0x7edf07ede58238026db3f90fc8032633b69b8de5"
288        )
289    )]
290    fn to_implicit_account_id(#[case] pk: &str, #[case] expected: &str) {
291        assert_eq!(
292            pk.parse::<PublicKey>().unwrap().to_implicit_account_id(),
293            AccountIdRef::new_or_panic(expected)
294        );
295    }
296
297    #[rstest]
298    #[cfg_attr(
299        feature = "ed25519",
300        case("ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJ"),
301        case("ed25519:")
302    )]
303    #[cfg_attr(
304        feature = "secp256k1",
305        case("secp256k1:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs"),
306        case("secp256k1:")
307    )]
308    #[cfg_attr(
309        feature = "p256",
310        case("p256:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs"),
311        case("p256:")
312    )]
313    fn parse_invalid_length(#[case] pk: &str) {
314        assert_eq!(pk.parse::<PublicKey>(), Err(ParseCurveError::InvalidLength));
315    }
316}