|
|
|
@@ -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)
|
|
|
|
|
}
|
|
|
|
|
}
|