random progress
parent
eac80b68a2
commit
cac70ae717
File diff suppressed because it is too large
Load Diff
44
Cargo.toml
44
Cargo.toml
|
@ -7,7 +7,49 @@ license = "EUPL-1.2"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["sso", "oidc", "oauth", "oauth2"]
|
keywords = ["sso", "oidc", "oauth", "oauth2"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.64"
|
rust-version = "1.65"
|
||||||
exclude = ["doc/", "example/"]
|
exclude = ["doc/", "example/"]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.66"
|
||||||
|
argon2 = { version = "0.4.1", features = ["std"] }
|
||||||
|
axum = { version = "0.6.1", features = ["http2"] }
|
||||||
|
axum-extra = { version = "0.4.2", features = ["cookie"] }
|
||||||
|
base16ct = "0.1.1"
|
||||||
|
base64ct = "1.5.3"
|
||||||
|
blake2 = "0.10.5"
|
||||||
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
|
clap = { version = "4.0.29", features = ["derive"] }
|
||||||
|
password-hash = "0.4.2"
|
||||||
|
rand = { version = "0.8.5", default-features = false, features = ["getrandom"] }
|
||||||
|
rsa = "0.7.2"
|
||||||
|
rusqlite = { version = "0.28.0", features = ["serde_json", "time", "uuid"] }
|
||||||
|
rustls = "0.20.7"
|
||||||
|
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.148", features = ["derive"] }
|
||||||
|
serde_json = "1.0.89"
|
||||||
|
sha2 = { version = "0.10.6", features = ["oid"] }
|
||||||
|
subtle = "2.4.1"
|
||||||
|
tokio = { version = "1.22.0", features = ["rt-multi-thread", "fs", "io-util", "io-std", "macros", "net", "signal", "sync", "time"] }
|
||||||
|
toml = "0.5.9"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = { version = "0.3.16", features = ["json"] }
|
||||||
|
uuid = { version = "1.2.2", features = ["v4"] }
|
||||||
|
zeroize = { version = "1.5.7", features = ["std", "derive"] }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
incremental = false
|
||||||
|
lto = "fat"
|
||||||
|
panic = "abort"
|
||||||
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[profile.smaller]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = "s"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
bundled = ["rusqlite/bundled"]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# ChibiAuth
|
# ChibiAuth
|
||||||
|
|
||||||
Naive OpenID Provider.
|
Naive IAM server.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
[server]
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 9909
|
||||||
|
database = "example/database.db"
|
||||||
|
base-url = "https://example.com"
|
||||||
|
|
||||||
|
[server.tls]
|
||||||
|
offload = false
|
||||||
|
certificate = "example/cert.pem"
|
||||||
|
private-key = "example/priv.pem"
|
||||||
|
|
||||||
|
[server.mtls]
|
||||||
|
enabled = false
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* API Tokens */
|
||||||
|
CREATE TABLE IF NOT EXISTS api_token
|
||||||
|
( selector TEXT PRIMARY KEY
|
||||||
|
, verifier BLOB NOT NULL
|
||||||
|
, expiration INTEGER NOT NULL /* TAI64N */
|
||||||
|
, description TEXT
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
/* General User Management */
|
||||||
|
CREATE TABLE IF NOT EXISTS users
|
||||||
|
( uuid BLOB PRIMARY KEY
|
||||||
|
, username TEXT NOT NULL
|
||||||
|
, passhash TEXT NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pass_reset
|
||||||
|
( selector TEXT PRIMARY KEY
|
||||||
|
, verifier BLOB NOT NULL
|
||||||
|
, expiration INTEGER NOT NULL
|
||||||
|
, uuid BLOB NOT NULL REFERENCES users ON DELETE CASCADE
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
/* OAuth & OIDC */
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_rs256
|
||||||
|
( kid TEXT UNIQUE NOT NULL
|
||||||
|
, keyblob BLOB NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_hs256
|
||||||
|
( kid TEXT UNIQUE NOT NULL
|
||||||
|
, keyblob BLOB NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_generation
|
||||||
|
( tokentype TEXT NOT NULL
|
||||||
|
, not_before INTEGER NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_clients
|
||||||
|
( uuid BLOB PRIMARY KEY
|
||||||
|
, name TEXT NOT NULL
|
||||||
|
, subdomains TEXT NOT NULL
|
||||||
|
, kid TEXT NOT NULL
|
||||||
|
, is_secret INTEGER NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_session_tokens
|
||||||
|
( tokenhash BLOB PRIMARY KEY
|
||||||
|
, uuid BLOB REFERENCES users ON DELETE CASCADE,
|
||||||
|
, generation INTEGER NOT NULL
|
||||||
|
, expiration INTEGER NOT NULL
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS oidc_refresh_tokens
|
||||||
|
( tokenhash BLOB PRIMARY KEY
|
||||||
|
, uuid BLOB REFERENCES users ON DELETE CASCADE
|
||||||
|
, generation INTEGER NOT NULL
|
||||||
|
, expiration INTEGER NOT NULL
|
||||||
|
) STRICT;
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod oidc;
|
||||||
|
mod api;
|
|
@ -0,0 +1,25 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use blake2::{Blake2b512, Blake2s256, Digest};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use secrecy::{ExposeSecret, Secret, SecretString};
|
||||||
|
|
||||||
|
pub struct ApiKey {
|
||||||
|
selector: String,
|
||||||
|
verifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiKey {
|
||||||
|
pub fn new() -> Result<SecretString, ApiKey> {
|
||||||
|
let mut raw_key = [0u8; 32];
|
||||||
|
|
||||||
|
OsRng.try_fill_bytes(&mut key)?;
|
||||||
|
|
||||||
|
let key = base16ct::upper::encode_string(raw_key);
|
||||||
|
|
||||||
|
let mut h = Blake2b512::new();
|
||||||
|
|
||||||
|
h.update(b"");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
//! OIDC
|
||||||
|
//!
|
||||||
|
//! # JWK
|
||||||
|
//! Only `HS256` and `RS256` are supported.
|
||||||
|
|
||||||
|
mod hs256;
|
||||||
|
mod rs256;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use base64ct::{Base64Url, Encoding};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
|
||||||
|
pub use hs256::Hs256;
|
||||||
|
pub use rs256::Rs256;
|
||||||
|
|
||||||
|
pub struct OidcAuthnRequest {
|
||||||
|
pub user: String,
|
||||||
|
pub pass: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OidcDiscovery {
|
||||||
|
pub jwks: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OidcKeyEngine {
|
||||||
|
discovery: OidcDiscovery,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a 192-bit random key ID.
|
||||||
|
#[inline(always)]
|
||||||
|
fn generate_kid() -> Result<String> {
|
||||||
|
let mut kid_bytes = [0u8; 24];
|
||||||
|
|
||||||
|
OsRng.try_fill_bytes(&mut kid_bytes)?;
|
||||||
|
|
||||||
|
Ok(Base64Url::encode_string(&kid_bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OidcDiscovery {}
|
|
@ -0,0 +1,24 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use secrecy::Secret;
|
||||||
|
|
||||||
|
use crate::auth::oidc::generate_kid;
|
||||||
|
|
||||||
|
pub struct Hs256 {
|
||||||
|
kid: String,
|
||||||
|
key: Secret<[u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hs256 {
|
||||||
|
pub fn new() -> Result<Hs256> {
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
|
||||||
|
OsRng.try_fill_bytes(&mut key)?;
|
||||||
|
|
||||||
|
Ok(Hs256 {
|
||||||
|
kid: generate_kid()?,
|
||||||
|
key: Secret::new(key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use base64ct::{Base64Url, Encoding};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use rsa::pkcs1v15::{SigningKey, VerifyingKey};
|
||||||
|
use rsa::{PublicKeyParts, RsaPrivateKey, RsaPublicKey};
|
||||||
|
use serde_json::json;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::auth::oidc::generate_kid;
|
||||||
|
|
||||||
|
static RSA_BITS: usize = 2048;
|
||||||
|
|
||||||
|
/// RSASSA-PKCS1v1.5 with SHA-2-256
|
||||||
|
///
|
||||||
|
/// Only 2048-bit keys are used.
|
||||||
|
pub struct Rs256 {
|
||||||
|
kid: String,
|
||||||
|
signing_key: SigningKey<Sha256>,
|
||||||
|
verifying_key: VerifyingKey<Sha256>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rs256 {
|
||||||
|
pub fn new() -> Result<Rs256> {
|
||||||
|
let kid = generate_kid()?;
|
||||||
|
|
||||||
|
let p = RsaPrivateKey::new(&mut OsRng, RSA_BITS)?;
|
||||||
|
|
||||||
|
let signing_key = SigningKey::new_with_prefix(p);
|
||||||
|
|
||||||
|
let verifying_key = VerifyingKey::from(&signing_key);
|
||||||
|
|
||||||
|
Ok(Rs256 {
|
||||||
|
kid,
|
||||||
|
signing_key,
|
||||||
|
verifying_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discovery_key(&self) -> Result<serde_json::Value> {
|
||||||
|
let public_key: &RsaPublicKey = self.verifying_key.as_ref();
|
||||||
|
|
||||||
|
let n_bytes = public_key.n().to_bytes_be();
|
||||||
|
|
||||||
|
let n = Base64Url::encode_string(&n_bytes);
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"alg": "RS256",
|
||||||
|
"kty": "RSA",
|
||||||
|
"kid": self.kid,
|
||||||
|
"e": "AQAB",
|
||||||
|
"n": n,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rsa::{PublicKeyParts, RsaPublicKey};
|
||||||
|
|
||||||
|
use crate::auth::oidc::rs256::*;
|
||||||
|
|
||||||
|
// This can fail in two situations.
|
||||||
|
// 1. BigUint no longer uses the minimum bits required.
|
||||||
|
// 2. The `rsa` crate has changed its default exponent.
|
||||||
|
#[test]
|
||||||
|
fn rsa_default_exp_is_serialized_as_aqab() {
|
||||||
|
let private_key = RsaPrivateKey::new(&mut OsRng, RSA_BITS).unwrap();
|
||||||
|
|
||||||
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
|
||||||
|
let e_bytes = public_key.e().to_bytes_be();
|
||||||
|
|
||||||
|
let e_str = Base64Url::encode_string(&e_bytes);
|
||||||
|
|
||||||
|
assert_eq!(e_str, "AQAB");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
server: ServerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
host: String,
|
||||||
|
port: u32,
|
||||||
|
|
||||||
|
#[serde(rename = "base-url")]
|
||||||
|
base_url: String,
|
||||||
|
|
||||||
|
tls: TlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct TlsConfig {
|
||||||
|
offload: bool,
|
||||||
|
|
||||||
|
certificate: PathBuf,
|
||||||
|
|
||||||
|
#[serde(rename = "private-key")]
|
||||||
|
private_key: PathBuf,
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
use std::convert::AsRef;
|
||||||
|
use std::ops::{Deref, Drop};
|
||||||
|
use std::option::Option;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::vec::Vec;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
static SCHEMA: &str = include_str!("../schema.sql");
|
||||||
|
|
||||||
|
pub struct PooledConnection {
|
||||||
|
conn: Option<Connection>,
|
||||||
|
pool: Arc<Mutex<Vec<Connection>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Naive databse pool.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Database {
|
||||||
|
pool: Arc<Mutex<Vec<Connection>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires WAL and sets up complementary settings.
|
||||||
|
pub fn setup_sqlite_conn(path: &dyn AsRef<Path>) -> Result<Connection> {
|
||||||
|
let conn = Connection::open(path)?;
|
||||||
|
|
||||||
|
conn.pragma_update(None, "journal_mode", "wal")?;
|
||||||
|
conn.pragma_update(None, "synchronous", "normal")?;
|
||||||
|
conn.pragma_update(None, "busy_timeout", 5000)?;
|
||||||
|
|
||||||
|
conn.execute_batch(SCHEMA)?;
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for PooledConnection {
|
||||||
|
type Target = Connection;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.conn.as_ref().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PooledConnection {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut p = self.pool.blocking_lock();
|
||||||
|
|
||||||
|
p.push(self.conn.take().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub fn new(path: &dyn AsRef<Path>, capacity: usize) -> Result<Database> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
|
||||||
|
v.try_reserve_exact(capacity)?;
|
||||||
|
|
||||||
|
for _ in 0..capacity {
|
||||||
|
let c = setup_sqlite_conn(path)?;
|
||||||
|
|
||||||
|
v.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pool = Arc::new(Mutex::new(v));
|
||||||
|
|
||||||
|
Ok(Database { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&mut self) -> Result<PooledConnection> {
|
||||||
|
let mut pool = self.pool.lock().await;
|
||||||
|
|
||||||
|
let conn = pool.pop();
|
||||||
|
|
||||||
|
Ok(PooledConnection {
|
||||||
|
conn,
|
||||||
|
pool: Arc::clone(&self.pool),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
120
src/main.rs
120
src/main.rs
|
@ -1,3 +1,119 @@
|
||||||
fn main() {
|
mod auth;
|
||||||
println!("Hello, world!");
|
mod config;
|
||||||
|
mod database;
|
||||||
|
mod model;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use tracing::{error, Level};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Run the server
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Run {
|
||||||
|
/// Path to the configuration file
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
/// Emit logs as ND-JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
|
||||||
|
/// Repeat to increase verbosity up to 2 times
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Keygen {
|
||||||
|
/// Path to the configuration file
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
api: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
encrypt: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
oidc_rs256: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Gc {
|
||||||
|
/// Path to the configuration file
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: PathBuf,
|
||||||
|
},
|
||||||
|
|
||||||
|
Revoke {
|
||||||
|
/// Path to the configuration file
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logging(use_json: bool, verbosity: u8) -> Result<()> {
|
||||||
|
let max_level = match verbosity {
|
||||||
|
0 => Level::INFO,
|
||||||
|
1 => Level::DEBUG,
|
||||||
|
2.. => Level::TRACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fmt = tracing_subscriber::fmt().with_max_level(max_level);
|
||||||
|
|
||||||
|
if use_json {
|
||||||
|
let collector = fmt.json().finish();
|
||||||
|
tracing::subscriber::set_global_default(collector)?;
|
||||||
|
} else {
|
||||||
|
let collector = fmt.pretty().finish();
|
||||||
|
tracing::subscriber::set_global_default(collector)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::panic::set_hook(Box::new(|panic| {
|
||||||
|
if let Some(location) = panic.location() {
|
||||||
|
error!(
|
||||||
|
message = %panic,
|
||||||
|
panic.file = location.file(),
|
||||||
|
panic.line = location.line(),
|
||||||
|
panic.column = location.column(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
error!(message = %panic);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::Run {
|
||||||
|
config,
|
||||||
|
json,
|
||||||
|
verbose,
|
||||||
|
} => {
|
||||||
|
setup_logging(json, verbose);
|
||||||
|
}
|
||||||
|
Commands::Keygen {
|
||||||
|
config,
|
||||||
|
api,
|
||||||
|
encrypt,
|
||||||
|
oidc_rs256,
|
||||||
|
} => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod user;
|
|
@ -0,0 +1,64 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use argon2::password_hash::PasswordHash;
|
||||||
|
use argon2::{Algorithm, Argon2, PasswordHasher, PasswordVerifier, Version};
|
||||||
|
use password_hash::SaltString;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use secrecy::SecretString;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Parameters are from https://twitter.com/Sc00bzT/status/1557495201064558592
|
||||||
|
static ARGON2_M: u32 = 256;
|
||||||
|
static ARGON2_T: u32 = 8;
|
||||||
|
static ARGON2_P: u32 = 1;
|
||||||
|
static ARGON2_OUTPUT_LEN: Option<usize> = Some(32);
|
||||||
|
|
||||||
|
pub struct User {
|
||||||
|
uuid: Uuid,
|
||||||
|
username: String,
|
||||||
|
secrethash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn get_passphrase_kdf<'a>() -> Result<Argon2<'a>> {
|
||||||
|
let params = argon2::Params::new(256, 8, 1, Some(32))?;
|
||||||
|
|
||||||
|
let kdf = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||||
|
|
||||||
|
Ok(kdf)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn new(username: String, secret: &[u8]) -> Result<User> {
|
||||||
|
let params = argon2::Params::new(ARGON2_M, ARGON2_T, ARGON2_P, ARGON2_OUTPUT_LEN)?;
|
||||||
|
let kdf = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||||
|
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
|
||||||
|
let secrethash = kdf.hash_password(secret, &salt)?.to_string();
|
||||||
|
|
||||||
|
Ok(User {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
username,
|
||||||
|
secrethash,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn regenerate_id(self) -> User {
|
||||||
|
User {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
username: self.username,
|
||||||
|
secrethash: self.secrethash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(self, pass: &[u8]) -> Result<bool> {
|
||||||
|
let kdf = get_passphrase_kdf()?;
|
||||||
|
|
||||||
|
let parsed_hash = PasswordHash::new(&self.secrethash)?;
|
||||||
|
|
||||||
|
match kdf.verify_password(pass, &parsed_hash) {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use axum::extract::FromRef;
|
||||||
|
|
||||||
|
use crate::database::Database;
|
||||||
|
|
||||||
|
// clone if cloneable
|
||||||
|
pub struct AppState {
|
||||||
|
database: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for Database {
|
||||||
|
fn from_ref(state: &AppState) -> Database {
|
||||||
|
state.database.clone()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue