1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Simple Assymmetric locking mechanism using:
//!
//! * prime order group for DH
//! * HKDF for KDF
//! * chacha20poly1305 for symmetric encryption algorithm
//!
#![allow(clippy::op_ref)] // This needs to be here because the points of sec2 backend do not implement Copy
use crate::ec::ristretto255::{GroupElement, Scalar};
use cryptoxide::chacha20poly1305::ChaCha20Poly1305;
use cryptoxide::hkdf::hkdf_expand;
use cryptoxide::sha2;
use rand_core::{CryptoRng, RngCore};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecryptionError {
    DataTooSmall,
    PointInvalid,
    TagMismatch,
}

fn shared_key_to_symmetric_key(app_level_info: &[u8], p: &GroupElement) -> ChaCha20Poly1305 {
    let prk = &p.to_bytes();
    let mut symkey = [0u8; 32 + 12];
    hkdf_expand(sha2::Sha256::new(), prk, app_level_info, &mut symkey);
    ChaCha20Poly1305::new(&symkey[0..32], &symkey[32..], &[])
}

const SCHEME_OVERHEAD: usize = GroupElement::BYTES_LEN + 16; // 16 bytes of tag

/// Encrypt data in an assymetric lock
///
/// # Return
///
/// the data encrypted with a ephemeral public key in prefix and
/// the poly1305 tag in suffix.
pub fn encrypt<R: RngCore + CryptoRng>(
    rng: &mut R,
    app_info: &[u8],
    receiver_pk: &GroupElement,
    data: &[u8],
) -> Vec<u8> {
    // create a new ephemeral key and throw away the secret key keeping only the public key
    // and the shared key
    let r = Scalar::random(rng);
    let pk = GroupElement::generator() * &r;
    let shared = r * receiver_pk;

    // Create a ChaCha20Poly1305 encryption context
    let mut context = shared_key_to_symmetric_key(app_info, &shared);

    // encrypt the data with the context
    let mut out = vec![0u8; data.len() + SCHEME_OVERHEAD];
    out[0..GroupElement::BYTES_LEN].copy_from_slice(&pk.to_bytes());
    let (pk_and_encrypted, tag) = out.split_at_mut(GroupElement::BYTES_LEN + data.len());
    context.encrypt(data, &mut pk_and_encrypted[GroupElement::BYTES_LEN..], tag);
    out
}

/// Decrypt data in the asymmetric lock. this is the dual of 'encrypt'.
/// The data should in the form:
///
/// ```text
///     EPHEMERAL_PUBLIC_KEY || ENCRYPTED-DATA || POLY1305-TAG
/// ```
///
/// # Return
///
/// Error if:
/// * data is too small
/// * point is not in the first format
/// * tag don't match
/// Success otherwise
///
/// # Panics
///
/// If output 'out' is not 48 bytes less than 'data'
///
pub fn decrypt(
    app_info: &[u8],
    sk: &Scalar,
    data: &[u8],
    out: &mut [u8],
) -> Result<(), DecryptionError> {
    if data.len() < SCHEME_OVERHEAD {
        return Err(DecryptionError::DataTooSmall);
    }
    assert_eq!(data.len() - SCHEME_OVERHEAD, out.len());

    let pk_data = &data[0..GroupElement::BYTES_LEN];
    let payload = &data[GroupElement::BYTES_LEN..data.len() - 16];
    let tag = &data[data.len() - 16..];

    let pk = GroupElement::from_bytes(pk_data);
    let shared = sk * pk.ok_or(DecryptionError::PointInvalid)?;

    let mut context = shared_key_to_symmetric_key(app_info, &shared);
    if !context.decrypt(payload, out, tag) {
        return Err(DecryptionError::TagMismatch);
    }
    Ok(())
}

#[cfg(test)]
mod test {
    use super::*;
    use rand_core::OsRng;

    #[test]
    pub fn it_works() {
        let mut r = OsRng;

        // create a random keypair
        let sk_receiver = Scalar::random(&mut r);
        let pk_receiver = GroupElement::generator() * &sk_receiver;

        let app_info = b"hello";
        let msg = b"message";
        let mut out = vec![0; msg.len()];
        let encrypted = encrypt(&mut r, app_info, &pk_receiver, msg);
        decrypt(app_info, &sk_receiver, &encrypted, &mut out).unwrap();
        assert_eq!(out, msg);
    }
}