authenticator/models/
keyring.rs1use 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
91pub 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 argon2::verify_encoded(
126 &String::from_utf8_lossy(&i.secret().await?),
127 password.as_bytes(),
128 )?
129 }
130 None => false,
131 })
132}