initial commit

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-07-18 20:30:39 +08:00
commit bfcc98b50e
39 changed files with 2904 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "auth_password"
version = "0.1.0"
edition = "2024"
[dependencies]
semios_account = { path = "../../lib/semios_account", features = ["client"] }
auth_method_fx = { path = "../../lib/auth_method_fx" }
password-hash = { version = "0.6", features = ["phc", "rand_core", "getrandom"] }
argon2 = "0.6.0-rc.8"
uuid = "1"
dialoguer = "0.12.0"
rand = "0.10"
aes-gcm = "0.10.3"
generic-array = "0.14.7"
+25
View File
@@ -0,0 +1,25 @@
use auth_method_fx::Mode;
use semios_account::{
auth::{AuthenticateArgs, UpdateArgs},
wellknown::OP_MODE_API,
};
#[derive(Debug)]
pub struct ApiMode;
impl Mode for ApiMode {
fn name(&self) -> &'static str {
OP_MODE_API
}
fn authenticate(
&self,
AuthenticateArgs { user_uuid, mode }: AuthenticateArgs,
) -> Result<(), Box<dyn std::error::Error>> {
debug_assert_eq!(mode, self.name());
todo!();
}
fn update(&self, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>> {
todo!();
}
}
+281
View File
@@ -0,0 +1,281 @@
//! The backend implementation.
use aes_gcm::{
Aes256Gcm as AesGcm, Nonce,
aead::{Aead, KeyInit},
};
use argon2::{ARGON2D_IDENT, ARGON2I_IDENT, ARGON2ID_IDENT, PasswordHash, PasswordVerifier};
use generic_array::GenericArray;
use password_hash::PasswordHasher;
use rand::Rng;
use semios_account::{
client::Client,
protocol::{GetSecretArgs, SetSecretArgs},
secret::{MasterKeyProvision, Secret, SecurityFlags},
};
use uuid::Uuid;
/// Default algorithm to hash new password strings.
type PasswordHashAlgorithm = argon2::Argon2<'static>;
/// Name of secret used to store the hashed password string.
const SECRET_NAME_PASSWORD: &str = "org.semilabs.os.account.auth.password/Password";
/// Name of secret used to store the encrypted (in this crate) master key.
const SECRET_NAME_MASTER_KEY: &str = "org.semilabs.os.account.auth.password/MasterKey";
/// List of supported crypto algorithms.
const CRYPTO_ALGO_LIST: &[&dyn CryptoAlgo] = &[&Aes256Gcm];
/// Default crypto algorithm.
const DEFAULT_CRYPTO_ALGO: &dyn CryptoAlgo = CRYPTO_ALGO_LIST[0];
/// Updates password of a user.
pub fn update(
uuid: &Uuid,
old_password: &str,
new_password: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect_default()?;
let crypto_algo;
let clear_text_key;
let master_key_secret = client.get_secret(GetSecretArgs {
user: uuid.clone(),
name: SECRET_NAME_MASTER_KEY.into(),
});
if let Ok(master_key_secret) = &master_key_secret {
let (algo, encrypted_key) = extract_secret_master_key_data(&master_key_secret.data)?;
crypto_algo = find_crypto(algo)?;
clear_text_key = decrypt_key(crypto_algo, encrypted_key, old_password)?;
} else {
crypto_algo = DEFAULT_CRYPTO_ALGO;
clear_text_key = DEFAULT_CRYPTO_ALGO.generate_key();
}
let reencrypted_key = encrypt_key(crypto_algo, &clear_text_key, new_password)?;
client.set_secret(SetSecretArgs {
user: uuid.clone(),
secret: compose_secret_master_key(crypto_algo, &reencrypted_key),
})?;
client.set_secret(SetSecretArgs {
user: uuid.clone(),
secret: compose_secret_password(new_password)?,
})?;
Ok(())
}
/// Verifies if the provided password can login the user represented by the provided UUID.
pub fn verify(uuid: &Uuid, password: &str) -> Result<bool, Box<dyn std::error::Error>> {
let mut client = Client::connect_default()?;
let secret = client.get_secret(GetSecretArgs {
user: uuid.clone(),
name: SECRET_NAME_PASSWORD.into(),
})?;
verify_secret_password(&secret, password)
}
/// Provides master key of a user.
pub fn provide_master_key(
uuid: &Uuid,
password: &str,
) -> Result<MasterKeyProvision, Box<dyn std::error::Error>> {
let mut client = Client::connect_default()?;
let secret = client.get_secret(GetSecretArgs {
user: uuid.clone(),
name: SECRET_NAME_MASTER_KEY.into(),
})?;
let (algo, encrypted_key) = extract_secret_master_key_data(&secret.data)?;
let algo = find_crypto(algo)?;
let key = decrypt_key(algo, encrypted_key, password)?;
Ok(MasterKeyProvision {
algorithm: algo.name().into(),
key,
expiration_time: i64::MAX,
})
}
/// Verifies if the password matches the managed secret item.
///
/// # Errors
/// This function would return an error if:
///
/// - the PHC is invalid;
/// - the algorithm used is unsupported;
/// - the secret has invalid permission settings.
fn verify_secret_password(
secret: &Secret,
password: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
if !secret
.security_flags
.contains(SecurityFlags::WRITE_PROTECTED)
{
return Err(Box::from("insecure password secret"));
}
let phc = str::from_utf8(&secret.data)?;
verify_phc(phc, password)
}
/// Verifies if the password matches the PHC text.
///
/// # Errors
/// This function would return an error if the PHC is invalid, or uses an unsupported algorithm.
fn verify_phc(phc: &str, password: &str) -> Result<bool, Box<dyn std::error::Error>> {
let phc = PasswordHash::new(phc)?;
match phc.algorithm {
ARGON2D_IDENT | ARGON2ID_IDENT | ARGON2I_IDENT => Ok(argon2::Argon2::default()
.verify_password(password.as_bytes(), &phc)
.is_ok()),
_ => Err(Box::from("Unsupported algorithm")),
}
}
/// Compose a [`Secret`] representing to the given password text.
fn compose_secret_password(password: &str) -> Result<Secret, Box<dyn std::error::Error>> {
Ok(Secret {
name: SECRET_NAME_PASSWORD.into(),
data: compose_phc(password)?.into_bytes(),
security_flags: SecurityFlags::WRITE_PROTECTED,
creation_time: 0,
expiration_time: i64::MAX,
})
}
/// Composes a PHC string, hashing the provided password text using the default algorithm.
fn compose_phc(password: &str) -> Result<String, Box<dyn std::error::Error>> {
let algorithm = PasswordHashAlgorithm::default();
Ok(algorithm.hash_password(password.as_bytes())?.to_string())
}
/// Extracts algorithm and encrypted key from master key secret data.
fn extract_secret_master_key_data(
data: &[u8],
) -> Result<(&str, &[u8]), Box<dyn std::error::Error>> {
let zero_position = data.iter().position(|x| *x == 0).unwrap_or_default();
if zero_position == 0 {
return Err(Box::from("invalid master key format"));
}
let Ok(algo) = str::from_utf8(&data[..zero_position]) else {
return Err(Box::from("invalid master key format"));
};
Ok((algo, &data[zero_position + 1..]))
}
fn compose_secret_master_key(algorithm: &dyn CryptoAlgo, encrypted_key: &[u8]) -> Secret {
Secret {
name: SECRET_NAME_MASTER_KEY.into(),
data: compose_secret_master_key_data(algorithm, encrypted_key),
security_flags: SecurityFlags::WRITE_PROTECTED,
creation_time: 0,
expiration_time: i64::MAX,
}
}
/// Composes data of the [`Secret`] representing to the master key.
fn compose_secret_master_key_data(algorithm: &dyn CryptoAlgo, encrypted_key: &[u8]) -> Vec<u8> {
[algorithm.name().as_bytes(), encrypted_key].join(&b'\0')
}
/// Encrypts a key via a password.
fn encrypt_key(
algorithm: &dyn CryptoAlgo,
key: &[u8],
password: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
Ok(algorithm.encrypt(&key_by_password(algorithm, password), key))
}
/// Decrypts a encrypted key via a password.
fn decrypt_key(
algorithm: &dyn CryptoAlgo,
encrypted_key: &[u8],
password: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
algorithm.decrypt(&key_by_password(algorithm, password), encrypted_key)
}
fn key_by_password(algorithm: &dyn CryptoAlgo, password: &str) -> Vec<u8> {
let mut key = password.as_bytes().to_vec();
key.resize(algorithm.key_len(), 0);
key
}
fn find_crypto(name: &str) -> Result<&dyn CryptoAlgo, Box<dyn std::error::Error>> {
CRYPTO_ALGO_LIST
.iter()
.find(|x| x.name() == name)
.copied()
.ok_or_else(|| Box::from("unknown crypto algorithm"))
}
trait CryptoAlgo: Send + Sync {
/// Returns name of the algorithm.
fn name(&self) -> &'static str;
/// Length of a key, in bytes.
fn key_len(&self) -> usize;
/// Encrypt data.
fn encrypt(&self, key: &[u8], data: &[u8]) -> Vec<u8>;
/// Decrypt data.
fn decrypt(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
/// Generates a key.
fn generate_key(&self) -> Vec<u8> {
let mut v = vec![0; self.key_len()];
rand::fill(&mut v);
v
}
}
#[derive(Debug)]
struct Aes256Gcm;
impl CryptoAlgo for Aes256Gcm {
fn name(&self) -> &'static str {
MasterKeyProvision::ALGORITHM_AES_256_GCM
}
fn key_len(&self) -> usize {
256 / 8
}
fn encrypt(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
assert_eq!(key.len(), self.key_len(), "Invalid key length");
let mut nonce = [0u8; 12];
rand::rng().fill_bytes(&mut nonce);
let nonce = Nonce::from_slice(&nonce);
let cipher = AesGcm::new(GenericArray::from_slice(key));
let ciphertext = cipher
.encrypt(nonce, data.as_ref())
.expect("encryption failure!");
let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
result.extend_from_slice(nonce);
result.extend_from_slice(&ciphertext);
result
}
fn decrypt(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
assert_eq!(key.len(), self.key_len(), "Invalid key length");
if data.len() < 12 {
return Err("Data too short".into());
}
let (nonce_bytes, ciphertext) = data.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let cipher = AesGcm::new(GenericArray::from_slice(key));
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| format!("decryption failure: {:?}", e))?;
Ok(plaintext)
}
}
+42
View File
@@ -0,0 +1,42 @@
use auth_method_fx::Mode;
use semios_account::{
auth::{AuthenticateArgs, UpdateArgs},
wellknown::OP_MODE_CLI,
};
#[derive(Debug)]
pub struct CliMode;
impl Mode for CliMode {
fn name(&self) -> &'static str {
OP_MODE_CLI
}
fn authenticate(&self, args: AuthenticateArgs) -> Result<(), Box<dyn std::error::Error>> {
let password: String = dialoguer::Password::new()
.allow_empty_password(true)
.with_prompt("Password")
.interact()?;
if !matches!(crate::backend::verify(&args.user_uuid, &password), Ok(true)) {
eprintln!("Sorry.");
std::process::exit(1);
}
Ok(())
}
fn update(&self, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>> {
let old_password: String = dialoguer::Password::new()
.allow_empty_password(true)
.with_prompt("Old Password")
.interact()?;
let new_password: String = dialoguer::Password::new()
.allow_empty_password(true)
.with_prompt("New Password")
.with_confirmation("Confirm password", "Passwords mismatching")
.interact()?;
if let Err(err) = crate::backend::update(&args.user_uuid, &old_password, &new_password) {
eprintln!("Unable to update user password: {err}");
std::process::exit(1);
}
Ok(())
}
}
+11
View File
@@ -0,0 +1,11 @@
mod api;
mod backend;
mod cli;
fn main() -> Result<(), Box<dyn std::error::Error>> {
auth_method_fx::App::new("password")
.provides_master_key()
.mode(Box::new(api::ApiMode))
.mode(Box::new(cli::CliMode))
.run()
}