defuse_core/
public_key.rs

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