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 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 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 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 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 pub fn access_get_req(refresh_token: &[u8]) -> Bytes {
184 Bytes::copy_from_slice(refresh_token)
188 }
189}
190
191fn to_svg_string(qr: &QrCode, border: i32) -> String {
193 assert!(border >= 0, "Border must be non-negative");
194 let mut result = String::new();
195 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}