kestrel_crypto/
lib.rs

1// Copyright The Kestrel Contributors
2// SPDX-License-Identifier: BSD-3-Clause
3
4//! The Kestrel cryptography library.
5//! This library provides implementations of ChaCha20-Poly1305, X25519,
6//! SHA-256, HMAC-SHA-256 and the Noise X protocol.
7//!
8//! The goal of this library is not to serve as a general purpose
9//! cryptographic library, but the functions provided here could certainly
10//! be used as such.
11
12// @@SECURITY: We're doing a best effort job to zero all key data after use.
13// The APIs of some of the underlying crypto libraries result in some key
14// data being left on the stack.
15
16pub mod decrypt;
17pub mod encrypt;
18pub mod errors;
19mod noise;
20mod scrypt;
21
22use orion::hazardous::aead::chacha20poly1305 as chapoly;
23use orion::hazardous::ecc::x25519 as orion_x25519;
24use orion::hazardous::hash::sha2::sha256::Sha256;
25use orion::hazardous::kdf::hkdf::sha256 as hkdf;
26use orion::hazardous::mac::hmac::sha256 as hmac;
27
28use zeroize::{Zeroize, ZeroizeOnDrop};
29
30use errors::{ChaPolyDecryptError, DhError, NoiseError};
31use noise::HandshakeState;
32
33const CHUNK_SIZE: u32 = 65536;
34const SCRYPT_N: u32 = 32768;
35const SCRYPT_R: u32 = 8;
36const SCRYPT_P: u32 = 1;
37const TAG_SIZE: usize = 16;
38
39/// Key file format
40#[derive(Copy, Clone, PartialEq)]
41#[non_exhaustive]
42pub enum AsymFileFormat {
43    V1,
44}
45
46/// Password file format
47#[derive(Copy, Clone, PartialEq)]
48#[non_exhaustive]
49pub enum PassFileFormat {
50    V1,
51}
52
53/// File format versions
54#[derive(Copy, Clone, PartialEq)]
55#[non_exhaustive]
56pub enum FileFormat {
57    AsymV1,
58    PassV1,
59}
60
61/// Payload Key encrypted by the noise protocol
62#[derive(Clone)]
63pub struct PayloadKey {
64    key: [u8; 32],
65}
66
67impl PayloadKey {
68    /// Create a new Key. Keys must be 32 bytes
69    pub fn new(key: &[u8]) -> Self {
70        Self {
71            key: key.try_into().expect("Keys must be 32 bytes"),
72        }
73    }
74
75    /// Get the bytes of the key
76    pub fn as_bytes(&self) -> &[u8] {
77        self.key.as_slice()
78    }
79}
80
81impl Drop for PayloadKey {
82    fn drop(&mut self) {
83        self.zeroize();
84    }
85}
86
87impl Zeroize for PayloadKey {
88    fn zeroize(&mut self) {
89        self.key.zeroize();
90    }
91}
92
93impl ZeroizeOnDrop for PayloadKey {}
94
95/// X25519 Public Key
96#[derive(Clone)]
97pub struct PublicKey {
98    key: Vec<u8>,
99}
100
101/// X25519 Private Key
102#[derive(Clone)]
103pub struct PrivateKey {
104    key: Vec<u8>,
105}
106
107impl PublicKey {
108    // Expose the key 32 byte public key
109    pub fn as_bytes(&self) -> &[u8] {
110        self.key.as_ref()
111    }
112}
113
114/// Convert a raw 32 byte public key into a PublicKey
115impl TryFrom<&[u8]> for PublicKey {
116    type Error = &'static str;
117
118    fn try_from(raw_key: &[u8]) -> Result<PublicKey, Self::Error> {
119        if raw_key.len() != 32 {
120            return Err("Public keys must be 32 bytes");
121        }
122        let pk = raw_key.to_vec();
123        Ok(PublicKey { key: pk })
124    }
125}
126
127impl PrivateKey {
128    /// Generate a new private key from 32 secure random bytes
129    pub fn generate() -> PrivateKey {
130        let key = secure_random(32);
131        PrivateKey { key }
132    }
133
134    /// Expose the raw 32 byte private key
135    pub fn as_bytes(&self) -> &[u8] {
136        self.key.as_ref()
137    }
138
139    /// Derive the public key from the private key
140    pub fn to_public(&self) -> Result<PublicKey, DhError> {
141        let pk = x25519_derive_public(&self.key)?;
142        Ok(PublicKey::try_from(pk.as_slice()).unwrap())
143    }
144
145    /// X25519 Key Exchange between private and a public key,
146    /// returning the raw shared secret
147    pub fn diffie_hellman(&self, public_key: &PublicKey) -> Result<Vec<u8>, DhError> {
148        x25519(self.as_bytes(), public_key.as_bytes())
149    }
150}
151
152/// Convert a raw 32 byte private key into a PrivateKey
153impl TryFrom<&[u8]> for PrivateKey {
154    type Error = &'static str;
155
156    fn try_from(raw_key: &[u8]) -> Result<PrivateKey, Self::Error> {
157        if raw_key.len() != 32 {
158            return Err("Private keys must be 32 bytes");
159        }
160        let sk = raw_key.to_vec();
161        Ok(PrivateKey { key: sk })
162    }
163}
164
165impl Drop for PrivateKey {
166    fn drop(&mut self) {
167        self.zeroize();
168    }
169}
170
171impl Zeroize for PrivateKey {
172    fn zeroize(&mut self) {
173        self.key.as_mut_slice().zeroize();
174    }
175}
176
177impl ZeroizeOnDrop for PrivateKey {}
178
179/// RFC 7748 compliant X25519.
180/// k is the private key and u is the public key.
181/// Keys must be 32 bytes.
182/// Returns a [DhError] if the shared secret is the all zero output
183pub fn x25519(k: &[u8], u: &[u8]) -> Result<Vec<u8>, DhError> {
184    let mut sk: [u8; 32] = k.try_into().expect("Private key must be 32 bytes");
185    let pk: [u8; 32] = u.try_into().expect("Public key must be 32 bytes");
186
187    let private_key = orion_x25519::PrivateKey::from_slice(&sk).unwrap();
188    let public_key: orion_x25519::PublicKey = orion_x25519::PublicKey::from_slice(&pk).unwrap();
189
190    let shared_secret =
191        orion_x25519::key_agreement(&private_key, &public_key).map_err(|_| DhError)?;
192    let res = shared_secret.unprotected_as_bytes().to_vec();
193    sk.zeroize();
194
195    Ok(res)
196}
197
198/// Derive an X25519 public key from a private key.
199/// The private key must be 32 bytes.
200///
201/// This function is not expected to ever return an error for the code path
202/// deriving public keys. A [DhError] is returned here only because the x25519
203/// implementation returns an error.
204pub fn x25519_derive_public(private_key: &[u8]) -> Result<Vec<u8>, DhError> {
205    let sk = orion_x25519::PrivateKey::from_slice(private_key).unwrap();
206    let pk = orion_x25519::PublicKey::try_from(&sk).map_err(|_| DhError)?;
207
208    Ok(pk.to_bytes().as_ref().to_vec())
209}
210
211/// A struct containing the result of a [`noise_encrypt`]
212pub struct NoiseEncryptMsg {
213    pub ciphertext: Vec<u8>,
214    pub handshake_hash: [u8; 32],
215}
216
217/// Encrypt the payload key using the noise X protocol.
218///
219/// Passing None for ephemeral, and ephemeral_public will generate
220/// fresh keys. This is almost certainly what you want.
221///
222/// Sender and ephemeral private and public keys must match.
223/// prologue is included in the handshake hash, ensuring the handshake fails
224/// if the data isn't identical.
225/// payload must be <= 65439 bytes. The resulting message is a maximum of
226/// 65535 bytes.
227///
228/// Returns the handshake message ciphertext.
229pub fn noise_encrypt(
230    sender: &PrivateKey,
231    sender_public: &PublicKey,
232    recipient: &PublicKey,
233    ephemeral: Option<&PrivateKey>,
234    ephemeral_public: Option<&PublicKey>,
235    prologue: &[u8],
236    payload_key: &PayloadKey,
237) -> Result<NoiseEncryptMsg, NoiseError> {
238    let mut handshake_state = HandshakeState::init_x(
239        true,
240        prologue,
241        sender.clone(),
242        sender_public.clone(),
243        ephemeral.cloned(),
244        ephemeral_public.cloned(),
245        Some(recipient.clone()),
246    );
247
248    let noise_handshake = handshake_state.write_message(payload_key.as_bytes())?;
249    let handshake_hash = noise_handshake.handshake_hash;
250    let ciphertext = noise_handshake.message;
251
252    Ok(NoiseEncryptMsg {
253        ciphertext,
254        handshake_hash,
255    })
256}
257
258/// A struct containing the result of a [`noise_decrypt`]
259/// PublicKey is the sender's public key
260pub struct NoiseDecryptMsg {
261    pub payload_key: PayloadKey,
262    pub public_key: PublicKey,
263    pub handshake_hash: [u8; 32],
264}
265
266/// Decrypt the payload key using the noise protocol.
267/// The given recipient public key must match the recipient private key.
268/// Returns the payload key, and the sender's [PublicKey]
269pub fn noise_decrypt(
270    recipient: &PrivateKey,
271    recipient_public: &PublicKey,
272    prologue: &[u8],
273    handshake_message: &[u8],
274) -> Result<NoiseDecryptMsg, NoiseError> {
275    let initiator = false;
276    let mut handshake_state = noise::HandshakeState::init_x(
277        initiator,
278        prologue,
279        recipient.clone(),
280        recipient_public.clone(),
281        None,
282        None,
283        None,
284    );
285
286    // Decrypt the payload key
287    let noise_handshake = handshake_state.read_message(handshake_message)?;
288    let handshake_hash = noise_handshake.handshake_hash;
289    if noise_handshake.message.len() != 32 {
290        return Err(NoiseError::Other(
291            "Expected payload key to be 32 bytes.".to_string(),
292        ));
293    }
294
295    let payload_key = PayloadKey::new(noise_handshake.message.as_slice());
296    let sender_pubkey = handshake_state
297        .get_pubkey()
298        .expect("Expected to get the sender's public key");
299
300    Ok(NoiseDecryptMsg {
301        payload_key,
302        public_key: sender_pubkey,
303        handshake_hash,
304    })
305}
306
307/// ChaCha20-Poly1305 encrypt function as specified by the noise protocol.
308/// The nonce is stored as a little endian integer in the lowest eight
309/// bytes of the nonce. The top four bytes of the nonce are zeros.
310/// Returns the ciphertxt and 16 byte Poly1305 tag appended.
311#[allow(clippy::let_and_return)]
312pub(crate) fn chapoly_encrypt_noise(
313    key: &[u8],
314    nonce: u64,
315    ad: &[u8],
316    plaintext: &[u8],
317) -> Vec<u8> {
318    // For ChaCha20-Poly1305 the noise spec says that the nonce should use
319    // little endian.
320    let nonce_bytes = nonce.to_le_bytes();
321    let mut final_nonce_bytes = [0u8; 12];
322    final_nonce_bytes[4..].copy_from_slice(&nonce_bytes);
323
324    chapoly_encrypt_ietf(key, &final_nonce_bytes, plaintext, ad)
325}
326
327/// RFC 8439 ChaCha20-Poly1305 encrypt function.
328/// The key must be 32 bytes and the nonce must be 12 bytes.
329/// The aad should be an empty slice if not used.
330/// Returns the ciphertext.
331#[allow(clippy::let_and_return, clippy::redundant_field_names)]
332pub fn chapoly_encrypt_ietf(key: &[u8], nonce: &[u8], plaintext: &[u8], aad: &[u8]) -> Vec<u8> {
333    let nonce = chapoly::Nonce::from_slice(nonce).expect("Nonce must be 12 bytes");
334    let mut ct_and_tag = vec![0u8; plaintext.len() + TAG_SIZE];
335    let key = chapoly::SecretKey::from_slice(key).expect("Key must be 32 bytes");
336    chapoly::seal(
337        &key,
338        &nonce,
339        plaintext,
340        Some(aad),
341        ct_and_tag.as_mut_slice(),
342    )
343    .expect("ChaCha20-Poly11305 encryption failed");
344
345    ct_and_tag
346}
347
348/// ChaCha20-Poly1305 decrypt function as specified by the noise protocol.
349/// The nonce is stored as a little endian integer in the lowest eight
350/// bytes of the nonce. The top four bytes of the nonce are zeros.
351/// The poly1305 tag must be included as the last 16 bytes of the ciphertext.
352/// Returns the plaintext.
353pub(crate) fn chapoly_decrypt_noise(
354    key: &[u8],
355    nonce: u64,
356    ad: &[u8],
357    ciphertext: &[u8],
358) -> Result<Vec<u8>, ChaPolyDecryptError> {
359    assert_eq!(key.len(), 32);
360
361    // For ChaCha20-Poly1305 the noise spec says that the nonce should use
362    // little endian.
363    let nonce_bytes = nonce.to_le_bytes();
364    let mut final_nonce_bytes = [0u8; 12];
365    final_nonce_bytes[4..].copy_from_slice(&nonce_bytes);
366
367    chapoly_decrypt_ietf(key, &final_nonce_bytes, ciphertext, ad)
368}
369
370/// RFC 8439 ChaCha20-Poly1305 decrypt function.
371/// The key must be 32 bytes and the nonce must be 12 bytes.
372/// The 16 byte poly1305 tag must be appended to the ciphertext.
373/// Returns the plaintext.
374#[allow(clippy::redundant_field_names)]
375pub fn chapoly_decrypt_ietf(
376    key: &[u8],
377    nonce: &[u8],
378    ciphertext: &[u8],
379    aad: &[u8],
380) -> Result<Vec<u8>, ChaPolyDecryptError> {
381    let nonce = chapoly::Nonce::from_slice(nonce).expect("Nonce must be 12 bytes");
382    let key = chapoly::SecretKey::from_slice(key).expect("Key must be 32 bytes");
383    let pt_size = std::cmp::max(ciphertext.len() - TAG_SIZE, 0);
384    let mut plaintext = vec![0u8; pt_size];
385
386    chapoly::open(
387        &key,
388        &nonce,
389        ciphertext,
390        Some(aad),
391        plaintext.as_mut_slice(),
392    )
393    .map_err(|_| ChaPolyDecryptError)?;
394
395    Ok(plaintext)
396}
397
398/// SHA-256
399pub fn sha256(data: &[u8]) -> Vec<u8> {
400    Sha256::digest(data).unwrap().as_ref().to_vec()
401}
402
403/// HMAC-SHA-256
404pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
405    let sk = hmac::SecretKey::from_slice(key).unwrap();
406    hmac::HmacSha256::hmac(&sk, data)
407        .unwrap()
408        .unprotected_as_bytes()
409        .to_vec()
410}
411
412fn hkdf_noise(chaining_key: &[u8], ikm: &[u8]) -> (Vec<u8>, Vec<u8>) {
413    let counter1: [u8; 1] = [0x01];
414    let mut counter2: [u8; 33] = [0u8; 33];
415    let temp_key = hmac_sha256(chaining_key, ikm);
416    let output1 = hmac_sha256(&temp_key, &counter1);
417    counter2[..32].copy_from_slice(&output1);
418    counter2[32..].copy_from_slice(&[0x02]);
419    let output2 = hmac_sha256(&temp_key, &counter2);
420    counter2.zeroize();
421    (output1, output2)
422}
423
424/// HKDF-SHA256
425/// If no info or salt is required, use the empty slice.
426pub fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], len: usize) -> Vec<u8> {
427    let mut okm = vec![0u8; len];
428
429    hkdf::derive_key(salt, ikm, Some(info), okm.as_mut_slice()).unwrap();
430
431    okm
432}
433
434/// Derives a secret key from a password and a salt using scrypt.
435/// Recommended parameters are n = 32768, r = 8, p = 1
436/// Parameter n must be larger than 1 and a power of 2.
437pub fn scrypt(password: &[u8], salt: &[u8], n: u32, r: u32, p: u32, dk_len: usize) -> Vec<u8> {
438    scrypt::scrypt(password, salt, n as usize, r as usize, p as usize, dk_len)
439}
440
441/// Generates the specified amount of bytes from a CSPRNG
442pub fn secure_random(len: usize) -> Vec<u8> {
443    let mut data = vec![0u8; len];
444    getrandom::fill(&mut data).expect("CSPRNG gen failed");
445    data
446}
447
448#[cfg(test)]
449mod tests {
450    use super::{
451        chapoly_decrypt_ietf, chapoly_decrypt_noise, chapoly_encrypt_ietf, chapoly_encrypt_noise,
452        hkdf_sha256, hmac_sha256, scrypt, sha256, x25519,
453    };
454    use super::{PrivateKey, PublicKey};
455
456    #[test]
457    fn test_chapoly_encrypt() {
458        let expected =
459            hex::decode("cc459a8b9d29617bb70791e7b158dfaf36585f656aec0ada3899fdcd").unwrap();
460        let pt = b"Hello world!";
461        let key: [u8; 32] = [
462            0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2,
463            0x66, 0x45, 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5,
464            0x1d, 0xb9, 0x2c, 0x2a,
465        ];
466        let nonce: u64 = 0;
467        let ad = [0x00, 0x00, 0x00, 0x0C];
468
469        let ct_and_tag = chapoly_encrypt_noise(&key, nonce, &ad, pt);
470
471        assert_eq!(&expected[..], &ct_and_tag[..]);
472    }
473
474    #[test]
475    fn test_chapoly_enc_empty_pt() {
476        let expected_ct = hex::decode("c7a7077a5e9d774b510100904c7dc805").unwrap();
477        let key = hex::decode("68301045a4494999d59ffa818ee5fafc2878bf96c32acf5fa40dbe93e8ac98ce")
478            .unwrap();
479        let nonce = [0u8; 12];
480        let aad: [u8; 1] = [0x01];
481
482        let ct = chapoly_encrypt_ietf(key.as_slice(), &nonce, &[], &aad);
483
484        assert_eq!(expected_ct.as_slice(), ct.as_slice());
485    }
486
487    #[test]
488    fn test_decrypt() {
489        let key = hex::decode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
490            .unwrap();
491        let nonce: u64 = 0;
492        let ad = hex::decode("0000000C").unwrap();
493        let expected = b"Hello world!";
494        let ct_and_tag =
495            hex::decode("cc459a8b9d29617bb70791e7b158dfaf36585f656aec0ada3899fdcd").unwrap();
496
497        let pt = chapoly_decrypt_noise(&key, nonce, &ad, &ct_and_tag).unwrap();
498
499        assert_eq!(expected, pt.as_slice());
500    }
501
502    #[test]
503    fn test_chapoly_dec_empty_pt() {
504        let ct = hex::decode("c7a7077a5e9d774b510100904c7dc805").unwrap();
505        let key = hex::decode("68301045a4494999d59ffa818ee5fafc2878bf96c32acf5fa40dbe93e8ac98ce")
506            .unwrap();
507        let nonce = [0u8; 12];
508        let aad: [u8; 1] = [0x01];
509
510        let pt = chapoly_decrypt_ietf(key.as_slice(), &nonce, ct.as_slice(), &aad).unwrap();
511
512        let expected_pt: [u8; 0] = [];
513        assert_eq!(&expected_pt, pt.as_slice());
514    }
515
516    #[test]
517    fn test_sha256() {
518        let data = b"hello";
519        let got = sha256(data);
520        let expected =
521            hex::decode("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
522                .unwrap();
523        assert_eq!(&got, expected.as_slice());
524    }
525
526    #[test]
527    fn test_hmac_sha256() {
528        let key = b"yellowsubmarine.yellowsubmarine.";
529        let message = b"Hello, world!";
530        let expected =
531            hex::decode("3cb82dc71c26dfe8be75805f6438027d5170f3fdcd8057f0a55d1c7c1743224c")
532                .unwrap();
533        let result = hmac_sha256(key, message);
534
535        assert_eq!(&expected, &result);
536    }
537
538    #[test]
539    fn test_hkdf_sha256() {
540        // RFC-5869 Test Case 1
541        let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
542        let salt = hex::decode("000102030405060708090a0b0c").unwrap();
543        let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap();
544        let length = 42;
545
546        let expected_okm = hex::decode(
547            "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
548        )
549        .unwrap();
550
551        let result_okm = hkdf_sha256(&salt, &ikm, &info, length);
552
553        assert_eq!(&expected_okm, &result_okm);
554
555        // RFC-5869 Test Case 3
556        let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
557        let salt = &[];
558        let info = &[];
559        let length = 42;
560
561        let expected_okm = hex::decode(
562            "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8",
563        )
564        .unwrap();
565
566        let result_okm = hkdf_sha256(salt, &ikm, info, length);
567        assert_eq!(&expected_okm, &result_okm);
568    }
569
570    #[test]
571    fn test_scrypt() {
572        let password = b"hackme";
573        let salt = b"yellowsubmarine.";
574
575        let expected1 =
576            hex::decode("3ebb9ac0d1da595f755407fe8fc246fe67fe6075730fc6e853351c2834bd6157")
577                .unwrap();
578        let result1 = scrypt(password, salt, 32768, 8, 1, 32);
579        assert_eq!(&expected1, &result1);
580
581        let expected2 = hex::decode("3ebb9ac0d1da595f").unwrap();
582        let result2 = scrypt(password, salt, 32768, 8, 1, 8);
583        assert_eq!(&expected2, &result2);
584
585        let expected3 = hex::decode("87b33dba57a7633a3df7741eabee3de0").unwrap();
586        let result3 = scrypt(password, salt, 1024, 8, 1, 16);
587        assert_eq!(&expected3, &result3);
588    }
589
590    #[test]
591    fn test_rfc7748_diffie_hellman_vectors() {
592        let alice_private_expected =
593            hex::decode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
594                .unwrap();
595        let alice_public_expected =
596            hex::decode("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
597                .unwrap();
598        let bob_private_expected =
599            hex::decode("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb")
600                .unwrap();
601        let bob_public_expected =
602            hex::decode("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f")
603                .unwrap();
604        let expected_shared_secret =
605            hex::decode("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742")
606                .unwrap();
607
608        let alice_private = PrivateKey::try_from(alice_private_expected.as_slice()).unwrap();
609        let alice_public = PublicKey::try_from(alice_public_expected.as_slice()).unwrap();
610        assert_eq!(
611            &alice_public_expected,
612            &alice_private.to_public().unwrap().as_bytes()
613        );
614
615        let bob_private = PrivateKey::try_from(bob_private_expected.as_slice()).unwrap();
616        let bob_public = PublicKey::try_from(bob_public_expected.as_slice()).unwrap();
617
618        let alice_to_bob = x25519(alice_private.as_bytes(), bob_public.as_bytes()).unwrap();
619        let bob_to_alice = x25519(bob_private.as_bytes(), alice_public.as_bytes()).unwrap();
620        let alice_to_bob2 = alice_private.diffie_hellman(&bob_public).unwrap();
621
622        assert_eq!(&alice_to_bob, &bob_to_alice);
623        assert_eq!(&alice_to_bob, &alice_to_bob2);
624        assert_eq!(&alice_to_bob, expected_shared_secret.as_slice());
625    }
626
627    #[test]
628    fn test_private_to_public() {
629        let alice_private_expected =
630            hex::decode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
631                .unwrap();
632        let alice_public_expected =
633            hex::decode("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
634                .unwrap();
635        let got_public = PrivateKey::try_from(&alice_private_expected[..])
636            .unwrap()
637            .to_public()
638            .unwrap();
639
640        assert_eq!(&alice_public_expected[..], got_public.as_bytes());
641    }
642}