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")))]
130const _: () = {
131    use near_sdk::{
132        schemars::{
133            JsonSchema,
134            r#gen::SchemaGenerator,
135            schema::{InstanceType, Metadata, Schema, SchemaObject},
136        },
137        serde_json,
138    };
139
140    impl JsonSchema for PublicKey {
141        fn schema_name() -> String {
142            String::schema_name()
143        }
144
145        fn is_referenceable() -> bool {
146            false
147        }
148
149        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
150            SchemaObject {
151                instance_type: Some(InstanceType::String.into()),
152                extensions: [("contentEncoding", "base58".into())]
153                    .into_iter()
154                    .map(|(k, v)| (k.to_string(), v))
155                    .collect(),
156                metadata: Some(
157                    Metadata {
158                        examples: [Self::example_ed25519(), Self::example_secp256k1()]
159                            .map(serde_json::to_value)
160                            .map(Result::unwrap)
161                            .into(),
162                        ..Default::default()
163                    }
164                    .into(),
165                ),
166                ..Default::default()
167            }
168            .into()
169        }
170    }
171
172    impl PublicKey {
173        pub(super) fn example_ed25519() -> Self {
174            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm"
175                .parse()
176                .unwrap()
177        }
178
179        pub(super) fn example_secp256k1() -> Self {
180            "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3"
181                .parse()
182                .unwrap()
183        }
184    }
185};
186
187#[cfg(test)]
188mod tests {
189    use rstest::rstest;
190
191    use super::*;
192
193    #[rstest]
194    #[case(
195        "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm",
196        "423df0a6640e9467769c55a573f15b9ee999dc8970048959c72890abf5cc3a8e"
197    )]
198    #[case(
199        "secp256k1:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
200        "0xbff77166b39599e54e391156eef7b8191e02be92"
201    )]
202    #[case(
203        "p256:3aMVMxsoAnHUbweXMtdKaN1uJaNwsfKv7wnc97SDGjXhyK62VyJwhPUPLZefKVthcoUcuWK6cqkSU4M542ipNxS3",
204        "0x7edf07ede58238026db3f90fc8032633b69b8de5"
205    )]
206    fn to_implicit_account_id(#[case] pk: &str, #[case] expected: &str) {
207        assert_eq!(
208            pk.parse::<PublicKey>().unwrap().to_implicit_account_id(),
209            AccountIdRef::new_or_panic(expected)
210        );
211    }
212
213    #[rstest]
214    fn parse_invalid_length(
215        #[values(
216            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJ",
217            "ed25519:",
218            "secp256k1:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs",
219            "secp256k1:",
220            "p256:p3UPfBR3kWxE2C8wF1855eguaoRvoW6jV5ZXbu3sTTCs",
221            "p256:"
222        )]
223        pk: &str,
224    ) {
225        assert_eq!(pk.parse::<PublicKey>(), Err(ParseCurveError::InvalidLength));
226    }
227}