authenticator/models/
keyring.rs
use std::{collections::HashMap, sync::OnceLock};
use rand::RngCore;
use crate::config;
pub static SECRET_SERVICE: OnceLock<oo7::Keyring> = OnceLock::new();
fn token_attributes(token_id: &str) -> HashMap<&str, &str> {
HashMap::from([
("application", config::APP_ID),
("type", "token"),
("token_id", token_id),
])
}
fn password_attributes() -> HashMap<&'static str, &'static str> {
HashMap::from([("application", config::APP_ID), ("type", "password")])
}
fn encode_argon2(secret: &str) -> anyhow::Result<String> {
let password = secret.as_bytes();
let mut salt = [0u8; 64];
rand::rng().fill_bytes(&mut salt);
let config = argon2::Config::default();
let hash = argon2::hash_encoded(password, &salt, &config)?;
Ok(hash)
}
pub async fn store(label: &str, token: &str) -> anyhow::Result<String> {
let token_id = encode_argon2(token)?;
let attributes = token_attributes(&token_id);
let base64_encoded_token = hex::encode(token.as_bytes());
SECRET_SERVICE
.get()
.unwrap()
.create_item(label, &attributes, base64_encoded_token.as_bytes(), true)
.await?;
Ok(token_id)
}
pub async fn token(token_id: &str) -> anyhow::Result<Option<String>> {
let attributes = token_attributes(token_id);
let items = SECRET_SERVICE
.get()
.unwrap()
.search_items(&attributes)
.await?;
Ok(match items.first() {
Some(e) => Some(String::from_utf8(hex::decode(&*e.secret().await?)?)?),
_ => None,
})
}
pub async fn remove_token(token_id: &str) -> anyhow::Result<()> {
let attributes = token_attributes(token_id);
SECRET_SERVICE.get().unwrap().delete(&attributes).await?;
Ok(())
}
pub async fn token_exists(token: &str) -> anyhow::Result<bool> {
let attributes = HashMap::from([("application", config::APP_ID), ("type", "token")]);
let items = SECRET_SERVICE
.get()
.unwrap()
.search_items(&attributes)
.await?;
for item in items {
let item_token = String::from_utf8(hex::decode(&*item.secret().await?)?)?;
if item_token == token {
return Ok(true);
}
}
Ok(false)
}
pub async fn has_set_password() -> anyhow::Result<bool> {
let attributes = password_attributes();
match SECRET_SERVICE
.get()
.unwrap()
.search_items(&attributes)
.await
{
Ok(items) => Ok(!items.is_empty()),
_ => Ok(false),
}
}
pub async fn set_password(password: &str) -> anyhow::Result<()> {
let encoded_password = encode_argon2(password)?;
let attributes = password_attributes();
SECRET_SERVICE
.get()
.unwrap()
.create_item(
"Authenticator password",
&attributes,
encoded_password.as_bytes(),
true,
)
.await?;
Ok(())
}
pub async fn reset_password() -> anyhow::Result<()> {
let attributes = password_attributes();
SECRET_SERVICE.get().unwrap().delete(&attributes).await?;
Ok(())
}
pub async fn is_current_password(password: &str) -> anyhow::Result<bool> {
let attributes = password_attributes();
let items = SECRET_SERVICE
.get()
.unwrap()
.search_items(&attributes)
.await?;
Ok(match items.first() {
Some(i) => {
argon2::verify_encoded(
&String::from_utf8_lossy(&i.secret().await?),
password.as_bytes(),
)?
}
None => false,
})
}