atagen

malice

public
Star0
Watch0
aa3dce5114d0530325dccb1cd8fdd52785f2d586
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
crates/malice-push/src/ece.rsRust225 lines7.4 KB
1
//! RFC 8291 message encryption with the aes128gcm content coding of RFC 8188,
2
//! as a single record.
3
4
use aws_lc_rs::{
5
    aead::{AES_128_GCM, Aad, LessSafeKey, Nonce, UnboundKey},
6
    agreement::{self, ECDH_P256, PrivateKey, UnparsedPublicKey},
7
    hkdf::{HKDF_SHA256, KeyType, Salt},
8
};
9
10
const RECORD_SIZE: u32 = 4096;
11
const TAG_LEN: usize = 16;
12
const HEADER_LEN: usize = 16 + 4 + 1 + 65;
13
/// Push services cap the encrypted body at 4096 bytes.
14
pub const MAX_PLAINTEXT: usize = 4096 - HEADER_LEN - TAG_LEN - 1;
15
16
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17
pub enum EceError {
18
    BadKey,
19
    TooLong,
20
    Crypto,
21
}
22
23
impl std::fmt::Display for EceError {
24
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25
        f.write_str(match self {
26
            Self::BadKey => "the subscription's keys are not valid",
27
            Self::TooLong => "the push payload is too long",
28
            Self::Crypto => "push encryption failed",
29
        })
30
    }
