Skip to main content

defuse_core/
public_key.rs

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