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::{Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1};
9
10#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
11#[near(serializers = [borsh])]
12#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
13#[cfg_attr(
14    feature = "serde",
15    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
16)]
17pub enum PublicKey {
18    Ed25519(<Ed25519 as Curve>::PublicKey),
19    Secp256k1(<Secp256k1 as Curve>::PublicKey),
20    P256(<P256 as Curve>::PublicKey),
21}
22
23impl PublicKey {
24    #[inline]
25    pub const fn curve_type(&self) -> CurveType {
26        match self {
27            Self::Ed25519(_) => CurveType::Ed25519,
28            Self::Secp256k1(_) => CurveType::Secp256k1,
29            Self::P256(_) => CurveType::P256,
30        }
31    }
32
33    #[inline]
34    const fn data(&self) -> &[u8] {
35        #[allow(clippy::match_same_arms)]
36        match self {
37            Self::Ed25519(data) => data,
38            Self::Secp256k1(data) => data,
39            Self::P256(data) => data,
40        }
41    }
42
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(&env::keccak256_array(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(&env::keccak256_array(&[b"p256".as_slice(), pk].concat())[12..32])
71                )
72            }
73        }
74        .try_into()
75        .unwrap_or_else(|_| unreachable!())
76    }
77
78    #[inline]
79    pub fn from_implicit_account_id(account_id: &AccountIdRef) -> Option<Self> {
80        let mut pk = [0; 32];
81        // Only NearImplicitAccount can be reversed
82        hex::decode_to_slice(account_id.as_str(), &mut pk).ok()?;
83        Some(Self::Ed25519(pk))
84    }
85}
86
87impl Debug for PublicKey {
88    #[inline]
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(
91            f,
92            "{}:{}",
93            self.curve_type(),
94            bs58::encode(self.data()).into_string()
95        )
96    }
97}
98
99impl Display for PublicKey {
100    #[inline]
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        fmt::Debug::fmt(self, f)
103    }
104}
105
106impl FromStr for PublicKey {
107    type Err = ParseCurveError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        let (curve, data) = if let Some((curve, data)) = s.split_once(':') {
111            (
112                curve.parse().map_err(|_| ParseCurveError::WrongCurveType)?,
113                data,
114            )
115        } else {
116            (CurveType::Ed25519, s)
117        };
118        let decoder = bs58::decode(data.as_bytes());
119        match curve {
120            CurveType::Ed25519 => decoder.into_array_const().map(Self::Ed25519),
121            CurveType::Secp256k1 => decoder.into_array_const().map(Self::Secp256k1),
122            CurveType::P256 => decoder.into_array_const().map(Self::P256),
123        }
124        .map_err(Into::into)
125    }
126}
127
128#[cfg(all(feature = "abi", not(target_arch = "wasm32")))]
129mod abi {
130    use super::*;
131
132    use near_sdk::{
133        schemars::{
134            JsonSchema,
135            r#gen::SchemaGenerator,
136            schema::{InstanceType, Metadata, Schema, SchemaObject},
137        },
138        serde_json,
139    };
140
141    impl JsonSchema for PublicKey {
142        fn schema_name() -> String {
143            String::schema_name()
144        }
145
146        fn is_referenceable() -> bool {
147            false
148        }
149
150        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
151            SchemaObject {
152                instance_type: Some(InstanceType::String.into()),
153                extensions: [("contentEncoding", "base58".into())]
154                    .into_iter()
155                    .map(|(k, v)| (k.to_string(), v))
156                    .collect(),
157                metadata: Some(
158                    Metadata {
159                        examples: [Self::example_ed25519(), Self::example_secp256k1()]
160                            .map(serde_json::to_value)
161                            .map(Result::unwrap)
162                            .into(),
163                        ..Default::default()
164                    }
165                    .into(),
166                ),
167                ..Default::default()
168            }
169            .into()
170        }
171    }
172
173    impl PublicKey {
174        pub(super) fn example_ed25519() -> Self {
175            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm"
176                .parse()
177                .unwrap()
178        }
179
180        pub(super) fn example_secp256k1() -> Self {
181            "secp256k1:5KN6ZfGZgH1puWwH1Nc1P8xyrFZSPHDw3WUP6iitsjCECJLrGBq"
182                .parse()
183                .unwrap()
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn implicit_ed25519() {
194        assert_eq!(
195            "ed25519:5TagutioHgKLh7KZ1VEFBYfgRkPtqnKm9LoMnJMJugxm"
196                .parse::<PublicKey>()
197                .unwrap()
198                .to_implicit_account_id(),
199            AccountIdRef::new_or_panic(
200                "423df0a6640e9467769c55a573f15b9ee999dc8970048959c72890abf5cc3a8e"
201            )
202        );
203    }
204
205    #[test]
206    fn implicit_secp256k1() {
207        assert_eq!(
208            "secp256k1:5KN6ZfGZgH1puWwH1Nc1P8xyrFZSPHDw3WUP6iitsjCECJLrGBq"
209                .parse::<PublicKey>()
210                .unwrap()
211                .to_implicit_account_id(),
212            AccountIdRef::new_or_panic("0xbff77166b39599e54e391156eef7b8191e02be92")
213        );
214    }
215
216    #[test]
217    fn implicit_p256() {
218        assert_eq!(
219            "p256:5KN6ZfGZgH1puWwH1Nc1P8xyrFZSPHDw3WUP6iitsjCECJLrGBq"
220                .parse::<PublicKey>()
221                .unwrap()
222                .to_implicit_account_id(),
223            AccountIdRef::new_or_panic("0x7edf07ede58238026db3f90fc8032633b69b8de5")
224        );
225    }
226}