summaryrefslogtreecommitdiff
path: root/src/crypto.rs
blob: cf1044eda5568ab205096ce22df432fe0b7a3d98 (plain)
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
#![deny(clippy::pedantic)]
#![deny(clippy::restriction)]
#![allow(clippy::blanket_clippy_restriction_lints)]
#![allow(clippy::implicit_return)]
#![allow(clippy::missing_docs_in_private_items)]
#![allow(clippy::shadow_reuse)]

use std::fmt::Debug;

use hmac::{Hmac, Mac};
use password_hash::{Output, Salt};
use rand::RngCore;
use scrypt::scrypt;
use serde::{Deserialize, Serialize};
use sha2::Sha256;

const NAMESPACE: &[u8] = b"identity.mozilla.com/picl/v1/";

#[derive(Clone, PartialEq, Eq, Zeroize, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct SecretBytes<const N: usize>(pub [u8; N]);

impl<const N: usize> Drop for SecretBytes<N> {
    fn drop(&mut self) {
        self.zeroize();
    }
}

#[derive(Clone, PartialEq, Eq)]
pub struct TokenID(pub [u8; 32]);

impl<const N: usize> Debug for SecretBytes<N> {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        fmt.write_fmt(format_args!("SecretBytes {{ raw: {} }}", hex::encode(&self.0)))
    }
}

impl<const N: usize> SecretBytes<N> {
    fn xor(&self, other: &Self) -> Self {
        let mut result = self.clone();
        for (a, b) in result.0.iter_mut().zip(other.0.iter()) {
            *a ^= b;
        }
        result
    }
}

impl<const N: usize> From<SecretBytes<N>> for String {
    fn from(sb: SecretBytes<N>) -> Self {
        hex::encode(&sb.0)
    }
}

impl<const N: usize> TryFrom<String> for SecretBytes<N> {
    type Error = hex::FromHexError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        let mut result = Self([0; N]);
        hex::decode_to_slice(value, &mut result.0)?;
        Ok(result)
    }
}

impl From<SecretBytes<32>> for Output {
    fn from(s: SecretBytes<32>) -> Output {
        #[allow(clippy::unwrap_used)]
        Output::new(&s.0).unwrap()
    }
}

mod from_hkdf {
    use hkdf::Hkdf;
    use sha2::Sha256;

    // sealing lets us guarantee that SIZE is always correct,
    // which means that from_hkdf always receives correctly sized slices
    // and copies never fail
    mod private {
        pub trait Seal {}
        impl<const N: usize> Seal for super::super::SecretBytes<N> {}
        impl Seal for super::super::TokenID {}
        impl<L: Seal, R: Seal> Seal for (L, R) {}
    }

    pub trait FromHkdf: private::Seal {
        const SIZE: usize;
        fn from_hkdf(bytes: &[u8]) -> Self;
    }

    impl<const N: usize> FromHkdf for super::SecretBytes<N> {
        const SIZE: usize = N;
        fn from_hkdf(bytes: &[u8]) -> Self {
            #[allow(clippy::unwrap_used)]
            Self(bytes.try_into().unwrap())
        }
    }

    impl FromHkdf for super::TokenID {
        const SIZE: usize = 32;
        fn from_hkdf(bytes: &[u8]) -> Self {
            #[allow(clippy::expect_used)]
            Self(bytes.try_into().expect("hkdf failed"))
        }
    }

    impl<L: FromHkdf, R: FromHkdf> FromHkdf for (L, R) {
        const SIZE: usize = L::SIZE + R::SIZE;
        #[allow(clippy::indexing_slicing)]
        fn from_hkdf(bytes: &[u8]) -> Self {
            (L::from_hkdf(&bytes[0..L::SIZE]), R::from_hkdf(&bytes[L::SIZE..]))
        }
    }

    pub fn from_hkdf<O: FromHkdf>(key: &[u8], info: &[&[u8]]) -> O {
        let hk = Hkdf::<Sha256>::new(None, key);
        let mut buf = vec![0; O::SIZE];
        #[allow(clippy::expect_used)]
        // worth keeping an eye out for very large results (>255*32 bytes)
        hk.expand_multi_info(info, buf.as_mut_slice()).expect("hkdf failed");
        O::from_hkdf(&buf)
    }
}

use from_hkdf::from_hkdf;
use zeroize::Zeroize;