31
}
32
33
impl std::error::Error for EceError {}
34
35
struct Len(usize);
36
37
impl KeyType for Len {
38
    fn len(&self) -> usize {
39
        self.0
40
    }
41
}
42
43
fn expand(salt: &[u8], secret: &[u8], info: &[&[u8]], out: &mut [u8]) -> Result<(), EceError> {
44
    Salt::new(HKDF_SHA256, salt)
45
        .extract(secret)
46
        .expand(info, Len(out.len()))
47
        .and_then(|okm| okm.fill(out))
48
        .map_err(|_| EceError::Crypto)
49
}
50
51
struct Keys {
52
    cek: [u8; 16],
53
    nonce: [u8; 12],
54
}
55
56
fn derive(
57
    ecdh_secret: &[u8],
58
    auth: &[u8],
59
    ua_public: &[u8],
60
    as_public: &[u8],
61
    salt: &[u8],
62
) -> Result<Keys, EceError> {
63
    let mut ikm = [0u8; 32];
64
    expand(
65
        auth,
66
        ecdh_secret,
67
        &[b"WebPush: info\0", ua_public, as_public],
68
        &mut ikm,
69
    )?;
70
    let mut keys = Keys {
71
        cek: [0; 16],
72
        nonce: [0; 12],
73
    };
74
    expand(
75
        salt,
76
        &ikm,
77
        &[b"Content-Encoding: aes128gcm\0"],
78
        &mut keys.cek,
79
    )?;
80
    expand(salt, &ikm, &[b"Content-Encoding: nonce\0"], &mut keys.nonce)?;
81
    Ok(keys)
82
}
83
84
fn ecdh(private: &PrivateKey, public: &[u8]) -> Result<Vec<u8>, EceError> {
85
    agreement::agree(
86
        private,
87
        UnparsedPublicKey::new(&ECDH_P256, public),
88
        EceError::BadKey,
89
        |secret| Ok(secret.to_vec()),
90
    )
91
}
92
93
/// Encrypts `plaintext` for the subscription whose `p256dh` key is
94
/// `ua_public` (uncompressed, 65 bytes) and whose auth secret is `auth`.
95
pub fn encrypt(ua_public: &[u8], auth: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, EceError> {
96
    let as_private = PrivateKey::generate(&ECDH_P256).map_err(|_| EceError::Crypto)?;
97
    let mut salt = [0u8; 16];
98
    aws_lc_rs::rand::fill(&mut salt).map_err(|_| EceError::Crypto)?;
99
    encrypt_with(&as_private, salt, ua_public, auth, plaintext)
100
}
101
102
fn encrypt_with(
103
    as_private: &PrivateKey,
104
    salt: [u8; 16],
105
    ua_public: &[u8],
106
    auth: &[u8],
107
    plaintext: &[u8],
108
) -> Result<Vec<u8>, EceError> {
109
    if ua_public.len() != 65 || auth.len() != 16 {
110
        return Err(EceError::BadKey);
111
    }
112
    if plaintext.len() > MAX_PLAINTEXT {
113
        return Err(EceError::TooLong);
114
    }
115
    let as_public = as_private
116
        .compute_public_key()
117
        .map_err(|_| EceError::Crypto)?;
118
    let as_public = as_public.as_ref();
119
    let secret = ecdh(as_private, ua_public)?;
120
    let keys = derive(&secret, auth, ua_public, as_public, &salt)?;
121
122
    let mut out = Vec::with_capacity(HEADER_LEN + plaintext.len() + 1 + TAG_LEN);
123
    out.extend_from_slice(&salt);
124
    out.extend_from_slice(&RECORD_SIZE.to_be_bytes());
125
    out.push(as_public.len() as u8);
126
    out.extend_from_slice(as_public);
127
    let mut record = Vec::with_capacity(plaintext.len() + 1 + TAG_LEN);
128
    record.extend_from_slice(plaintext);
129
    record.push(2);
130
    LessSafeKey::new(UnboundKey::new(&AES_128_GCM, &keys.cek).map_err(|_| EceError::Crypto)?)
131
        .seal_in_place_append_tag(
132
            Nonce::assume_unique_for_key(keys.nonce),
133
            Aad::empty(),
134
            &mut record,
135
        )
136
        .map_err(|_| EceError::Crypto)?;
137
    out.extend_from_slice(&record);
138
    Ok(out)
139
}
140
141
/// The user agent's side, for tests and the mock push service.
142
pub fn decrypt(ua_private: &PrivateKey, auth: &[u8], body: &[u8]) -> Result<Vec<u8>, EceError> {
143
    if body.len() < 21 {
144
        return Err(EceError::Crypto);
145
    }
146
    let salt = &body[..16];
147
    let id_len = body[20] as usize;
148
    let as_public = body.get(21..21 + id_len).ok_or(EceError::Crypto)?;
149
    let mut record = body[21 + id_len..].to_vec();
150
    let ua_public = ua_private
151
        .compute_public_key()
152
        .map_err(|_| EceError::Crypto)?;
153
    let secret = ecdh(ua_private, as_public)?;
154
    let keys = derive(&secret, auth, ua_public.as_ref(), as_public, salt)?;
155
    let plain =
156
        LessSafeKey::new(UnboundKey::new(&AES_128_GCM, &keys.cek).map_err(|_| EceError::Crypto)?)
157
            .open_in_place(
158
                Nonce::assume_unique_for_key(keys.nonce),
159
                Aad::empty(),
160
                &mut record,
161
            )
162
            .map_err(|_| EceError::Crypto)?;
163
    match plain.iter().rposition(|&byte| byte != 0) {
164
        Some(end) if plain[end] == 2 => Ok(plain[..end].to_vec()),
165
        _ => Err(EceError::Crypto),
166
    }
167
}
168
169
#[cfg(test)]
170
mod tests {
171
    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
172
173
    use super::*;
174
175
    fn b64(value: &str) -> Vec<u8> {
176
        URL_SAFE_NO_PAD.decode(value).unwrap()
177
    }
178
179
    // RFC 8291 section 5 and appendix A
180
    const PLAINTEXT: &str = "V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24";
181
    const AS_PRIVATE: &str = "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw";
182
    const UA_PUBLIC: &str =
183
        "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4";
184
    const UA_PRIVATE: &str = "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94";
185
    const SALT: &str = "DGv6ra1nlYgDCS1FRnbzlw";
186
    const AUTH: &str = "BTBZMqHH6r4Tts7J_aSIgg";
187
    const MESSAGE: &str = "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN";
188
189
    #[test]
190
    fn rfc8291_vector_encrypts_exactly() {
191
        let as_private = PrivateKey::from_private_key(&ECDH_P256, &b64(AS_PRIVATE)).unwrap();
192
        let salt: [u8; 16] = b64(SALT).try_into().unwrap();
193
        let body = encrypt_with(
194
            &as_private,
195
            salt,
196
            &b64(UA_PUBLIC),
197
            &b64(AUTH),
198
            &b64(PLAINTEXT),
199
        )
200
        .unwrap();
201
        assert_eq!(URL_SAFE_NO_PAD.encode(body), MESSAGE);
202
    }
203
204
    #[test]
205
    fn rfc8291_vector_decrypts() {
206
        let ua_private = PrivateKey::from_private_key(&ECDH_P256, &b64(UA_PRIVATE)).unwrap();
207
        let plain = decrypt(&ua_private, &b64(AUTH), &b64(MESSAGE)).unwrap();
208
        assert_eq!(plain, b64(PLAINTEXT));
209
    }
210
211
    #[test]
212
    fn random_keys_round_trip_and_bad_input_is_refused() {
213
        let ua_private = PrivateKey::generate(&ECDH_P256).unwrap();
214
        let ua_public = ua_private.compute_public_key().unwrap();
215
        let auth = [9u8; 16];
216
        let body = encrypt(ua_public.as_ref(), &auth, b"hello").unwrap();
217
        assert_eq!(decrypt(&ua_private, &auth, &body).unwrap(), b"hello");
218
        assert!(decrypt(&ua_private, &[1u8; 16], &body).is_err());
219
        assert_eq!(encrypt(&[4u8; 65], &auth, b"x"), Err(EceError::BadKey));
220
        assert_eq!(
221
            encrypt(ua_public.as_ref(), &auth, &vec![0; MAX_PLAINTEXT + 1]),
222
            Err(EceError::TooLong)
223
        );
224
    }
225
}