p2panda_encryption/crypto/
x25519.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Elliptic-curve Diffie–Hellman (ECDH) key agreement scheme (X25519).
4use std::fmt;
5use std::hash::Hash as StdHash;
6
7use curve25519_dalek::scalar::clamp_integer;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::crypto::Secret;
12use crate::{Rng, RngError};
13
14/// 256-bit secret key size.
15pub const SECRET_KEY_SIZE: usize = 32;
16
17/// 256-bit public key size.
18pub const PUBLIC_KEY_SIZE: usize = 32;
19
20/// 256-bit shared secret size.
21pub const SHARED_SECRET_SIZE: usize = 32;
22
23/// Secret Curve25519 key used for ECDH key agreement.
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25pub struct SecretKey(Secret<SECRET_KEY_SIZE>);
26
27impl SecretKey {
28    #[cfg(not(feature = "test_utils"))]
29    pub(crate) fn from_bytes(bytes: [u8; SECRET_KEY_SIZE]) -> Self {
30        SecretKey(Secret::from_bytes(clamp_integer(bytes)))
31    }
32
33    #[cfg(feature = "test_utils")]
34    pub fn from_bytes(bytes: [u8; SECRET_KEY_SIZE]) -> Self {
35        SecretKey(Secret::from_bytes(clamp_integer(bytes)))
36    }
37
38    pub fn from_rng(rng: &Rng) -> Result<Self, RngError> {
39        Ok(Self::from_bytes(rng.random_array()?))
40    }
41
42    pub(crate) fn as_bytes(&self) -> &[u8; SECRET_KEY_SIZE] {
43        self.0.as_bytes()
44    }
45
46    pub fn public_key(&self) -> Result<PublicKey, X25519Error> {
47        let static_secret = x25519_dalek::StaticSecret::from(*self.0.as_bytes());
48        let public_key = x25519_dalek::PublicKey::from(&static_secret);
49        Ok(PublicKey(public_key.to_bytes()))
50    }
51
52    pub fn calculate_agreement(
53        &self,
54        their_public: &PublicKey,
55    ) -> Result<[u8; SHARED_SECRET_SIZE], X25519Error> {
56        let static_secret = x25519_dalek::StaticSecret::from(*self.0.as_bytes());
57        let shared_secret =
58            static_secret.diffie_hellman(&x25519_dalek::PublicKey::from(their_public.to_bytes()));
59        Ok(shared_secret.to_bytes())
60    }
61}
62
63/// Public Curve25519 key used for ECDH key agreement.
64#[derive(Copy, Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
65pub struct PublicKey(#[serde(with = "serde_bytes")] [u8; PUBLIC_KEY_SIZE]);
66
67impl PublicKey {
68    pub fn from_bytes(public_key: [u8; PUBLIC_KEY_SIZE]) -> Self {
69        Self(public_key)
70    }
71
72    pub fn as_bytes(&self) -> &[u8; PUBLIC_KEY_SIZE] {
73        &self.0
74    }
75
76    pub fn to_bytes(self) -> [u8; PUBLIC_KEY_SIZE] {
77        self.0
78    }
79
80    pub fn to_hex(self) -> String {
81        hex::encode(self.as_bytes())
82    }
83}
84
85impl fmt::Display for PublicKey {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}", self.to_hex())
88    }
89}
90
91#[derive(Debug, Error)]
92pub enum X25519Error {
93    #[error("invalid curve point or scalar")]
94    InvalidCurve,
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::crypto::Rng;
100
101    use super::SecretKey;
102
103    #[test]
104    fn diffie_hellmann() {
105        let rng = Rng::from_seed([1; 32]);
106
107        let alice_secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
108        let alice_public_key = alice_secret_key.public_key().unwrap();
109
110        let bob_secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
111        let bob_public_key = bob_secret_key.public_key().unwrap();
112
113        let alice_shared_secret = alice_secret_key
114            .calculate_agreement(&bob_public_key)
115            .unwrap();
116        let bob_shared_secret = bob_secret_key
117            .calculate_agreement(&alice_public_key)
118            .unwrap();
119
120        assert_eq!(alice_shared_secret, bob_shared_secret);
121    }
122}