impl<const N: usize> SecretBytes<N> {
    pub fn generate() -> Self {
        let mut result = Self([0; N]);
        rand::rngs::OsRng.fill_bytes(&mut result.0);
        result
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(transparent)]
pub struct AuthPW {
    pub pw: SecretBytes<32>,
}

pub struct StretchedPW {
    pub pw: Output,
}

impl AuthPW {
    pub fn stretch(&self, salt: Salt) -> anyhow::Result<StretchedPW> {
        let mut result = [0; 32];
        let mut buf = [0; Salt::MAX_LENGTH];
        let params = scrypt::Params::new(16, 8, 1)?;
        let salt = salt.b64_decode(&mut buf)?;
        scrypt(&self.pw.0, salt, &params, &mut result)?;
        Ok(StretchedPW { pw: Output::new(&result)? })
    }
}

impl StretchedPW {
    pub fn verify_hash(&self) -> Output {
        let raw: SecretBytes<32> = from_hkdf(self.pw.as_bytes(), &[NAMESPACE, b"verifyHash"]);
        raw.into()
    }

    fn wrap_wrap_key(&self) -> SecretBytes<32> {
        from_hkdf(self.pw.as_bytes(), &[NAMESPACE, b"wrapwrapKey"])
    }

    pub fn decrypt_wwkb(&self, wwkb: &SecretBytes<32>) -> SecretBytes<32> {
        wwkb.xor(&self.wrap_wrap_key())
    }

    pub fn rewrap_wkb(&self, wkb: &SecretBytes<32>) -> SecretBytes<32> {
        wkb.xor(&self.wrap_wrap_key())
    }
}

pub struct SessionCredentials {
    pub token_id: TokenID,
    pub req_hmac_key: SecretBytes<32>,
}

impl SessionCredentials {
    pub fn derive(seed: &SecretBytes<32>) -> Self {
        let (token_id, req_hmac_key) = from_hkdf(&seed.0, &[NAMESPACE, b"sessionToken"]);
        Self { token_id, req_hmac_key }
    }
}

pub struct KeyFetchReq {
    pub token_id: TokenID,
    pub req_hmac_key: SecretBytes<32>,
    pub key_request_key: SecretBytes<32>,
}

impl KeyFetchReq {
    pub fn from_token(key_fetch_token: &SecretBytes<32>) -> Self {
        let (token_id, (req_hmac_key, key_request_key)) =
            from_hkdf(&key_fetch_token.0, &[NAMESPACE, b"keyFetchToken"]);
        Self { token_id, req_hmac_key, key_request_key }
    }

    pub fn derive_resp(&self) -> KeyFetchResp {
        let (resp_hmac_key, resp_xor_key) =
            from_hkdf(&self.key_request_key.0, &[NAMESPACE, b"account/keys"]);
        KeyFetchResp { resp_hmac_key, resp_xor_key }
    }
}

pub struct KeyFetchResp {
    pub resp_hmac_key: SecretBytes<32>,
    pub resp_xor_key: SecretBytes<64>,
}

impl KeyFetchResp {
    pub fn wrap_keys(&self, keys: &KeyBundle) -> WrappedKeyBundle {
        let ciphertext = self.resp_xor_key.xor(&keys.to_bytes());
        #[allow(clippy::unwrap_used)]
        let mut hmac = Hmac::<Sha256>::new_from_slice(&self.resp_hmac_key.0).unwrap();
        hmac.update(&ciphertext.0);
        let hmac = *hmac.finalize().into_bytes().as_ref();
        WrappedKeyBundle { ciphertext, hmac }
    }
}

pub struct KeyBundle {
    pub ka: SecretBytes<32>,
    pub wrap_kb: SecretBytes<32>,
}

impl KeyBundle {
    pub fn to_bytes(&self) -> SecretBytes<64> {
        let mut result = SecretBytes([0; 64]);
        result.0[0..32].copy_from_slice(&self.ka.0);
        result.0[32..].copy_from_slice(&self.wrap_kb.0);
        result
    }
}

#[derive(Debug)]
pub struct WrappedKeyBundle {
    pub ciphertext: SecretBytes<64>,
    pub hmac: [u8; 32],
}

impl WrappedKeyBundle {
    pub fn to_bytes(&self) -> [u8; 96] {
        let mut result = [0; 96];
        result[0..64].copy_from_slice(&self.ciphertext.0);
        result[64..].copy_from_slice(&self.hmac);
        result
    }
}

pub struct PasswordChangeReq {
    pub token_id: TokenID,
    pub req_hmac_key: SecretBytes<32>,
}

impl PasswordChangeReq {
    pub fn from_change_token(token: &SecretBytes<32>) -> Self {
        let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"passwordChangeToken"]);
        Self { token_id, req_hmac_key }
    }

    pub fn from_forgot_token(token: &SecretBytes<32>) -> Self {
        let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"passwordForgotToken"]);
        Self { token_id, req_hmac_key }
    }
}

pub struct AccountResetReq {
    pub token_id: TokenID,
    pub req_hmac_key: SecretBytes<32>,
}

impl AccountResetReq {
    pub fn from_token(token: &SecretBytes<32>) -> Self {
        let (token_id, req_hmac_key) = from_hkdf(&token.0, &[NAMESPACE, b"accountResetToken"]);
        Self { token_id, req_hmac_key }
    }
}

