authenticator/models/
keyring.rs

1use std::{collections::HashMap, sync::OnceLock};
2
3use rand::Rng;
4
5use crate::config;
6
7pub static SECRET_SERVICE: OnceLock<oo7::Keyring> = OnceLock::new();
8
9fn token_attributes(token_id: &str) -> HashMap<&str, &str> {
10    HashMap::from([
11        ("application", config::APP_ID),
12        ("type", "token"),
13        ("token_id", token_id),
14    ])
15}
16
17fn password_attributes() -> HashMap<&'static str, &'static str> {
18    HashMap::from([("application", config::APP_ID), ("type", "password")])
19}
20
21fn encode_argon2(secret: &str) -> anyhow::Result<String> {
22    let password = secret.as_bytes();
23    let mut salt = [0u8; 64];
24    rand::rng().fill_bytes(&mut salt);
25    let config = argon2::Config::default();
26    let hash = argon2::hash_encoded(password, &salt, &config)?;
27
28    Ok(hash)
29}
30
31pub async fn store(label: &str, token: &str) -> anyhow::Result<String> {
32    let token_id = encode_argon2(token)?;
33    let attributes = token_attributes(&token_id);
34    let base64_encoded_token = hex::encode(token.as_bytes());
35    SECRET_SERVICE
36        .get()
37        .unwrap()
38        .create_item(label, &attributes, base64_encoded_token.as_bytes(), true)
39        .await?;
40    Ok(token_id)
41}
42
43pub async fn token(token_id: &str) -> anyhow::Result<Option<String>> {
44    let attributes = token_attributes(token_id);
45    let items = SECRET_SERVICE
46        .get()
47        .unwrap()
48        .search_items(&attributes)
49        .await?;
50    Ok(match items.first() {
51        Some(e) => Some(String::from_utf8(hex::decode(&*e.secret().await?)?)?),
52        _ => None,
53    })
54}
55
56pub async fn remove_token(token_id: &str) -> anyhow::Result<()> {
57    let attributes = token_attributes(token_id);
58    SECRET_SERVICE.get().unwrap().delete(&attributes).await?;
59    Ok(())
60}
61
62pub async fn token_exists(token: &str) -> anyhow::Result<bool> {
63    let attributes = HashMap::from([("application", config::APP_ID), ("type", "token")]);
64    let items = SECRET_SERVICE
65        .get()
66        .unwrap()
67        .search_items(&attributes)
68        .await?;
69    for item in items {
70        let item_token = String::from_utf8(hex::decode(&*item.secret().await?)?)?;
71        if item_token == token {
72            return Ok(true);
73        }
74    }
75    Ok(false)
76}
77
78pub async fn has_set_password() -> anyhow::Result<bool> {
79    let attributes = password_attributes();
80    match SECRET_SERVICE
81        .get()
82        .unwrap()
83        .search_items(&attributes)
84        .await
85    {
86        Ok(items) => Ok(!items.is_empty()),
87        _ => Ok(false),
88    }
89}
90
91/// Stores password using the Argon2 algorithm with a random 128bit salt.
92pub async fn set_password(password: &str) -> anyhow::Result<()> {
93    let encoded_password = encode_argon2(password)?;
94    let attributes = password_attributes();
95    SECRET_SERVICE
96        .get()
97        .unwrap()
98        .create_item(
99            "Authenticator password",
100            &attributes,
101            encoded_password.as_bytes(),
102            true,
103        )
104        .await?;
105    Ok(())
106}
107
108pub async fn reset_password() -> anyhow::Result<()> {
109    let attributes = password_attributes();
110    SECRET_SERVICE.get().unwrap().delete(&attributes).await?;
111    Ok(())
112}
113
114pub async fn is_current_password(password: &str) -> anyhow::Result<bool> {
115    let attributes = password_attributes();
116    let items = SECRET_SERVICE
117        .get()
118        .unwrap()
119        .search_items(&attributes)
120        .await?;
121    Ok(match items.first() {
122        Some(i) => {
123            // Verifies that the hash generated by `password` corresponds
124            // to `hash`.
125            argon2::verify_encoded(
126                &String::from_utf8_lossy(&i.secret().await?),
127                password.as_bytes(),
128            )?
129        }
130        None => false,
131    })
132}