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