use blake2::{
Blake2bVar,
digest::{Update, VariableOutput},
};
use bytes::{BufMut, Bytes, BytesMut};
use chacha20poly1305::{
XChaCha20Poly1305, XNonce,
aead::{Aead, AeadCore, KeyInit, OsRng},
};
use qrcodegen::QrCode;
use x25519_dalek::{PublicKey, StaticSecret};
use totp_rs::{Algorithm, Secret, TOTP};
use std::error::Error;
use std::fmt::Write;
pub struct AuthClient {}
impl AuthClient {
pub fn registration_start_req(
account: &[u8],
password: &[u8],
invite_token: Option<Bytes>,
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
let (client_state, client_start) = crate::registration::client_start(password)?;
let account_len = account.len();
if account_len > crate::MAX_ACCOUNT_LEN as usize {
return Err("account is too long".into());
}
let mut buf = BytesMut::with_capacity(1 + account_len + 32);
buf.put_u8(u8::try_from(account_len)?);
buf.put(account);
buf.put(&client_start[..]);
if let Some(token) = invite_token {
buf.put(token);
}
Ok((client_state, buf.into()))
}
pub fn registration_finish_req(
account: &[u8],
password: &[u8],
client_state: &[u8],
server_message: &[u8],
) -> Result<([u8; 32], Bytes), Box<dyn Error>> {
let client_finish =
crate::registration::client_finish(password, client_state, server_message)?;
let account_len = account.len();
if account_len > crate::MAX_ACCOUNT_LEN as usize {
return Err("account is too long".into());
}
let static_secret = StaticSecret::random_from_rng(OsRng);
let public_key = PublicKey::from(&static_secret);
let private_key = *static_secret.as_bytes();
let mut buf = BytesMut::with_capacity(1 + account_len + client_finish.len());
buf.put_u8(u8::try_from(account_len)?);
buf.put(account);
buf.put(&public_key.as_bytes()[..]);
buf.put(&client_finish[..]);
Ok((private_key, buf.into()))
}
pub fn decrypt_totp_mfa(
response: &Bytes,
private_key: [u8; 32],
app_name: String,
account: String,
) -> Result<(TOTP, String), Box<dyn Error>> {
let static_secret = StaticSecret::from(private_key);
let public_key_bytes: [u8; 32] = response[115..147].try_into()?;
let public_key = PublicKey::from(public_key_bytes);
let shared_secret = static_secret.diffie_hellman(&public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from_slice(&response[91..115]);
let (secret, codes) = match cipher.decrypt(nonce, &response[0..91]) {
Ok(secret_and_codes) => (
secret_and_codes[0..20].to_vec(),
std::str::from_utf8(&secret_and_codes[20..])?.to_string(),
),
Err(err) => return Err(err.to_string().into()),
};
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
Secret::Raw(secret).to_bytes()?,
Some(app_name),
account,
)?;
Ok((totp, codes))
}
pub fn decrypt_totp_mfa_code(
response: &Bytes,
private_key: [u8; 32],
app_name: String,
account: String,
) -> Result<(String, String), Box<dyn Error>> {
let (totp, recovery_codes) =
Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
let mfa_code = totp.generate_current()?;
Ok((mfa_code, recovery_codes))
}
pub fn decrypt_totp_mfa_to_qr_svg(
response: &Bytes,
private_key: [u8; 32],
app_name: String,
account: String,
) -> Result<(String, String), Box<dyn Error>> {
let (totp, recovery_codes) =
Self::decrypt_totp_mfa(response, private_key, app_name, account)?;
let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
let svg = to_svg_string(&qr, 4)?;
Ok((svg, recovery_codes))
}
pub fn login_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
let (client_state, client_start) = crate::login::client_start(password)?;
let account_len = account.len();
if account_len > crate::MAX_ACCOUNT_LEN as usize {
return Err("account is too long".into());
}
let mut buf = BytesMut::with_capacity(1 + account_len + client_start.len());
buf.put_u8(u8::try_from(account_len)?);
buf.put(account);
buf.put(&client_start[..]);
Ok((client_state, buf.into()))
}
pub fn login_finish_req(
account: &[u8],
password: &[u8],
mfa_hash: &[u8],
client_state: &[u8],
server_message: &[u8],
client_verifier: Option<&[u8]>,
) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
let (client_finish, session_key) =
crate::login::client_finish(password, client_state, server_message)?;
let account_len = account.len();
if account_len > crate::MAX_ACCOUNT_LEN as usize {
return Err("account is too long".into());
}
let mut buf = BytesMut::with_capacity(1 + account_len + 32 + 32 + 16 + 24 + 64);
buf.put_u8(u8::try_from(account_len)?);
buf.put(account);
let mut key = [0u8; 32];
let mut hasher = match Blake2bVar::new(32) {
Ok(h) => h,
Err(err) => return Err(err.to_string().into()),
};
hasher.update(&session_key[..]);
if let Err(err) = hasher.finalize_variable(&mut key) {
return Err(err.to_string().into());
}
let cipher = XChaCha20Poly1305::new(&key.into());
let mut mfa_hash_verifier_and_verifier_pub_key = BytesMut::with_capacity(64);
mfa_hash_verifier_and_verifier_pub_key.put(mfa_hash);
if let Some(client_verifier) = client_verifier {
mfa_hash_verifier_and_verifier_pub_key.put(client_verifier);
}
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let mut encrypted_mfa_hash =
match cipher.encrypt(&nonce, &mfa_hash_verifier_and_verifier_pub_key[..]) {
Ok(e) => e,
Err(err) => return Err(err.to_string().into()),
};
encrypted_mfa_hash.extend_from_slice(&nonce);
buf.put(&encrypted_mfa_hash[..]);
buf.put(&client_finish[..]);
Ok((buf.into(), session_key))
}
pub fn decrypt_token(response: &Bytes, session_key: &[u8]) -> Result<Bytes, Box<dyn Error>> {
crate::login::decrypt_token(response, session_key)
}
pub fn reset_password_login_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
Self::login_start_req(account, password)
}
pub fn reset_password_login_finish_req(
account: &[u8],
password: &[u8],
mfa_hash: &[u8],
client_state: &[u8],
server_message: &[u8],
client_verifier: Option<&[u8]>,
) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
Self::login_finish_req(
account,
password,
mfa_hash,
client_state,
server_message,
client_verifier,
)
}
pub fn password_reset_registration_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
Self::registration_start_req(account, password, None)
}
pub fn password_reset_registration_finish_req(
account: &[u8],
password: &[u8],
client_state: &[u8],
server_message: &[u8],
) -> Result<Bytes, Box<dyn Error>> {
let (_, req) =
Self::registration_finish_req(account, password, client_state, server_message)?;
Ok(req)
}
pub fn forgot_password_start_req(
account: &[u8],
new_password: &[u8],
recovery_code: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
let (state, payload) = Self::registration_start_req(account, new_password, None)?;
let mut payload: BytesMut = payload.into();
payload.extend_from_slice(recovery_code);
Ok((state, payload.into()))
}
pub fn forgot_password_finish_req(
account: &[u8],
new_password: &[u8],
client_state: &[u8],
server_message: &[u8],
recovery_code: &[u8],
) -> Result<Bytes, Box<dyn Error>> {
let (_, payload) =
Self::registration_finish_req(account, new_password, client_state, server_message)?;
let mut payload: BytesMut = payload.into();
payload.extend_from_slice(recovery_code);
Ok(payload.into())
}
pub fn reset_totp_mfa_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
Self::login_start_req(account, password)
}
pub fn reset_totp_mfa_finish_req(
account: &[u8],
password: &[u8],
mfa_hash: &[u8],
client_state: &[u8],
server_message: &[u8],
) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
Self::login_finish_req(
account,
password,
mfa_hash,
client_state,
server_message,
None,
)
}
pub fn decrypt_reset_totp_mfa(
response: &Bytes,
session_key: &[u8],
) -> Result<Bytes, Box<dyn Error>> {
crate::login::decrypt_token(response, session_key)
}
pub fn decrypt_reset_totp_mfa_code(
response: &Bytes,
session_key: &[u8],
app_name: String,
account: String,
) -> Result<String, Box<dyn Error>> {
let secret = Self::decrypt_reset_totp_mfa(response, session_key)?;
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
Secret::Raw(secret.to_vec()).to_bytes()?,
Some(app_name),
account,
)?;
let mfa_code = totp.generate_current()?;
Ok(mfa_code)
}
pub fn decrypt_reset_totp_mfa_to_qr_svg(
response: &Bytes,
session_key: &[u8],
app_name: String,
account: String,
) -> Result<String, Box<dyn Error>> {
let secret = Self::decrypt_reset_totp_mfa(response, session_key)?;
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
Secret::Raw(secret.to_vec()).to_bytes()?,
Some(app_name),
account,
)?;
let qr: QrCode = QrCode::encode_text(&totp.get_url(), qrcodegen::QrCodeEcc::Medium)?;
let svg = to_svg_string(&qr, 4)?;
Ok(svg)
}
pub fn lost_totp_mfa_start_req(
account: &[u8],
password: &[u8],
recovery_code: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
let (state, payload) = Self::login_start_req(account, password)?;
let mut payload: BytesMut = payload.into();
payload.extend_from_slice(recovery_code);
Ok((state, payload.into()))
}
pub fn lost_totp_mfa_finish_req(
account: &[u8],
password: &[u8],
client_state: &[u8],
server_message: &[u8],
recovery_code: &[u8],
) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
let (payload, session_key) = Self::login_finish_req(
account,
password,
&[0u8; 32][..],
client_state,
server_message,
None,
)?;
let mut payload: BytesMut = payload.into();
payload.extend_from_slice(recovery_code);
Ok((payload.into(), session_key))
}
pub fn decrypt_lost_totp_mfa(
response: &Bytes,
session_key: &[u8],
) -> Result<Bytes, Box<dyn Error>> {
Self::decrypt_reset_totp_mfa(response, session_key)
}
pub fn decrypt_lost_totp_mfa_code(
response: &Bytes,
session_key: &[u8],
app_name: String,
account: String,
) -> Result<String, Box<dyn Error>> {
Self::decrypt_reset_totp_mfa_code(response, session_key, app_name, account)
}
pub fn decrypt_lost_totp_mfa_to_qr_svg(
response: &Bytes,
session_key: &[u8],
app_name: String,
account: String,
) -> Result<String, Box<dyn Error>> {
Self::decrypt_reset_totp_mfa_to_qr_svg(response, session_key, app_name, account)
}
pub fn reset_recovery_codes_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
Self::login_start_req(account, password)
}
pub fn reset_recovery_codes_finish_req(
account: &[u8],
password: &[u8],
mfa_hash: &[u8],
client_state: &[u8],
server_message: &[u8],
) -> Result<(Bytes, Vec<u8>), Box<dyn Error>> {
Self::login_finish_req(
account,
password,
mfa_hash,
client_state,
server_message,
None,
)
}
pub fn decrypt_reset_recovery_codes(
response: &Bytes,
session_key: &[u8],
) -> Result<String, Box<dyn Error>> {
let codes = crate::login::decrypt_token(response, session_key)?;
let codes_str = std::str::from_utf8(&codes)?.to_string();
Ok(codes_str)
}
#[must_use]
pub fn access_get_req(refresh_token: &[u8]) -> Bytes {
Bytes::copy_from_slice(refresh_token)
}
pub fn delete_account_start_req(
account: &[u8],
password: &[u8],
) -> Result<(Vec<u8>, Bytes), Box<dyn Error>> {
Self::login_start_req(account, password)
}
pub fn delete_account_finish_req(
account: &[u8],
password: &[u8],
mfa_hash: &[u8],
client_state: &[u8],
server_message: &[u8],
) -> Result<Bytes, Box<dyn Error>> {
let (req, _) = Self::login_finish_req(
account,
password,
mfa_hash,
client_state,
server_message,
None,
)?;
Ok(req)
}
}
fn to_svg_string(qr: &QrCode, border: u8) -> Result<String, Box<dyn Error>> {
let border: i32 = border.into();
let mut result = String::new();
if let Some(border) = border.checked_mul(2)
&& let Some(dimension) = qr.size().checked_add(border)
{
writeln!(
result,
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">"
)?;
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
result += "\t<path d=\"";
for y in 0..qr.size() {
for x in 0..qr.size() {
if qr.get_module(x, y) {
if x != 0 || y != 0 {
result += " ";
}
write!(result, "M{},{}h1v1h-1z", x + border, y + border)?;
}
}
}
result += "\" fill=\"#000000\"/>\n";
result += "</svg>\n";
}
Ok(result)
}