Skip to main content

wechat_protocol/
business.rs

1//! Business encryption is separate from authenticated MMTLS records.
2use crate::{crypto::*, protobuf as p, MAX_MESSAGE_BYTES};
3use anyhow::{bail, ensure, Result};
4use openssl::{nid::Nid, symm::Mode};
5use zeroize::Zeroize;
6
7pub struct PublicProfile {
8    pub version: u16,
9    pub point: Vec<u8>,
10    pub signature_pem: String,
11}
12pub fn profile_for_region(region: u32) -> Result<PublicProfile> {
13    let version = match region {
14        1 => 10014,
15        2 => 50005,
16        _ => bail!("Unsupported business certificate region"),
17    };
18    let pins: serde_json::Value =
19        serde_json::from_str(include_str!("../fixtures/business-public-keys.json"))?;
20    let pin = pins["profiles"]
21        .as_array()
22        .unwrap()
23        .iter()
24        .find(|p| p["certVersion"].as_u64() == Some(version))
25        .ok_or_else(|| anyhow::anyhow!("Missing business pin"))?;
26    Ok(PublicProfile {
27        version: version as u16,
28        point: hex::decode(pin["ecdhPointHex"].as_str().unwrap())?,
29        signature_pem: pin["ecdsaPEM"].as_str().unwrap().to_string(),
30    })
31}
32pub struct Prelogin {
33    pub request: Vec<u8>,
34    local: Ecdh,
35    plain: Vec<u8>,
36    derived: Vec<u8>,
37    encrypted_payload: Vec<u8>,
38    signature_pem: String,
39    has_auth_key: bool,
40}
41impl Drop for Prelogin {
42    fn drop(&mut self) {
43        self.plain.zeroize();
44        self.derived.zeroize();
45    }
46}
47impl Prelogin {
48    pub fn prepare(plain: &[u8], profile: &PublicProfile, auth_key: &[u8]) -> Result<Self> {
49        ensure!(
50            plain.len() <= MAX_MESSAGE_BYTES,
51            "Prelogin plaintext too large"
52        );
53        ensure!(
54            profile.point.len() == 65 && profile.point[0] == 4,
55            "Invalid business pin"
56        );
57        ensure!(
58            auth_key.is_empty() || auth_key.len() >= 24,
59            "Invalid saved Hybrid auth key"
60        );
61        let local = Ecdh::generate(Nid::X9_62_PRIME256V1)?;
62        let point = local.public()?;
63        let prefix = [b"1415".as_slice(), &point].concat();
64        let aad = sha256(&prefix);
65        let mut shared = sha256(&local.derive(&profile.point)?);
66        let mut random32 = random::<32>()?;
67        let encrypted_random = envelope_seal(&random32, &shared[..24], &aad, random()?)?;
68        shared.zeroize();
69        let auth_block = if auth_key.is_empty() {
70            vec![]
71        } else {
72            envelope_seal(&random32, &auth_key[..24], &aad, random()?)?
73        };
74        let mut extracted = hmac(b"security hdkf expand", &random32)?;
75        let derived = expand(&extracted, &aad, 56)?;
76        random32.zeroize();
77        extracted.zeroize();
78        let payload_aad = sha256(&[prefix, encrypted_random.clone(), auth_block.clone()].concat());
79        let encrypted_payload = envelope_seal(plain, &derived[..24], &payload_aad, random()?)?;
80        let request = p::message(&[
81            p::uint(1, 1),
82            p::bytes(2, &p::message(&[p::uint(1, 415), p::bytes(2, &point)])?),
83            p::bytes(3, &encrypted_random),
84            p::bytes(4, &auth_block),
85            p::bytes(5, &encrypted_payload),
86        ])?;
87        Ok(Self {
88            request,
89            local,
90            plain: plain.to_vec(),
91            derived,
92            encrypted_payload,
93            signature_pem: profile.signature_pem.clone(),
94            has_auth_key: !auth_key.is_empty(),
95        })
96    }
97    pub fn decrypt_reply(&self, reply: &[u8]) -> Result<Vec<u8>> {
98        let f = p::decode(reply)?;
99        let key = p::decode(p::data(&f, 1)?)?;
100        ensure!(p::scalar(&key, 1)? == 415, "Unsupported business curve");
101        let point = p::data(&key, 2)?;
102        let credential = p::scalar(&f, 2)?;
103        ensure!(
104            point.len() == 65
105                && point[0] == 4
106                && [1, 2].contains(&credential)
107                && (credential != 2 || self.has_auth_key),
108            "Unsupported business credential"
109        );
110        let ciphertext = p::data(&f, 3)?;
111        if credential == 1 {
112            verify_signature(
113                &self.signature_pem,
114                &sha256(&[self.encrypted_payload.as_slice(), ciphertext].concat()),
115                p::data(&f, 4)?,
116            )?;
117        }
118        let mut shared = sha256(&self.local.derive(point)?);
119        let aad = sha256(
120            &[
121                &self.derived[24..],
122                &self.plain,
123                b"415",
124                point,
125                credential.to_string().as_bytes(),
126            ]
127            .concat(),
128        );
129        let plain = envelope_open(ciphertext, &shared[..24], &aad);
130        shared.zeroize();
131        plain
132    }
133}
134pub struct PacketPolicy<'a> {
135    pub client_version: u32,
136    pub uin: u32,
137    pub cookie: &'a [u8],
138    pub cgi: u16,
139    pub original_length: usize,
140    pub pre_encryption_length: usize,
141    pub key_version: u16,
142    pub cert_version: u16,
143    pub checksum: u32,
144    pub compression: u8,
145    pub encryption: u8,
146    pub notice_id: u32,
147    pub sequence: u16,
148}
149pub fn pack(policy: PacketPolicy<'_>, encrypted: &[u8]) -> Result<Vec<u8>> {
150    ensure!(
151        policy.cookie.len() <= 15 && policy.compression <= 3 && policy.encryption <= 15,
152        "Invalid business policy"
153    );
154    ensure!(
155        policy.original_length <= MAX_MESSAGE_BYTES
156            && policy.pre_encryption_length <= MAX_MESSAGE_BYTES,
157        "Business payload too large"
158    );
159    let tail = [
160        policy.client_version.to_be_bytes().to_vec(),
161        policy.uin.to_be_bytes().to_vec(),
162        policy.cookie.to_vec(),
163        p::varint(policy.cgi as u64),
164        p::varint(policy.original_length as u64),
165        p::varint(policy.pre_encryption_length as u64),
166        p::varint(policy.key_version as u64),
167        p::varint(policy.cert_version as u64),
168        p::varint(policy.checksum as u64),
169        vec![0xff],
170        p::varint(policy.notice_id as u64),
171        vec![0, 0],
172        p::varint(0),
173        policy.sequence.to_be_bytes().to_vec(),
174    ]
175    .concat();
176    let length = tail.len() + 3;
177    ensure!(length <= 63, "Unsupported extended business header");
178    let flags = ((length as u16) << 2)
179        | policy.compression as u16
180        | ((policy.cookie.len() as u16) << 8)
181        | ((policy.encryption as u16) << 12);
182    Ok([
183        vec![0xbf],
184        flags.to_le_bytes().to_vec(),
185        tail,
186        encrypted.to_vec(),
187    ]
188    .concat())
189}
190pub fn prelogin_packet(
191    prelogin: &Prelogin,
192    cgi: u16,
193    key_version: u16,
194    cookie: &[u8],
195) -> Result<Vec<u8>> {
196    pack(
197        PacketPolicy {
198            client_version: 0,
199            uin: 0,
200            cookie,
201            cgi,
202            original_length: prelogin.plain.len(),
203            pre_encryption_length: prelogin.plain.len(),
204            key_version,
205            cert_version: 0,
206            checksum: 0,
207            compression: 2,
208            encryption: 12,
209            notice_id: crate::notice::notice_id(&prelogin.request)?,
210            sequence: u16::from_be_bytes(random()?),
211        },
212        &prelogin.request,
213    )
214}
215pub struct Envelope {
216    pub return_code: i32,
217    pub cgi: u64,
218    pub original_length: usize,
219    pub pre_encryption_length: usize,
220    pub compression: u8,
221    pub encryption: u8,
222    pub cookie: Vec<u8>,
223    pub payload: Vec<u8>,
224}
225pub fn split(input: &[u8]) -> Result<Envelope> {
226    ensure!(input.len() >= 10, "Truncated business response");
227    let base = usize::from(input[0] == 0xbf);
228    let flags = u16::from_le_bytes(input[base..base + 2].try_into()?);
229    let header = ((flags >> 2) & 63) as usize;
230    let cookie_len = ((flags >> 8) & 15) as usize;
231    let cookie_offset = base + 10;
232    ensure!(
233        header >= cookie_offset + cookie_len && header <= input.len(),
234        "Invalid business boundary"
235    );
236    let return_code = i32::from_be_bytes(input[base + 2..base + 6].try_into()?);
237    let mut offset = cookie_offset + cookie_len;
238    let cgi = p::read_varint(&input[..header], &mut offset)?;
239    let original_length = usize::try_from(p::read_varint(&input[..header], &mut offset)?)?;
240    let pre_encryption_length = usize::try_from(p::read_varint(&input[..header], &mut offset)?)?;
241    p::read_varint(&input[..header], &mut offset)?;
242    p::read_varint(&input[..header], &mut offset)?;
243    if base == 0 {
244        ensure!(offset == header, "Unknown business response extension");
245    }
246    ensure!(
247        original_length <= MAX_MESSAGE_BYTES && pre_encryption_length <= MAX_MESSAGE_BYTES,
248        "Response too large"
249    );
250    Ok(Envelope {
251        return_code,
252        cgi,
253        original_length,
254        pre_encryption_length,
255        compression: (flags & 3) as u8,
256        encryption: (flags >> 12) as u8,
257        cookie: input[cookie_offset..cookie_offset + cookie_len].to_vec(),
258        payload: input[header..].to_vec(),
259    })
260}
261pub fn checksum(uin: u32, payload: &[u8]) -> u32 {
262    let mut input = (payload.len() as u32).to_be_bytes().to_vec();
263    input.extend(md5(&uin.to_be_bytes()));
264    let prefix = md5(&input);
265    let mut a = 1u32;
266    let mut b = 0u32;
267    for byte in prefix.iter().chain(payload) {
268        a = (a + *byte as u32) % 65521;
269        b = (b + a) % 65521;
270    }
271    (b << 16) | a
272}
273pub struct AuthenticatedPacket {
274    pub packet: Vec<u8>,
275    cgi: u16,
276    mode: u8,
277    key: Vec<u8>,
278}
279impl Drop for AuthenticatedPacket {
280    fn drop(&mut self) {
281        self.key.zeroize();
282    }
283}
284impl AuthenticatedPacket {
285    pub fn prepare(state: &crate::AccountState, cgi: u16, plain: &[u8]) -> Result<Self> {
286        state.validate()?;
287        ensure!(
288            plain.len() <= MAX_MESSAGE_BYTES,
289            "Authenticated request too large"
290        );
291        let mode = if !state.request_key.is_empty() && !state.response_key.is_empty() {
292            13
293        } else {
294            5
295        };
296        let zipped = if mode == 5 {
297            deflate(plain)?
298        } else {
299            plain.to_vec()
300        };
301        let compressed = mode == 5 && zipped.len() < plain.len();
302        let body = if compressed { zipped.as_slice() } else { plain };
303        let key = if mode == 13 {
304            ensure!(
305                state.request_key.len() == 24 && state.response_key.len() == 24,
306                "Invalid main session keys"
307            );
308            state.response_key.clone()
309        } else {
310            ensure!(state.session_key.len() == 16, "Invalid origin session key");
311            state.session_key.clone()
312        };
313        let encrypted = if mode == 13 {
314            envelope_seal(body, &state.request_key, b"", random()?)?
315        } else {
316            cbc(body, &key, Mode::Encrypt)?
317        };
318        let packet = pack(
319            PacketPolicy {
320                client_version: state.device.client_version as u32,
321                uin: state.uin,
322                cookie: &state.cookie,
323                cgi,
324                original_length: plain.len(),
325                pre_encryption_length: body.len(),
326                key_version: 0,
327                cert_version: 39,
328                checksum: checksum(state.uin, plain),
329                compression: if compressed { 1 } else { 2 },
330                encryption: mode,
331                notice_id: crate::notice::notice_id(&encrypted)?,
332                sequence: u16::from_be_bytes(random()?),
333            },
334            &encrypted,
335        )?;
336        Ok(Self {
337            packet,
338            cgi,
339            mode,
340            key,
341        })
342    }
343    /// Call only on the plaintext returned by the authenticated MMTLS transport.
344    pub fn decode_verified_transport_reply(&self, input: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
345        let envelope = split(input)?;
346        ensure!(
347            envelope.return_code == 0,
348            "Native envelope returned {}",
349            envelope.return_code
350        );
351        ensure!(
352            envelope.cgi == self.cgi as u64
353                && envelope.encryption == self.mode
354                && [1, 2].contains(&envelope.compression),
355            "Unsupported authenticated response policy"
356        );
357        let body = if self.mode == 13 {
358            envelope_open(&envelope.payload, &self.key, b"")?
359        } else {
360            cbc(&envelope.payload, &self.key, Mode::Decrypt)?
361        };
362        ensure!(
363            body.len() == envelope.pre_encryption_length,
364            "Authenticated body length mismatch"
365        );
366        let plain = if envelope.compression == 1 {
367            inflate(&body)?
368        } else {
369            body
370        };
371        ensure!(
372            plain.len() == envelope.original_length,
373            "Authenticated plaintext length mismatch"
374        );
375        Ok((plain, envelope.cookie))
376    }
377}
378pub fn account_keys(inner: &Ecdh, auth: &[u8]) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
379    let f = p::decode(auth)?;
380    let flags = match p::optional(&f, 19)? {
381        Some(p::Value::Varint(v)) => *v,
382        None => 0,
383        _ => bail!("Invalid auth flags"),
384    };
385    let raw = p::unbuffer(p::data(&f, 3)?)?;
386    let session = if flags & 4 != 0 {
387        let e = p::decode(p::data(&f, 2)?)?;
388        ensure!(p::scalar(&e, 1)? == 713, "Unsupported account curve");
389        let point = p::unbuffer(p::data(&e, 2)?)?;
390        ensure!(point.len() == 57 && point[0] == 4, "Invalid account point");
391        let mut secret = inner.derive(&point)?;
392        ensure!(secret.len() == 28, "Invalid P224 secret");
393        let mut key = md5(&secret);
394        secret.zeroize();
395        let result = cbc(&raw, &key, Mode::Decrypt);
396        key.zeroize();
397        result?
398    } else {
399        raw
400    };
401    ensure!(
402        !session.is_empty() && session.len() <= 65536,
403        "Invalid derived session key"
404    );
405    let get = |n| -> Result<Vec<u8>> {
406        Ok(match p::optional_data(&f, n)? {
407            Some(b) => p::unbuffer(b)?,
408            None => vec![],
409        })
410    };
411    Ok((session, get(23)?, get(24)?))
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    #[test]
418    fn independent_checksum_vectors() {
419        for (uin, payload, expected) in [
420            (0, vec![], 0x4e970913),
421            (1, b"a".to_vec(), 0x39d20759),
422            (0x12345678, b"abc".to_vec(), 0x69430a30),
423            (u32::MAX, (0..=255).collect(), 0xf74e868e),
424        ] {
425            assert_eq!(checksum(uin, &payload), expected);
426        }
427    }
428    #[test]
429    fn prelogin_profiles_validate() {
430        for region in [1, 2] {
431            let profile = profile_for_region(region).unwrap();
432            assert!(
433                Prelogin::prepare(b"synthetic", &profile, &[])
434                    .unwrap()
435                    .request
436                    .len()
437                    > 100
438            );
439        }
440        assert!(profile_for_region(3).is_err());
441    }
442    #[test]
443    fn encrypted_envelope_rejects_aad() {
444        let key = [3; 24];
445        let aad = sha256(b"a");
446        let e = envelope_seal(b"synthetic", &key, &aad, [4; 12]).unwrap();
447        assert_eq!(envelope_open(&e, &key, &aad).unwrap(), b"synthetic");
448        assert!(envelope_open(&e, &key, &sha256(b"b")).is_err());
449    }
450    #[test]
451    fn synthetic_signed_peer_and_saved_credential() {
452        use openssl::{
453            ec::{EcGroup, EcKey},
454            hash::MessageDigest,
455            pkey::PKey,
456            sign::Signer,
457        };
458        let static_key = Ecdh::generate(Nid::X9_62_PRIME256V1).unwrap();
459        let signing = PKey::from_ec_key(
460            EcKey::generate(&EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap()).unwrap(),
461        )
462        .unwrap();
463        let profile = PublicProfile {
464            version: 1,
465            point: static_key.public().unwrap(),
466            signature_pem: String::from_utf8(signing.public_key_to_pem().unwrap()).unwrap(),
467        };
468        for credential in [1, 2] {
469            let auth = if credential == 2 {
470                vec![11; 380]
471            } else {
472                vec![]
473            };
474            let request = b"synthetic login";
475            let local = Prelogin::prepare(request, &profile, &auth).unwrap();
476            let f = p::decode(&local.request).unwrap();
477            let key = p::decode(p::data(&f, 2).unwrap()).unwrap();
478            let point = p::data(&key, 2).unwrap();
479            let prefix = [b"1415".as_slice(), point].concat();
480            let aad = sha256(&prefix);
481            let random = envelope_open(
482                p::data(&f, 3).unwrap(),
483                &sha256(&static_key.derive(point).unwrap())[..24],
484                &aad,
485            )
486            .unwrap();
487            let derived =
488                expand(&hmac(b"security hdkf expand", &random).unwrap(), &aad, 56).unwrap();
489            assert_eq!(
490                envelope_open(
491                    p::data(&f, 5).unwrap(),
492                    &derived[..24],
493                    &sha256(
494                        &[
495                            prefix,
496                            p::data(&f, 3).unwrap().to_vec(),
497                            p::data(&f, 4).unwrap().to_vec()
498                        ]
499                        .concat()
500                    )
501                )
502                .unwrap(),
503                request
504            );
505            if credential == 2 {
506                assert_eq!(
507                    envelope_open(p::data(&f, 4).unwrap(), &auth[..24], &aad).unwrap(),
508                    random
509                );
510            }
511            let ephemeral = Ecdh::generate(Nid::X9_62_PRIME256V1).unwrap();
512            let server_point = ephemeral.public().unwrap();
513            let response_aad = sha256(
514                &[
515                    &derived[24..],
516                    request,
517                    b"415",
518                    &server_point,
519                    credential.to_string().as_bytes(),
520                ]
521                .concat(),
522            );
523            let ciphertext = envelope_seal(
524                b"synthetic authenticated reply",
525                &sha256(&ephemeral.derive(point).unwrap())[..24],
526                &response_aad,
527                [4; 12],
528            )
529            .unwrap();
530            let mut fields = vec![
531                p::bytes(
532                    1,
533                    &p::message(&[p::uint(1, 415), p::bytes(2, &server_point)]).unwrap(),
534                ),
535                p::uint(2, credential),
536                p::bytes(3, &ciphertext),
537            ];
538            if credential == 1 {
539                let mut signer = Signer::new(MessageDigest::sha256(), &signing).unwrap();
540                signer
541                    .update(&sha256(&[p::data(&f, 5).unwrap(), &ciphertext].concat()))
542                    .unwrap();
543                fields.push(p::bytes(4, &signer.sign_to_vec().unwrap()));
544            }
545            let response = p::message(&fields).unwrap();
546            assert_eq!(
547                local.decrypt_reply(&response).unwrap(),
548                b"synthetic authenticated reply"
549            );
550            let mut corrupt = response;
551            let n = corrupt.len();
552            corrupt[n - 1] ^= 1;
553            assert!(local.decrypt_reply(&corrupt).is_err());
554        }
555    }
556}