Skip to main content

ferogram_mtproto/
authentication.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2//
3// ferogram: async Telegram MTProto client in Rust
4// https://github.com/ankit-chaubey/ferogram
5//
6// Licensed under either the MIT License or the Apache License 2.0.
7// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
8// https://github.com/ankit-chaubey/ferogram
9//
10// Feel free to use, modify, and share this code.
11// Please keep this notice when redistributing.
12
13use std::fmt;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use ferogram_crypto::{
17    AuthKey, aes, check_p_and_g, factorize, fill_random, generate_key_data_from_nonce, rsa,
18};
19use ferogram_tl_types::{Cursor, Deserializable, Serializable};
20use num_bigint::BigUint;
21
22// Manual TL serialization helper for PQInnerDataDc
23// Constructor: p_q_inner_data_dc#a9f55f95
24//   pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int
25// TL "string" (bytes) encoding: if len < 254 -> [len_byte, data..., 0-pad to 4-align],
26// else [0xfe, len_lo, len_mid, len_hi, data..., 0-pad to 4-align].
27fn tl_serialize_bytes(v: &[u8]) -> Vec<u8> {
28    let len = v.len();
29    let mut out = Vec::new();
30    if len < 254 {
31        out.push(len as u8);
32        out.extend_from_slice(v);
33        let total = 1 + len;
34        let pad = (4 - total % 4) % 4;
35        out.extend(std::iter::repeat_n(0u8, pad));
36    } else {
37        out.push(0xfe);
38        out.push((len & 0xff) as u8);
39        out.push(((len >> 8) & 0xff) as u8);
40        out.push(((len >> 16) & 0xff) as u8);
41        out.extend_from_slice(v);
42        let total = 4 + len;
43        let pad = (4 - total % 4) % 4;
44        out.extend(std::iter::repeat_n(0u8, pad));
45    }
46    out
47}
48
49/// Serialize a `p_q_inner_data_dc` (constructor 0xa9f55f95) from raw fields.
50/// Needed because the generated TL bindings only expose `PQInnerData`
51/// (legacy, no DC id) which Telegram rejects for non-DC2 connections.
52fn serialize_pq_inner_data_dc(
53    pq: &[u8],
54    p: &[u8],
55    q: &[u8],
56    nonce: &[u8; 16],
57    server_nonce: &[u8; 16],
58    new_nonce: &[u8; 32],
59    dc_id: i32,
60) -> Vec<u8> {
61    let mut out = Vec::new();
62    // Constructor id (little-endian)
63    out.extend_from_slice(&0xa9f55f95_u32.to_le_bytes());
64    out.extend(tl_serialize_bytes(pq));
65    out.extend(tl_serialize_bytes(p));
66    out.extend(tl_serialize_bytes(q));
67    out.extend_from_slice(nonce);
68    out.extend_from_slice(server_nonce);
69    out.extend_from_slice(new_nonce);
70    out.extend_from_slice(&dc_id.to_le_bytes());
71    out
72}
73
74/// Serialize p_q_inner_data_temp_dc#56fddf88 (temp key variant with expires_in).
75#[allow(clippy::too_many_arguments)]
76fn serialize_pq_inner_data_temp_dc(
77    pq: &[u8],
78    p: &[u8],
79    q: &[u8],
80    nonce: &[u8; 16],
81    server_nonce: &[u8; 16],
82    new_nonce: &[u8; 32],
83    dc_id: i32,
84    expires_in: i32,
85) -> Vec<u8> {
86    let mut out = Vec::new();
87    out.extend_from_slice(&0x56fddf88_u32.to_le_bytes()); // p_q_inner_data_temp_dc
88    out.extend(tl_serialize_bytes(pq));
89    out.extend(tl_serialize_bytes(p));
90    out.extend(tl_serialize_bytes(q));
91    out.extend_from_slice(nonce);
92    out.extend_from_slice(server_nonce);
93    out.extend_from_slice(new_nonce);
94    out.extend_from_slice(&dc_id.to_le_bytes());
95    out.extend_from_slice(&expires_in.to_le_bytes()); // extra field vs permanent variant
96    out
97}
98
99/// Errors that can occur during auth key generation.
100#[allow(missing_docs)]
101#[derive(Clone, Debug, PartialEq)]
102pub enum Error {
103    InvalidNonce {
104        got: [u8; 16],
105        expected: [u8; 16],
106    },
107    InvalidPqSize {
108        size: usize,
109    },
110    UnknownFingerprints {
111        fingerprints: Vec<i64>,
112    },
113    DhParamsFail,
114    InvalidServerNonce {
115        got: [u8; 16],
116        expected: [u8; 16],
117    },
118    EncryptedResponseNotPadded {
119        len: usize,
120    },
121    InvalidDhInnerData {
122        error: ferogram_tl_types::deserialize::Error,
123    },
124    InvalidDhPrime {
125        source: ferogram_crypto::DhError,
126    },
127    GParameterOutOfRange {
128        value: BigUint,
129        low: BigUint,
130        high: BigUint,
131    },
132    DhGenRetry,
133    DhGenFail,
134    InvalidAnswerHash {
135        got: [u8; 20],
136        expected: [u8; 20],
137    },
138    InvalidNewNonceHash {
139        got: [u8; 16],
140        expected: [u8; 16],
141    },
142}
143
144impl std::error::Error for Error {}
145
146impl fmt::Display for Error {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        match self {
149            Self::InvalidNonce { got, expected } => {
150                write!(f, "nonce mismatch: got {got:?}, expected {expected:?}")
151            }
152            Self::InvalidPqSize { size } => write!(f, "pq size {size} invalid (expected 8)"),
153            Self::UnknownFingerprints { fingerprints } => {
154                write!(f, "no known fingerprint in {fingerprints:?}")
155            }
156            Self::DhParamsFail => write!(f, "server returned DH params failure"),
157            Self::InvalidServerNonce { got, expected } => write!(
158                f,
159                "server_nonce mismatch: got {got:?}, expected {expected:?}"
160            ),
161            Self::EncryptedResponseNotPadded { len } => {
162                write!(f, "encrypted answer len {len} is not 16-byte aligned")
163            }
164            Self::InvalidDhInnerData { error } => {
165                write!(f, "DH inner data deserialization error: {error}")
166            }
167            Self::InvalidDhPrime { source } => {
168                write!(f, "DH prime/generator validation failed: {source}")
169            }
170            Self::GParameterOutOfRange { value, low, high } => {
171                write!(f, "g={value} not in range ({low}, {high})")
172            }
173            Self::DhGenRetry => write!(f, "DH gen retry requested"),
174            Self::DhGenFail => write!(f, "DH gen failed"),
175            Self::InvalidAnswerHash { got, expected } => write!(
176                f,
177                "answer hash mismatch: got {got:?}, expected {expected:?}"
178            ),
179            Self::InvalidNewNonceHash { got, expected } => write!(
180                f,
181                "new nonce hash mismatch: got {got:?}, expected {expected:?}"
182            ),
183        }
184    }
185}
186
187/// State after step 1.
188pub struct Step1 {
189    nonce: [u8; 16],
190}
191
192/// State after step 2.
193#[derive(Clone)]
194pub struct Step2 {
195    nonce: [u8; 16],
196    server_nonce: [u8; 16],
197    new_nonce: [u8; 32],
198}
199
200/// Pre-processed server DH parameters retained so that step 3 can be
201/// repeated on `dh_gen_retry` without having to re-decrypt the server response.
202#[derive(Clone)]
203pub struct DhParamsForRetry {
204    /// Server-supplied DH prime (big-endian bytes).
205    pub dh_prime: Vec<u8>,
206    /// DH generator `g`.
207    pub g: u32,
208    /// Server's public DH value `g_a` (big-endian bytes).
209    pub g_a: Vec<u8>,
210    /// Server's reported Unix timestamp (used to compute `time_offset`).
211    pub server_time: i32,
212    /// AES key derived from nonces for this session's IGE encryption.
213    pub aes_key: [u8; 32],
214    /// AES IV derived from nonces for this session's IGE encryption.
215    pub aes_iv: [u8; 32],
216}
217
218/// State after step 3.
219pub struct Step3 {
220    nonce: [u8; 16],
221    server_nonce: [u8; 16],
222    new_nonce: [u8; 32],
223    time_offset: i32,
224    /// Auth key candidate bytes (needed to derive `auth_key_aux_hash` on retry).
225    auth_key: [u8; 256],
226    /// The processed DH parameters stored so `retry_step3` can re-derive g_b
227    /// without re-parsing the encrypted server response.
228    pub dh_params: DhParamsForRetry,
229}
230
231/// Result of [`finish`] either the handshake is done, or the server wants us
232/// to retry step 3 with the `auth_key_aux_hash` as `retry_id`.
233pub enum FinishResult {
234    /// Handshake complete.
235    Done(Finished),
236    /// Server sent `dh_gen_retry`.  Call [`retry_step3`] with the returned
237    /// `retry_id` and the stored [`DhParamsForRetry`] from the previous Step3.
238    Retry {
239        /// The `auth_key_aux_hash` to embed as `retry_id` in the next attempt.
240        retry_id: i64,
241        /// DH parameters to feed back into [`retry_step3`].
242        dh_params: DhParamsForRetry,
243        /// Client nonce from the original step 1.
244        nonce: [u8; 16],
245        /// Server nonce from the ResPQ response.
246        server_nonce: [u8; 16],
247        /// Fresh nonce generated in step 2.
248        new_nonce: [u8; 32],
249    },
250}
251
252/// The final output of a successful auth key handshake.
253#[derive(Clone, Debug, PartialEq)]
254pub struct Finished {
255    /// The 256-byte Telegram authorization key.
256    pub auth_key: [u8; 256],
257    /// Clock skew in seconds relative to the server.
258    pub time_offset: i32,
259    /// Initial server salt.
260    pub first_salt: i64,
261}
262
263/// Generate a `req_pq_multi` request. Returns the request + opaque state.
264pub fn step1() -> Result<(ferogram_tl_types::functions::ReqPqMulti, Step1), Error> {
265    let mut buf = [0u8; 16];
266    fill_random(&mut buf);
267    do_step1(&buf)
268}
269
270fn do_step1(random: &[u8; 16]) -> Result<(ferogram_tl_types::functions::ReqPqMulti, Step1), Error> {
271    let nonce = *random;
272    Ok((
273        ferogram_tl_types::functions::ReqPqMulti { nonce },
274        Step1 { nonce },
275    ))
276}
277
278/// Process `ResPQ` and generate `req_DH_params`.
279///
280/// `dc_id` must be the numerical DC id of the server we are connecting to
281/// (e.g. 1 ... 5).  It is embedded in the `PQInnerDataDc` payload so that
282/// Telegram can reject misrouted handshakes on non-DC2 endpoints.
283pub fn step2(
284    data: Step1,
285    response: ferogram_tl_types::enums::ResPq,
286    dc_id: i32,
287) -> Result<(ferogram_tl_types::functions::ReqDhParams, Step2), Error> {
288    let mut rnd = [0u8; 256];
289    fill_random(&mut rnd);
290    do_step2(data, response, &rnd, dc_id)
291}
292
293/// Like `step2` but requests a temporary auth key (PFS).
294/// Uses p_q_inner_data_temp_dc instead of p_q_inner_data_dc.
295/// Caller must bind the resulting key via auth.bindTempAuthKey before any RPCs.
296pub fn step2_temp(
297    data: Step1,
298    response: ferogram_tl_types::enums::ResPq,
299    dc_id: i32,
300    expires_in: i32,
301) -> Result<(ferogram_tl_types::functions::ReqDhParams, Step2), Error> {
302    let mut rnd = [0u8; 256];
303    fill_random(&mut rnd);
304    do_step2_temp(data, response, &rnd, dc_id, expires_in)
305}
306
307fn do_step2_temp(
308    data: Step1,
309    response: ferogram_tl_types::enums::ResPq,
310    random: &[u8; 256],
311    dc_id: i32,
312    expires_in: i32,
313) -> Result<(ferogram_tl_types::functions::ReqDhParams, Step2), Error> {
314    let Step1 { nonce } = data;
315    let ferogram_tl_types::enums::ResPq::ResPq(res_pq) = response;
316    check_nonce(&res_pq.nonce, &nonce)?;
317    if res_pq.pq.len() != 8 {
318        return Err(Error::InvalidPqSize {
319            size: res_pq.pq.len(),
320        });
321    }
322    let pq = u64::from_be_bytes(res_pq.pq.as_slice().try_into().unwrap());
323    let (p, q) = factorize(pq);
324    let mut new_nonce = [0u8; 32];
325    new_nonce.copy_from_slice(&random[..32]);
326    let rnd224: &[u8; 224] = random[32..].try_into().unwrap();
327    fn trim_be(v: u64) -> Vec<u8> {
328        let b = v.to_be_bytes();
329        let skip = b.iter().position(|&x| x != 0).unwrap_or(7);
330        b[skip..].to_vec()
331    }
332    let p_bytes = trim_be(p);
333    let q_bytes = trim_be(q);
334    let pq_inner = serialize_pq_inner_data_temp_dc(
335        &pq.to_be_bytes(),
336        &p_bytes,
337        &q_bytes,
338        &nonce,
339        &res_pq.server_nonce,
340        &new_nonce,
341        dc_id,
342        expires_in,
343    );
344    let fingerprint = res_pq
345        .server_public_key_fingerprints
346        .iter()
347        .copied()
348        .find(|&fp| key_for_fingerprint(fp).is_some())
349        .ok_or_else(|| Error::UnknownFingerprints {
350            fingerprints: res_pq.server_public_key_fingerprints.clone(),
351        })?;
352    let key = key_for_fingerprint(fingerprint).unwrap();
353    let ciphertext = rsa::encrypt_hashed(&pq_inner, &key, rnd224);
354    Ok((
355        ferogram_tl_types::functions::ReqDhParams {
356            nonce,
357            server_nonce: res_pq.server_nonce,
358            p: p_bytes,
359            q: q_bytes,
360            public_key_fingerprint: fingerprint,
361            encrypted_data: ciphertext,
362        },
363        Step2 {
364            nonce,
365            server_nonce: res_pq.server_nonce,
366            new_nonce,
367        },
368    ))
369}
370
371fn do_step2(
372    data: Step1,
373    response: ferogram_tl_types::enums::ResPq,
374    random: &[u8; 256],
375    dc_id: i32,
376) -> Result<(ferogram_tl_types::functions::ReqDhParams, Step2), Error> {
377    let Step1 { nonce } = data;
378
379    // ResPq has a single constructor: resPQ -> variant ResPq
380    let ferogram_tl_types::enums::ResPq::ResPq(res_pq) = response;
381
382    check_nonce(&res_pq.nonce, &nonce)?;
383
384    if res_pq.pq.len() != 8 {
385        return Err(Error::InvalidPqSize {
386            size: res_pq.pq.len(),
387        });
388    }
389
390    let pq = u64::from_be_bytes(res_pq.pq.as_slice().try_into().unwrap());
391    let (p, q) = factorize(pq);
392
393    let mut new_nonce = [0u8; 32];
394    new_nonce.copy_from_slice(&random[..32]);
395
396    // random[32..256] is 224 bytes for RSA padding
397    let rnd224: &[u8; 224] = random[32..].try_into().unwrap();
398
399    fn trim_be(v: u64) -> Vec<u8> {
400        let b = v.to_be_bytes();
401        let skip = b.iter().position(|&x| x != 0).unwrap_or(7);
402        b[skip..].to_vec()
403    }
404
405    let p_bytes = trim_be(p);
406    let q_bytes = trim_be(q);
407
408    // Serialize PQInnerDataDc (constructor 0xa9f55f95) manually.
409    // The legacy PQInnerData constructor (#83c95aec, no dc field) is rejected
410    // by Telegram servers for connections to non-DC2 endpoints.
411    let pq_inner = serialize_pq_inner_data_dc(
412        &pq.to_be_bytes(),
413        &p_bytes,
414        &q_bytes,
415        &nonce,
416        &res_pq.server_nonce,
417        &new_nonce,
418        dc_id,
419    );
420
421    let fingerprint = res_pq
422        .server_public_key_fingerprints
423        .iter()
424        .copied()
425        .find(|&fp| key_for_fingerprint(fp).is_some())
426        .ok_or_else(|| Error::UnknownFingerprints {
427            fingerprints: res_pq.server_public_key_fingerprints.clone(),
428        })?;
429
430    let key = key_for_fingerprint(fingerprint).unwrap();
431    let ciphertext = rsa::encrypt_hashed(&pq_inner, &key, rnd224);
432
433    Ok((
434        ferogram_tl_types::functions::ReqDhParams {
435            nonce,
436            server_nonce: res_pq.server_nonce,
437            p: p_bytes,
438            q: q_bytes,
439            public_key_fingerprint: fingerprint,
440            encrypted_data: ciphertext,
441        },
442        Step2 {
443            nonce,
444            server_nonce: res_pq.server_nonce,
445            new_nonce,
446        },
447    ))
448}
449
450/// Process `ServerDhParams` into a reusable [`DhParamsForRetry`] + send the
451/// first `set_client_DH_params` request.
452///
453/// `retry_id` should be 0 on the first call, or `auth_key_aux_hash` (returned
454/// by [`finish`] as [`FinishResult::Retry`]) on subsequent attempts.
455pub fn step3(
456    data: Step2,
457    response: ferogram_tl_types::enums::ServerDhParams,
458) -> Result<(ferogram_tl_types::functions::SetClientDhParams, Step3), Error> {
459    let mut rnd = [0u8; 272];
460    fill_random(&mut rnd);
461    let now = SystemTime::now()
462        .duration_since(UNIX_EPOCH)
463        .unwrap()
464        .as_secs() as i32;
465    do_step3(data, response, &rnd, now, 0)
466}
467
468/// Re-run the client DH params generation after a `dh_gen_retry` response.
469/// Feed the `dh_params`, `nonce`, `server_nonce`, `new_nonce` from
470/// [`FinishResult::Retry`] and the `retry_id` (= `auth_key_aux_hash`).
471pub fn retry_step3(
472    dh_params: &DhParamsForRetry,
473    nonce: [u8; 16],
474    server_nonce: [u8; 16],
475    new_nonce: [u8; 32],
476    retry_id: i64,
477) -> Result<(ferogram_tl_types::functions::SetClientDhParams, Step3), Error> {
478    let mut rnd = [0u8; 272];
479    fill_random(&mut rnd);
480    let now = SystemTime::now()
481        .duration_since(UNIX_EPOCH)
482        .unwrap()
483        .as_secs() as i32;
484    generate_client_dh_params(
485        dh_params,
486        nonce,
487        server_nonce,
488        new_nonce,
489        retry_id,
490        &rnd,
491        now,
492    )
493}
494
495fn generate_client_dh_params(
496    dh: &DhParamsForRetry,
497    nonce: [u8; 16],
498    server_nonce: [u8; 16],
499    new_nonce: [u8; 32],
500    retry_id: i64,
501    random: &[u8; 272],
502    now: i32,
503) -> Result<(ferogram_tl_types::functions::SetClientDhParams, Step3), Error> {
504    let dh_prime = BigUint::from_bytes_be(&dh.dh_prime);
505    let g = BigUint::from(dh.g);
506    let g_a = BigUint::from_bytes_be(&dh.g_a);
507    let time_offset = dh.server_time - now;
508
509    let b = BigUint::from_bytes_be(&random[..256]);
510    let g_b = g.modpow(&b, &dh_prime);
511
512    let one = BigUint::from(1u32);
513    let safety = one.clone() << (2048 - 64);
514    check_g_in_range(&g_b, &one, &(&dh_prime - &one))?;
515    check_g_in_range(&g_b, &safety, &(&dh_prime - &safety))?;
516
517    let client_dh_inner = ferogram_tl_types::enums::ClientDhInnerData::ClientDhInnerData(
518        ferogram_tl_types::types::ClientDhInnerData {
519            nonce,
520            server_nonce,
521            retry_id,
522            g_b: g_b.to_bytes_be(),
523        },
524    )
525    .to_bytes();
526
527    let digest: [u8; 20] = ferogram_crypto::sha1!(&client_dh_inner);
528
529    let pad_len = (16 - ((20 + client_dh_inner.len()) % 16)) % 16;
530    let rnd16 = &random[256..256 + pad_len.min(16)];
531
532    let mut hashed = Vec::with_capacity(20 + client_dh_inner.len() + pad_len);
533    hashed.extend_from_slice(&digest);
534    hashed.extend_from_slice(&client_dh_inner);
535    hashed.extend_from_slice(&rnd16[..pad_len]);
536
537    let key: [u8; 32] = dh.aes_key;
538    let iv: [u8; 32] = dh.aes_iv;
539    aes::ige_encrypt(&mut hashed, &key, &iv);
540
541    // Compute auth_key = g_a^b mod dh_prime for this attempt.
542    let mut auth_key_bytes = [0u8; 256];
543    let gab_bytes = g_a.modpow(&b, &dh_prime).to_bytes_be();
544    let skip = 256 - gab_bytes.len();
545    auth_key_bytes[skip..].copy_from_slice(&gab_bytes);
546
547    Ok((
548        ferogram_tl_types::functions::SetClientDhParams {
549            nonce,
550            server_nonce,
551            encrypted_data: hashed,
552        },
553        Step3 {
554            nonce,
555            server_nonce,
556            new_nonce,
557            time_offset,
558            auth_key: auth_key_bytes,
559            dh_params: dh.clone(),
560        },
561    ))
562}
563
564fn do_step3(
565    data: Step2,
566    response: ferogram_tl_types::enums::ServerDhParams,
567    random: &[u8; 272],
568    now: i32,
569    retry_id: i64,
570) -> Result<(ferogram_tl_types::functions::SetClientDhParams, Step3), Error> {
571    let Step2 {
572        nonce,
573        server_nonce,
574        new_nonce,
575    } = data;
576
577    let mut server_dh_ok = match response {
578        ferogram_tl_types::enums::ServerDhParams::Fail(f) => {
579            check_nonce(&f.nonce, &nonce)?;
580            check_server_nonce(&f.server_nonce, &server_nonce)?;
581            return Err(Error::DhParamsFail);
582        }
583        ferogram_tl_types::enums::ServerDhParams::Ok(x) => x,
584    };
585
586    check_nonce(&server_dh_ok.nonce, &nonce)?;
587    check_server_nonce(&server_dh_ok.server_nonce, &server_nonce)?;
588
589    if server_dh_ok.encrypted_answer.len() % 16 != 0 {
590        return Err(Error::EncryptedResponseNotPadded {
591            len: server_dh_ok.encrypted_answer.len(),
592        });
593    }
594
595    let (key_arr, iv_arr) = generate_key_data_from_nonce(&server_nonce, &new_nonce);
596    aes::ige_decrypt(&mut server_dh_ok.encrypted_answer, &key_arr, &iv_arr);
597    let plain = server_dh_ok.encrypted_answer;
598
599    let got_hash: [u8; 20] = plain[..20].try_into().unwrap();
600    let mut cursor = Cursor::from_slice(&plain[20..]);
601
602    let inner = match ferogram_tl_types::enums::ServerDhInnerData::deserialize(&mut cursor) {
603        Ok(ferogram_tl_types::enums::ServerDhInnerData::ServerDhInnerData(x)) => x,
604        Err(e) => return Err(Error::InvalidDhInnerData { error: e }),
605    };
606
607    let expected_hash: [u8; 20] = ferogram_crypto::sha1!(&plain[20..20 + cursor.pos()]);
608    if got_hash != expected_hash {
609        return Err(Error::InvalidAnswerHash {
610            got: got_hash,
611            expected: expected_hash,
612        });
613    }
614
615    check_nonce(&inner.nonce, &nonce)?;
616    check_server_nonce(&inner.server_nonce, &server_nonce)?;
617
618    check_p_and_g(&inner.dh_prime, inner.g as u32)
619        .map_err(|source| Error::InvalidDhPrime { source })?;
620
621    // Validate g_a range.
622    let dh_prime_bn = BigUint::from_bytes_be(&inner.dh_prime);
623    let one = BigUint::from(1u32);
624    let g_a_bn = BigUint::from_bytes_be(&inner.g_a);
625    let safety = one.clone() << (2048 - 64);
626    check_g_in_range(&g_a_bn, &safety, &(&dh_prime_bn - &safety))?;
627
628    let dh = DhParamsForRetry {
629        dh_prime: inner.dh_prime,
630        g: inner.g as u32,
631        g_a: inner.g_a,
632        server_time: inner.server_time,
633        aes_key: key_arr,
634        aes_iv: iv_arr,
635    };
636
637    generate_client_dh_params(&dh, nonce, server_nonce, new_nonce, retry_id, random, now)
638}
639
640/// Finalise the handshake.
641///
642/// Returns [`FinishResult::Done`] on success or [`FinishResult::Retry`] when
643/// the server sends `dh_gen_retry` (up to 5 attempts are typical). On retry,
644/// call [`retry_step3`] with the returned fields, send the new request, receive
645/// the answer, then call `finish` again.
646pub fn finish(
647    data: Step3,
648    response: ferogram_tl_types::enums::SetClientDhParamsAnswer,
649) -> Result<FinishResult, Error> {
650    let Step3 {
651        nonce,
652        server_nonce,
653        new_nonce,
654        time_offset,
655        auth_key: auth_key_bytes,
656        dh_params,
657    } = data;
658
659    struct DhData {
660        nonce: [u8; 16],
661        server_nonce: [u8; 16],
662        hash: [u8; 16],
663        num: u8,
664    }
665
666    let dh = match response {
667        // Variant names come from the constructor names: dh_gen_ok -> DhGenOk, etc.
668        ferogram_tl_types::enums::SetClientDhParamsAnswer::DhGenOk(x) => DhData {
669            nonce: x.nonce,
670            server_nonce: x.server_nonce,
671            hash: x.new_nonce_hash1,
672            num: 1,
673        },
674        ferogram_tl_types::enums::SetClientDhParamsAnswer::DhGenRetry(x) => DhData {
675            nonce: x.nonce,
676            server_nonce: x.server_nonce,
677            hash: x.new_nonce_hash2,
678            num: 2,
679        },
680        ferogram_tl_types::enums::SetClientDhParamsAnswer::DhGenFail(x) => DhData {
681            nonce: x.nonce,
682            server_nonce: x.server_nonce,
683            hash: x.new_nonce_hash3,
684            num: 3,
685        },
686    };
687
688    check_nonce(&dh.nonce, &nonce)?;
689    check_server_nonce(&dh.server_nonce, &server_nonce)?;
690
691    let auth_key = AuthKey::from_bytes(auth_key_bytes);
692    let expected_hash = auth_key.calc_new_nonce_hash(&new_nonce, dh.num);
693    check_new_nonce_hash(&dh.hash, &expected_hash)?;
694
695    let first_salt = {
696        let mut buf = [0u8; 8];
697        for ((dst, a), b) in buf.iter_mut().zip(&new_nonce[..8]).zip(&server_nonce[..8]) {
698            *dst = a ^ b;
699        }
700        i64::from_le_bytes(buf)
701    };
702
703    match dh.num {
704        1 => Ok(FinishResult::Done(Finished {
705            auth_key: auth_key.to_bytes(),
706            time_offset,
707            first_salt,
708        })),
709        2 => {
710            // dh_gen_retry: compute auth_key_aux_hash = SHA1(auth_key)[0..8] as i64 LE.
711            let aux_hash: [u8; 20] = ferogram_crypto::sha1!(auth_key.to_bytes());
712            let retry_id = i64::from_le_bytes(aux_hash[..8].try_into().unwrap());
713            Ok(FinishResult::Retry {
714                retry_id,
715                dh_params,
716                nonce,
717                server_nonce,
718                new_nonce,
719            })
720        }
721        _ => Err(Error::DhGenFail),
722    }
723}
724
725fn check_nonce(got: &[u8; 16], expected: &[u8; 16]) -> Result<(), Error> {
726    if got == expected {
727        Ok(())
728    } else {
729        Err(Error::InvalidNonce {
730            got: *got,
731            expected: *expected,
732        })
733    }
734}
735fn check_server_nonce(got: &[u8; 16], expected: &[u8; 16]) -> Result<(), Error> {
736    if got == expected {
737        Ok(())
738    } else {
739        Err(Error::InvalidServerNonce {
740            got: *got,
741            expected: *expected,
742        })
743    }
744}
745fn check_new_nonce_hash(got: &[u8; 16], expected: &[u8; 16]) -> Result<(), Error> {
746    if got == expected {
747        Ok(())
748    } else {
749        Err(Error::InvalidNewNonceHash {
750            got: *got,
751            expected: *expected,
752        })
753    }
754}
755fn check_g_in_range(val: &BigUint, lo: &BigUint, hi: &BigUint) -> Result<(), Error> {
756    if lo < val && val < hi {
757        Ok(())
758    } else {
759        Err(Error::GParameterOutOfRange {
760            value: val.clone(),
761            low: lo.clone(),
762            high: hi.clone(),
763        })
764    }
765}
766
767/// RSA key by server fingerprint. Includes both production and test DC keys.
768#[allow(clippy::unreadable_literal)]
769pub fn key_for_fingerprint(fp: i64) -> Option<rsa::Key> {
770    Some(match fp {
771        // Production DC key (fingerprint -3414540481677951611)
772        -3414540481677951611 => rsa::Key::new(
773            "29379598170669337022986177149456128565388431120058863768162556424047512191330847455146576344487764408661701890505066208632169112269581063774293102577308490531282748465986139880977280302242772832972539403531316010870401287642763009136156734339538042419388722777357134487746169093539093850251243897188928735903389451772730245253062963384108812842079887538976360465290946139638691491496062099570836476454855996319192747663615955633778034897140982517446405334423701359108810182097749467210509584293428076654573384828809574217079944388301239431309115013843331317877374435868468779972014486325557807783825502498215169806323",
774            "65537",
775        )?,
776        // Test DC key (fingerprint -5595554452916591101)
777        -5595554452916591101 => rsa::Key::new(
778            "25342889448840415564971689590713473206898847759084779052582026594546022463853940585885215951168491965708222649399180603818074200620463776135424884632162512403163793083921641631564740959529419359595852941166848940585952337613333022396096584117954892216031229237302943701877588456738335398602461675225081791820393153757504952636234951323237820036543581047826906120927972487366805292115792231423684261262330394324750785450942589751755390156647751460719351439969059949569615302809050721500330239005077889855323917509948255722081644689442127297605422579707142646660768825302832201908302295573257427896031830742328565032949",
779            "65537",
780        )?,
781        _ => return None,
782    })
783}