Skip to main content

ferogram_session/
string_session.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
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
16
17use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
18
19const VERSION_V1: u8 = 1;
20const VERSION_V2: u8 = 2;
21
22const AUTH_KEY_LEN: usize = 256;
23
24#[derive(Debug, Clone)]
25pub struct FullSession {
26    pub dc_id: u8,
27    pub ip: IpAddr,
28    pub port: u16,
29    pub auth_key: [u8; AUTH_KEY_LEN],
30    pub user_id: i64,
31    pub server_salt: i64,
32    pub seq_no: u32,
33    pub layer: u32,
34}
35
36#[derive(Debug, Clone)]
37pub struct Session {
38    pub dc_id: u8,
39    pub ip: IpAddr,
40    pub port: u16,
41    pub auth_key: [u8; AUTH_KEY_LEN],
42    pub user_id: i64,
43}
44
45#[derive(Debug, Clone)]
46pub enum StringSession {
47    V1(FullSession),
48    V2(Session),
49}
50
51#[derive(Debug, thiserror::Error)]
52pub enum StringSessionError {
53    #[error("base64 decode error: {0}")]
54    Base64(#[from] base64::DecodeError),
55    #[error("invalid or truncated session data")]
56    InvalidData,
57    #[error("unsupported version: {0}")]
58    UnsupportedVersion(u8),
59    #[error("unknown ip type byte: {0}")]
60    UnknownIpType(u8),
61}
62
63impl StringSession {
64    /// Decode a string session. Auto-detects V1 or V2 from the version byte.
65    pub fn decode(s: &str) -> Result<Self, StringSessionError> {
66        let bytes = URL_SAFE_NO_PAD.decode(s.trim())?;
67
68        if bytes.is_empty() {
69            return Err(StringSessionError::InvalidData);
70        }
71
72        match bytes[0] {
73            VERSION_V1 => decode_v1(&bytes).map(StringSession::V1),
74            VERSION_V2 => decode_v2(&bytes).map(StringSession::V2),
75            v => Err(StringSessionError::UnsupportedVersion(v)),
76        }
77    }
78
79    /// Encode as V2 (minimal). This is the default.
80    pub fn encode(&self) -> String {
81        match self {
82            StringSession::V2(s) => encode_v2(s),
83            StringSession::V1(s) => encode_v2(&Session {
84                dc_id: s.dc_id,
85                ip: s.ip,
86                port: s.port,
87                auth_key: s.auth_key,
88                user_id: s.user_id,
89            }),
90        }
91    }
92
93    /// Encode as V1 (full session with salt, seq_no, layer).
94    /// Use this for manual transfer or when full state is needed.
95    pub fn encode_v1(&self) -> String {
96        match self {
97            StringSession::V1(s) => encode_v1(s),
98            StringSession::V2(_) => {
99                panic!("cannot encode V2 session as V1: missing server_salt, seq_no, layer")
100            }
101        }
102    }
103
104    /// The minimal V2 fields, regardless of which version was decoded. For a
105    /// V1 session this drops `server_salt`/`seq_no`/`layer`; use
106    /// [`Self::full_session`] if you need those.
107    pub fn session(&self) -> Session {
108        match self {
109            StringSession::V2(s) => s.clone(),
110            StringSession::V1(s) => Session {
111                dc_id: s.dc_id,
112                ip: s.ip,
113                port: s.port,
114                auth_key: s.auth_key,
115                user_id: s.user_id,
116            },
117        }
118    }
119
120    /// The full V1 fields, or `None` if this is a V2 session (V2 never
121    /// carried `server_salt`/`seq_no`/`layer` to begin with).
122    pub fn full_session(&self) -> Option<&FullSession> {
123        match self {
124            StringSession::V1(s) => Some(s),
125            StringSession::V2(_) => None,
126        }
127    }
128
129    /// Which version this session was decoded as (1 or 2), independent of
130    /// which version [`Self::encode`] would produce.
131    pub fn version(&self) -> u8 {
132        match self {
133            StringSession::V1(_) => VERSION_V1,
134            StringSession::V2(_) => VERSION_V2,
135        }
136    }
137}
138
139impl From<Session> for StringSession {
140    fn from(s: Session) -> Self {
141        StringSession::V2(s)
142    }
143}
144
145impl From<FullSession> for StringSession {
146    fn from(s: FullSession) -> Self {
147        StringSession::V1(s)
148    }
149}
150
151fn encode_v2(s: &Session) -> String {
152    let ip_bytes = ip_to_bytes(s.ip);
153    let ip_type = ip_type_byte(s.ip);
154
155    let mut buf = Vec::with_capacity(1 + 1 + 1 + ip_bytes.len() + 2 + 8 + AUTH_KEY_LEN);
156    buf.push(VERSION_V2);
157    buf.push(s.dc_id);
158    buf.push(ip_type);
159    buf.extend_from_slice(&ip_bytes);
160    buf.extend_from_slice(&s.port.to_be_bytes());
161    buf.extend_from_slice(&s.user_id.to_be_bytes());
162    buf.extend_from_slice(&s.auth_key);
163
164    URL_SAFE_NO_PAD.encode(&buf)
165}
166
167fn encode_v1(s: &FullSession) -> String {
168    let ip_bytes = ip_to_bytes(s.ip);
169    let ip_type = ip_type_byte(s.ip);
170
171    let mut buf = Vec::with_capacity(1 + 1 + 1 + ip_bytes.len() + 2 + 8 + 8 + 4 + 4 + AUTH_KEY_LEN);
172    buf.push(VERSION_V1);
173    buf.push(s.dc_id);
174    buf.push(ip_type);
175    buf.extend_from_slice(&ip_bytes);
176    buf.extend_from_slice(&s.port.to_be_bytes());
177    buf.extend_from_slice(&s.user_id.to_be_bytes());
178    buf.extend_from_slice(&s.server_salt.to_be_bytes());
179    buf.extend_from_slice(&s.seq_no.to_be_bytes());
180    buf.extend_from_slice(&s.layer.to_be_bytes());
181    buf.extend_from_slice(&s.auth_key);
182
183    URL_SAFE_NO_PAD.encode(&buf)
184}
185
186fn decode_v2(bytes: &[u8]) -> Result<Session, StringSessionError> {
187    let mut c = 1usize;
188
189    let dc_id = read_u8(bytes, &mut c)?;
190    let ip = read_ip(bytes, &mut c)?;
191
192    if bytes.len() < c + 2 + 8 + AUTH_KEY_LEN {
193        return Err(StringSessionError::InvalidData);
194    }
195
196    let port = read_u16_be(bytes, &mut c)?;
197    let user_id = read_i64_be(bytes, &mut c)?;
198    let auth_key = read_auth_key(bytes, &mut c)?;
199
200    Ok(Session {
201        dc_id,
202        ip,
203        port,
204        auth_key,
205        user_id,
206    })
207}
208
209fn decode_v1(bytes: &[u8]) -> Result<FullSession, StringSessionError> {
210    let mut c = 1usize;
211
212    let dc_id = read_u8(bytes, &mut c)?;
213    let ip = read_ip(bytes, &mut c)?;
214
215    if bytes.len() < c + 2 + 8 + 8 + 4 + 4 + AUTH_KEY_LEN {
216        return Err(StringSessionError::InvalidData);
217    }
218
219    let port = read_u16_be(bytes, &mut c)?;
220    let user_id = read_i64_be(bytes, &mut c)?;
221    let server_salt = read_i64_be(bytes, &mut c)?;
222    let seq_no = read_u32_be(bytes, &mut c)?;
223    let layer = read_u32_be(bytes, &mut c)?;
224    let auth_key = read_auth_key(bytes, &mut c)?;
225
226    Ok(FullSession {
227        dc_id,
228        ip,
229        port,
230        auth_key,
231        user_id,
232        server_salt,
233        seq_no,
234        layer,
235    })
236}
237
238fn read_u8(bytes: &[u8], c: &mut usize) -> Result<u8, StringSessionError> {
239    if bytes.len() < *c + 1 {
240        return Err(StringSessionError::InvalidData);
241    }
242    let v = bytes[*c];
243    *c += 1;
244    Ok(v)
245}
246
247fn read_u16_be(bytes: &[u8], c: &mut usize) -> Result<u16, StringSessionError> {
248    let v = u16::from_be_bytes(
249        bytes[*c..*c + 2]
250            .try_into()
251            .map_err(|_| StringSessionError::InvalidData)?,
252    );
253    *c += 2;
254    Ok(v)
255}
256
257fn read_u32_be(bytes: &[u8], c: &mut usize) -> Result<u32, StringSessionError> {
258    let v = u32::from_be_bytes(
259        bytes[*c..*c + 4]
260            .try_into()
261            .map_err(|_| StringSessionError::InvalidData)?,
262    );
263    *c += 4;
264    Ok(v)
265}
266
267fn read_i64_be(bytes: &[u8], c: &mut usize) -> Result<i64, StringSessionError> {
268    let v = i64::from_be_bytes(
269        bytes[*c..*c + 8]
270            .try_into()
271            .map_err(|_| StringSessionError::InvalidData)?,
272    );
273    *c += 8;
274    Ok(v)
275}
276
277fn read_auth_key(bytes: &[u8], c: &mut usize) -> Result<[u8; AUTH_KEY_LEN], StringSessionError> {
278    let key: [u8; AUTH_KEY_LEN] = bytes[*c..*c + AUTH_KEY_LEN]
279        .try_into()
280        .map_err(|_| StringSessionError::InvalidData)?;
281    *c += AUTH_KEY_LEN;
282    Ok(key)
283}
284
285fn read_ip(bytes: &[u8], c: &mut usize) -> Result<IpAddr, StringSessionError> {
286    let ip_type = read_u8(bytes, c)?;
287    match ip_type {
288        4 => {
289            if bytes.len() < *c + 4 {
290                return Err(StringSessionError::InvalidData);
291            }
292            let octets: [u8; 4] = bytes[*c..*c + 4]
293                .try_into()
294                .map_err(|_| StringSessionError::InvalidData)?;
295            *c += 4;
296            Ok(IpAddr::V4(Ipv4Addr::from(octets)))
297        }
298        6 => {
299            if bytes.len() < *c + 16 {
300                return Err(StringSessionError::InvalidData);
301            }
302            let octets: [u8; 16] = bytes[*c..*c + 16]
303                .try_into()
304                .map_err(|_| StringSessionError::InvalidData)?;
305            *c += 16;
306            Ok(IpAddr::V6(Ipv6Addr::from(octets)))
307        }
308        other => Err(StringSessionError::UnknownIpType(other)),
309    }
310}
311
312fn ip_to_bytes(ip: IpAddr) -> Vec<u8> {
313    match ip {
314        IpAddr::V4(v4) => v4.octets().to_vec(),
315        IpAddr::V6(v6) => v6.octets().to_vec(),
316    }
317}
318
319fn ip_type_byte(ip: IpAddr) -> u8 {
320    match ip {
321        IpAddr::V4(_) => 4,
322        IpAddr::V6(_) => 6,
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn dummy_key() -> [u8; AUTH_KEY_LEN] {
331        let mut k = [0u8; AUTH_KEY_LEN];
332        for (i, b) in k.iter_mut().enumerate() {
333            *b = i as u8;
334        }
335        k
336    }
337
338    fn ipv4() -> IpAddr {
339        IpAddr::V4(Ipv4Addr::new(149, 154, 167, 51))
340    }
341
342    fn ipv6() -> IpAddr {
343        IpAddr::V6(Ipv6Addr::new(0x2001, 0xb28, 0xf23d, 0, 0, 0, 0, 0xa))
344    }
345
346    #[test]
347    fn v2_roundtrip_ipv4() {
348        let s = StringSession::V2(Session {
349            dc_id: 2,
350            ip: ipv4(),
351            port: 443,
352            auth_key: dummy_key(),
353            user_id: 123456789,
354        });
355
356        let encoded = s.encode();
357        let decoded = StringSession::decode(&encoded).unwrap();
358
359        assert_eq!(decoded.version(), 2);
360        let d = decoded.session();
361        assert_eq!(d.dc_id, 2);
362        assert_eq!(d.ip, ipv4());
363        assert_eq!(d.port, 443);
364        assert_eq!(d.user_id, 123456789);
365        assert_eq!(d.auth_key, dummy_key());
366    }
367
368    #[test]
369    fn v2_roundtrip_ipv6() {
370        let s = StringSession::V2(Session {
371            dc_id: 4,
372            ip: ipv6(),
373            port: 443,
374            auth_key: dummy_key(),
375            user_id: -987654321,
376        });
377
378        let encoded = s.encode();
379        let decoded = StringSession::decode(&encoded).unwrap();
380
381        assert_eq!(decoded.version(), 2);
382        let d = decoded.session();
383        assert_eq!(d.ip, ipv6());
384        assert_eq!(d.user_id, -987654321);
385    }
386
387    #[test]
388    fn v1_roundtrip_ipv4() {
389        let s = StringSession::V1(FullSession {
390            dc_id: 1,
391            ip: ipv4(),
392            port: 443,
393            auth_key: dummy_key(),
394            user_id: 111,
395            server_salt: -999,
396            seq_no: 42,
397            layer: 166,
398        });
399
400        let encoded = s.encode_v1();
401        let decoded = StringSession::decode(&encoded).unwrap();
402
403        assert_eq!(decoded.version(), 1);
404        let f = decoded.full_session().unwrap();
405        assert_eq!(f.dc_id, 1);
406        assert_eq!(f.ip, ipv4());
407        assert_eq!(f.port, 443);
408        assert_eq!(f.user_id, 111);
409        assert_eq!(f.server_salt, -999);
410        assert_eq!(f.seq_no, 42);
411        assert_eq!(f.layer, 166);
412        assert_eq!(f.auth_key, dummy_key());
413    }
414
415    #[test]
416    fn v1_roundtrip_ipv6() {
417        let s = StringSession::V1(FullSession {
418            dc_id: 5,
419            ip: ipv6(),
420            port: 443,
421            auth_key: dummy_key(),
422            user_id: 777,
423            server_salt: 12345,
424            seq_no: 10,
425            layer: 166,
426        });
427
428        let encoded = s.encode_v1();
429        let decoded = StringSession::decode(&encoded).unwrap();
430
431        assert_eq!(decoded.version(), 1);
432        let f = decoded.full_session().unwrap();
433        assert_eq!(f.ip, ipv6());
434        assert_eq!(f.layer, 166);
435    }
436
437    #[test]
438    fn v1_encode_produces_v2_when_called_via_encode() {
439        let s = StringSession::V1(FullSession {
440            dc_id: 2,
441            ip: ipv4(),
442            port: 443,
443            auth_key: dummy_key(),
444            user_id: 555,
445            server_salt: 0,
446            seq_no: 0,
447            layer: 166,
448        });
449
450        let encoded = s.encode();
451        let decoded = StringSession::decode(&encoded).unwrap();
452        assert_eq!(decoded.version(), 2);
453    }
454
455    #[test]
456    fn v2_encoded_length_ipv4() {
457        let s = StringSession::V2(Session {
458            dc_id: 1,
459            ip: ipv4(),
460            port: 443,
461            auth_key: dummy_key(),
462            user_id: 1,
463        });
464        assert_eq!(s.encode().len(), 364);
465    }
466
467    #[test]
468    fn rejects_truncated() {
469        assert!(StringSession::decode("Ag").is_err());
470    }
471
472    #[test]
473    fn rejects_unsupported_version() {
474        let bad = URL_SAFE_NO_PAD.encode(&[99u8]);
475        assert!(matches!(
476            StringSession::decode(&bad),
477            Err(StringSessionError::UnsupportedVersion(99))
478        ));
479    }
480
481    #[test]
482    fn full_session_returns_none_for_v2() {
483        let s = StringSession::V2(Session {
484            dc_id: 1,
485            ip: ipv4(),
486            port: 443,
487            auth_key: dummy_key(),
488            user_id: 1,
489        });
490        assert!(s.full_session().is_none());
491    }
492}