use anyhow::Error; use futures_util::StreamExt; use giterated_models::authenticated::UserAuthenticationToken; use giterated_models::instance::{AuthenticationTokenRequest, Instance, RegisterAccountRequest}; use giterated_models::repository::{Repository, RepositorySummary}; use giterated_models::user::User; use giterated_stack::AuthenticatedUser; use std::sync::Arc; use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, rand_core::OsRng, RsaPrivateKey, RsaPublicKey, }; use secrecy::ExposeSecret; use sqlx::{Either, PgPool}; use tokio::sync::Mutex; use crate::authentication::AuthenticationTokenGranter; use crate::backend::git::GitRepository; use super::{AuthBackend, MetadataBackend, UserBackend}; pub struct UserAuth { pub pg_pool: PgPool, pub this_instance: Instance, pub auth_granter: Arc>, pub settings_provider: Arc>, } impl UserAuth { pub fn new( pool: PgPool, this_instance: &Instance, granter: Arc>, settings_provider: Arc>, ) -> Self { Self { pg_pool: pool, this_instance: this_instance.clone(), auth_granter: granter, settings_provider, } } } #[async_trait::async_trait] impl UserBackend for UserAuth { async fn exists(&mut self, user: &User) -> Result { Ok(sqlx::query_as!( UserRow, r#"SELECT * FROM users WHERE username = $1"#, user.username ) .fetch_one(&self.pg_pool.clone()) .await .is_ok()) } async fn repositories_for_user( &mut self, _requester: &Option, user: &User, ) -> Result, Error> { let mut repositories = sqlx::query_as!( GitRepository, r#"SELECT owner_user, name, description, visibility as "visibility: _", default_branch FROM repositories WHERE owner_user = $1"#, user.to_string() ) .fetch_many(&self.pg_pool); let mut return_repositories = vec![]; while let Some(Ok(Either::Right(repository_row))) = repositories.next().await { return_repositories.push(RepositorySummary { repository: Repository { owner: repository_row.owner_user.clone(), name: repository_row.name, instance: self.this_instance.clone(), }, owner: repository_row.owner_user, visibility: repository_row.visibility, description: repository_row.description, last_commit: None, }) } Ok(return_repositories) } } #[async_trait::async_trait] impl AuthBackend for UserAuth { async fn register( &mut self, request: RegisterAccountRequest, ) -> Result { const BITS: usize = 2048; let private_key = RsaPrivateKey::new(&mut OsRng, BITS).unwrap(); let public_key = RsaPublicKey::from(&private_key); let key = { let mut target: [u8; 32] = [0; 32]; let mut index = 0; let mut iterator = request.password.expose_secret().0.as_bytes().iter(); while index < 32 { if let Some(next) = iterator.next() { target[index] = *next; index += 1; } else { iterator = request.password.expose_secret().0.as_bytes().iter(); } } target }; let key: &Key = &key.into(); let cipher = Aes256Gcm::new(key); let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, private_key.to_pkcs8_der().unwrap().as_bytes()) .unwrap(); let private_key_enc = format!("{}#{}", STANDARD.encode(nonce), STANDARD.encode(ciphertext)); let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let password_hash = argon2 .hash_password(request.password.expose_secret().0.as_bytes(), &salt) .unwrap() .to_string(); let user = match sqlx::query_as!( UserRow, r#"INSERT INTO users VALUES ($1, $2, $3, $4, $5) returning *"#, request.username, "example.com", password_hash, public_key .to_public_key_pem(rsa::pkcs8::LineEnding::LF) .unwrap(), private_key_enc ) .fetch_one(&self.pg_pool) .await { Ok(user) => user, Err(err) => { error!("Failed inserting into the database! {:?}", err); return Err(err.into()); } }; let mut granter = self.auth_granter.lock().await; let token = granter .create_token_for( &User { username: user.username, instance: self.this_instance.clone(), }, &self.this_instance, ) .await; Ok(UserAuthenticationToken::from(token)) } async fn login( &mut self, source: &Instance, request: AuthenticationTokenRequest, ) -> Result { let user = sqlx::query_as!( UserRow, r#"SELECT * FROM users WHERE username = $1"#, request.username ) .fetch_one(&self.pg_pool) .await?; let hash = PasswordHash::new(&user.password).unwrap(); if Argon2::default() .verify_password(request.password.expose_secret().0.as_bytes(), &hash) .is_err() { return Err(Error::from(AuthenticationError::InvalidPassword)); } let mut granter = self.auth_granter.lock().await; let token = granter .create_token_for( &User { username: user.username, instance: self.this_instance.clone(), }, source, ) .await; Ok(UserAuthenticationToken::from(token)) } } #[allow(unused)] #[derive(Debug, sqlx::FromRow)] struct UserRow { pub username: String, pub email: Option, pub password: String, pub public_key: String, pub enc_private_key: Vec, } #[allow(unused)] #[derive(Debug, sqlx::FromRow)] struct UserValue { pub username: String, pub name: String, pub value: String, } #[derive(Debug, thiserror::Error)] pub enum AuthenticationError { #[error("invalid password")] InvalidPassword, }