cbwaw/
client.rs

1use bytes::{BufMut, Bytes, BytesMut};
2use chacha20poly1305::{
3    XChaCha20Poly1305, XNonce,
4    aead::{Aead, KeyInit},
5};
6use qrcodegen::QrCode;
7use x25519_dalek::{PublicKey, StaticSecret};
8
9pub use rand::rngs::OsRng;
10
11use totp_rs::{Algorithm, Secret, TOTP};
12
13pub struct AuthClient {}
14
15impl AuthClient {
16    /// account_len.account.client_start
17    /// (client_state, payload)
18    pub fn registration_start_req(
19        account: &[u8],
20        password: &[u8],
21    ) -> Result<(Vec<u8>, Bytes), Box<dyn std::error::Error>> {
22        let (client_state, client_start) = crate::registration::client_start(password)?;
23
24        let account_len = account.len();
25        if account_len > crate::MAX_ACCOUNT_LEN as usize {
26            return Err("account is too long".into());
27        }
28
29        let mut buf = BytesMut::with_capacity(1 + account_len + client_start.len());
30
31        buf.put_u8(account_len as u8);
32        buf.put(&account[..]);
33        buf.put(&client_start[..]);
34
35        Ok((client_state, buf.into()))
36    }
37
38    /// account_len.account.client_finish
39    /// payload
40    pub fn registration_finish_req(
41        account: &[u8],
42        password: &[u8],
43        client_state: &[u8],
44        server_message: &[u8],
45    ) -> Result<([u8; 32], Bytes), Box<dyn std::error::Error>> {
46        let client_finish =
47            crate::registration::client_finish(password, client_state, server_message)?;
48
49        let account_len = account.len();
50        if account_len > crate::MAX_ACCOUNT_LEN as usize {
51            return Err("account is too long".into());
52        }
53
54        let static_secret = StaticSecret::random_from_rng(OsRng);
55        let public_key = PublicKey::from(&static_secret);
56
57        let private_key = *static_secret.as_bytes();
58
59        let mut buf = BytesMut::with_capacity(1 + account_len + client_finish.len());
60
61        buf.put_u8(account_len as u8);
62        buf.put(&account[..]);
63        buf.put(&public_key.as_bytes()[..]);
64        buf.put(&client_finish[..]);
65
66        Ok((private_key, buf.into()))
67    }
68
69    pub fn decrypt_totp_mfa(
70        response: Bytes,
71        private_key: [u8; 32],
72        app_name: String,
73        account: String,
74    ) -> Result<TOTP, Box<dyn std::error::Error>> {
75        let static_secret = StaticSecret::from(private_key);
76        let public_key_bytes: [u8; 32] = response[60..92].try_into()?;
77        let public_key = PublicKey::from(public_key_bytes);
78
79        let shared_secret = static_secret.diffie_hellman(&public_key);
80
81        let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
82
83        let nonce = XNonce::from_slice(&response[36..60]);
84        let secret = match cipher.decrypt(nonce, &response[0..36]) {
85            Ok(secret) => secret,
86            Err(err) => return Err(err.to_string().into()),
87        };
88
89        let totp = TOTP::new(
90            Algorithm::SHA1,
91            6,
92            1,
93            30,
94            Secret::Raw(secret.clone()).to_bytes()?,
95            Some(app_name),
96            account,
97        )?;
98
99        Ok(totp)
100    }
101
102    pub fn decrypt_totp_mfa_code(
103        response: Bytes,
104        private_key: [u8; 32],
105        app_name: String,
106        account: String,
107    ) -> Result<String, Box<dyn std::error::Error>> {
108        let totp = Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
109        let code = totp.generate_current()?;
110        Ok(code)
111    }
112
113    pub fn decrypt_totp_mfa_to_qr_svg(
114        response: Bytes,
115        private_key: [u8; 32],
116        app_name: String,
117        account: String,
118    ) -> Result<String, Box<dyn std::error::Error>> {
119        let totp = Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
120        let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
121
122        let svg = to_svg_string(&qr, 4);
123        Ok(svg)
124    }
125
126    /// account_len.account.client_start
127    /// (client_state, payload)
128    pub fn login_start_req(
129        account: &[u8],
130        password: &[u8],
131    ) -> Result<(Vec<u8>, Bytes), Box<dyn std::error::Error>> {
132        let (client_state, client_start) = crate::login::client_start(password)?;
133
134        let account_len = account.len();
135        if account_len > crate::MAX_ACCOUNT_LEN as usize {
136            return Err("account is too long".into());
137        }
138
139        let mut buf = BytesMut::with_capacity(1 + account_len + client_start.len());
140
141        buf.put_u8(account_len as u8);
142        buf.put(&account[..]);
143        buf.put(&client_start[..]);
144
145        Ok((client_state, buf.into()))
146    }
147
148    /// account_len.account.client_finish
149    /// payload
150    pub fn login_finish_req(
151        account: &[u8],
152        password: &[u8],
153        mfa_code: String,
154        client_state: &[u8],
155        server_message: &[u8],
156    ) -> Result<(Bytes, Vec<u8>), Box<dyn std::error::Error>> {
157        let (client_finish, session_key) =
158            crate::login::client_finish(password, client_state, server_message)?;
159
160        let account_len = account.len();
161        if account_len > crate::MAX_ACCOUNT_LEN as usize {
162            return Err("account is too long".into());
163        }
164
165        let mut buf = BytesMut::with_capacity(1 + account_len + client_finish.len());
166
167        buf.put_u8(account_len as u8);
168        buf.put(account);
169        buf.put(mfa_code.as_bytes());
170        buf.put(&client_finish[..]);
171
172        Ok((buf.into(), session_key))
173    }
174
175    pub fn decrypt_token(
176        response: Bytes,
177        session_key: &[u8],
178    ) -> Result<Bytes, Box<dyn std::error::Error>> {
179        crate::login::decrypt_token(&response, session_key)
180    }
181
182    /// refresh_token
183    pub fn access_get_req(refresh_token: &[u8]) -> Bytes {
184        // ?? might be worth having some kind of hash ratcheting
185        // ?? session key, to add a layer of obfuscation. short
186        // ?? TTLs and TLS should be fine for now.
187        Bytes::copy_from_slice(refresh_token)
188    }
189}
190
191// copied from qrcodegen examples https://github.com/nayuki/QR-Code-generator/blob/2c9044de6b049ca25cb3cd1649ed7e27aa055138/rust/examples/qrcodegen-demo.rs#L169
192fn to_svg_string(qr: &QrCode, border: i32) -> String {
193    assert!(border >= 0, "Border must be non-negative");
194    let mut result = String::new();
195    // result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
196    // result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
197    let dimension = qr
198        .size()
199        .checked_add(border.checked_mul(2).unwrap())
200        .unwrap();
201    result += &format!(
202        "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n",
203        dimension
204    );
205    result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
206    result += "\t<path d=\"";
207    for y in 0..qr.size() {
208        for x in 0..qr.size() {
209            if qr.get_module(x, y) {
210                if x != 0 || y != 0 {
211                    result += " ";
212                }
213                result += &format!("M{},{}h1v1h-1z", x + border, y + border);
214            }
215        }
216    }
217    result += "\" fill=\"#000000\"/>\n";
218    result += "</svg>\n";
219    result
220}