#[cfg(test)]
mod test {
    use hex_literal::hex;
    use password_hash::{Output, SaltString};

    use crate::crypto::{KeyBundle, KeyFetchReq, SessionCredentials};

    use super::{AuthPW, SecretBytes};

    macro_rules! shex {
        ( $s: literal ) => {
            SecretBytes(hex!($s))
        };
    }

    #[test]
    fn test_derive_session() {
        let creds = SessionCredentials::derive(&SecretBytes(hex!(
            "a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"
        )));
        assert_eq!(
            creds.token_id.0,
            hex!("c0a29dcf46174973da1378696e4c82ae10f723cf4f4d9f75e39f4ae3851595ab")
        );
        assert_eq!(
            creds.req_hmac_key.0,
            hex!("9d8f22998ee7f579 8b887042466b72d5 3e56ab0c094388bf 65831f702d2febc0")
        );
    }

    #[test]
    fn test_key_fetch() {
        let key_fetch = KeyFetchReq::from_token(&shex!(
            "8081828384858687 88898a8b8c8d8e8f 9091929394959697 98999a9b9c9d9e9f"
        ));
        assert_eq!(
            key_fetch.token_id.0,
            hex!("3d0a7c02a15a62a2882f76e39b6494b500c022a8816e048625a495718998ba60")
        );
        assert_eq!(
            key_fetch.req_hmac_key.0,
            hex!("87b8937f61d38d0e 29cd2d5600b3f4da 0aa48ac41de36a0e fe84bb4a9872ceb7")
        );
        assert_eq!(
            key_fetch.key_request_key.0,
            hex!("14f338a9e8c6324d 9e102d4e6ee83b20 9796d5c74bb734a4 10e729e014a4a546")
        );

        let resp = key_fetch.derive_resp();
        assert_eq!(
            resp.resp_hmac_key.0,
            hex!("f824d2953aab9faf 51a1cb65ba9e7f9e 5bf91c8d8fd1ac1c 8c2d31853a8a1210")
        );
        assert_eq!(
            resp.resp_xor_key.0,
            hex!(
                "ce7d7aa77859b235 9932970bbe2101f2 e80d01faf9191bd5 ee52181d2f0b7809
                 8281ba8cff392543 3a89f7c3095e0c89 900a469d60790c83 3281c4df1a11c763"
            )
        );

        let bundle = KeyBundle {
            ka: shex!("2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f"),
            wrap_kb: shex!("7effe354abecbcb2 34a8dfc2d7644b4a d339b525589738f2 d27341bb8622ecd8"),
        };
        assert_eq!(
            bundle.to_bytes().0,
            hex!(
                "2021222324252627 28292a2b2c2d2e2f 3031323334353637 38393a3b3c3d3e3f
                 7effe354abecbcb2 34a8dfc2d7644b4a d339b525589738f2 d27341bb8622ecd8"
            )
        );

        let wrapped = resp.wrap_keys(&bundle);
        assert_eq!(
            wrapped.ciphertext.0,
            hex!(
                "ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636
                 fc7e59d854d599f1 0e212801de3a47c3 4333f3b838ee3471 e0f285649c332bbb"
            )
        );
        assert_eq!(
            wrapped.hmac,
            hex!("4c17f42a0b319bbb a327d2b326ad23e9 37219b4de32e3ec7 b3e3f740522ad6ef")
        );
        assert_eq!(
            wrapped.to_bytes(),
            hex!(
                "ee5c58845c7c9412 b11bbd20920c2fdd d83c33c9cd2c2de2 d66b222613364636
                 fc7e59d854d599f1 0e212801de3a47c3 4333f3b838ee3471 e0f285649c332bbb
                 4c17f42a0b319bbb a327d2b326ad23e9 37219b4de32e3ec7 b3e3f740522ad6ef"
            )
        );
    }

    #[test]
    fn test_stretch() -> anyhow::Result<()> {
        let auth_pw = AuthPW {
            pw: SecretBytes(hex!(
                "247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375"
            )),
        };

        let stretched = auth_pw.stretch(
            SaltString::b64_encode(&hex!(
                "00f0000000000000 0000000000000000 0000000000000000 0000000000000000"
            ))?
            .as_salt(),
        )?;
        assert_eq!(
            stretched.pw,
            Output::new(&hex!(
                "441509e25c92ee10 3d5a1a874e6f155d f25a44d06e61c894 616c9e85181dba97"
            ))?
        );

        assert_eq!(
            stretched.verify_hash().as_bytes(),
            hex!("a4765bf103dc057f 4cf4bc2c131ddb67 16e8a4333cc55e1d 3c449f31f0eec4f1")
        );

        assert_eq!(
            stretched.wrap_wrap_key().0,
            hex!("3ebea117efa9faf5 7ce195899b290505 8368e7760cc26ea5 8a2a1be0da7fb287")
        );
        Ok(())
    }
}