Skip to main content

ferogram_crypto/
srp.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15//! Telegram SRP 2FA (RFC 5054-based).
16
17use hmac::Hmac;
18use num_bigint::{BigInt, Sign};
19use num_traits::ops::euclid::Euclid;
20use sha2::{Digest, Sha256, Sha512};
21
22fn sha256(parts: &[&[u8]]) -> [u8; 32] {
23    let mut h = Sha256::new();
24    for p in parts {
25        h.update(p);
26    }
27    h.finalize().into()
28}
29
30fn sh(data: &[u8], salt: &[u8]) -> [u8; 32] {
31    sha256(&[salt, data, salt])
32}
33
34fn ph1(password: &[u8], salt1: &[u8], salt2: &[u8]) -> [u8; 32] {
35    sh(&sh(password, salt1), salt2)
36}
37
38fn ph2(password: &[u8], salt1: &[u8], salt2: &[u8]) -> [u8; 32] {
39    let hash1 = ph1(password, salt1, salt2);
40    let mut dk = [0u8; 64];
41    pbkdf2::pbkdf2::<Hmac<Sha512>>(&hash1, salt1, 100_000, &mut dk).unwrap();
42    sh(&dk, salt2)
43}
44
45fn pad256(data: &[u8]) -> [u8; 256] {
46    let mut out = [0u8; 256];
47    let start = 256usize.saturating_sub(data.len());
48    out[start..].copy_from_slice(&data[data.len().saturating_sub(256)..]);
49    out
50}
51
52fn xor32(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
53    let mut out = [0u8; 32];
54    for i in 0..32 {
55        out[i] = a[i] ^ b[i];
56    }
57    out
58}
59
60/// Error returned when SRP parameter validation fails.
61#[derive(Debug)]
62pub enum SrpError {
63    /// Server's g_b outside (1, p-1). RFC 5054 s2.6: exposes verifier offline.
64    GbOutOfRange,
65    /// Client's ephemeral g_a is outside (1, p-1). Degenerate exponentiation.
66    GaOutOfRange,
67}
68
69impl std::fmt::Display for SrpError {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            SrpError::GbOutOfRange => write!(f, "SRP: server g_b out of safe range"),
73            SrpError::GaOutOfRange => write!(f, "SRP: client g_a out of safe range"),
74        }
75    }
76}
77
78impl std::error::Error for SrpError {}
79
80/// Compute SRP `(M1, g_a)` for Telegram 2FA.
81///
82/// Returns `Err(SrpError)` if g_b or g_a is outside (1, p-1).
83/// RFC 5054 s2.6: out-of-range g_b lets a server recover the verifier offline.
84pub fn calculate_2fa(
85    salt1: &[u8],
86    salt2: &[u8],
87    p: &[u8],
88    g: i32,
89    g_b: &[u8],
90    a: &[u8],
91    password: impl AsRef<[u8]>,
92) -> Result<([u8; 32], [u8; 256]), SrpError> {
93    let big_p = BigInt::from_bytes_be(Sign::Plus, p);
94    let g_b = pad256(g_b);
95    let a = pad256(a);
96    let g_hash = pad256(&[g as u8]);
97
98    let big_g_b = BigInt::from_bytes_be(Sign::Plus, &g_b);
99    let big_g = BigInt::from(g as u32);
100    let big_a = BigInt::from_bytes_be(Sign::Plus, &a);
101
102    // Validate g_b in (1, p-1) as required by spec.
103    {
104        let one = BigInt::from(1u32);
105        let p_minus_one = &big_p - &one;
106        if big_g_b <= one || big_g_b >= p_minus_one {
107            return Err(SrpError::GbOutOfRange);
108        }
109    }
110
111    let k = sha256(&[p, &g_hash]);
112    let big_k = BigInt::from_bytes_be(Sign::Plus, &k);
113
114    let g_a = big_g.modpow(&big_a, &big_p);
115    let g_a = pad256(&g_a.to_bytes_be().1);
116
117    // Validate our own g_a in (1, p-1).
118    {
119        let big_g_a = BigInt::from_bytes_be(Sign::Plus, &g_a);
120        let one = BigInt::from(1u32);
121        let p_minus_one = &big_p - &one;
122        if big_g_a <= one || big_g_a >= p_minus_one {
123            return Err(SrpError::GaOutOfRange);
124        }
125    }
126
127    let u = sha256(&[&g_a, &g_b]);
128    let big_u = BigInt::from_bytes_be(Sign::Plus, &u);
129
130    let x = ph2(password.as_ref(), salt1, salt2);
131    let big_x = BigInt::from_bytes_be(Sign::Plus, &x);
132
133    let big_v = big_g.modpow(&big_x, &big_p);
134    let big_kv = (big_k * big_v) % &big_p;
135
136    let big_t = (big_g_b - big_kv).rem_euclid(&big_p);
137
138    let exp = big_a + big_u * big_x;
139    let big_sa = big_t.modpow(&exp, &big_p);
140
141    let k_a = sha256(&[&pad256(&big_sa.to_bytes_be().1)]);
142
143    let h_p = sha256(&[p]);
144    let h_g = sha256(&[&g_hash]);
145    let p_xg = xor32(&h_p, &h_g);
146    let m1 = sha256(&[
147        &p_xg,
148        &sha256(&[salt1]),
149        &sha256(&[salt2]),
150        &g_a,
151        &g_b,
152        &k_a,
153    ]);
154
155    Ok((m1, g_a))
156}