Skip to main content

lfsx_server/console/
tokens.rs

1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use crate::error::Error;
7use crate::storage::Store;
8
9const FILE: &str = "dashboard-tokens.json";
10const PREFIX: &str = "lfsx_";
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Issued {
14    pub name: String,
15    pub created: u64,
16    hash: String,
17}
18
19#[derive(Debug, thiserror::Error)]
20pub enum Refusal {
21    #[error("a token name is 1 to 64 letters, digits, dots, dashes or underscores")]
22    Unreadable,
23    #[error("a token named {0} already exists")]
24    Taken(String),
25    #[error("no token is named {0}")]
26    Unknown(String),
27    #[error(transparent)]
28    Store(#[from] Error),
29}
30
31fn digest(token: &str) -> String {
32    hex::encode(Sha256::digest(token.as_bytes()))
33}
34
35fn readable(name: &str) -> bool {
36    (1..=64).contains(&name.len())
37        && name
38            .chars()
39            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
40}
41
42pub async fn issued(store: &Store) -> Result<Vec<Issued>, Error> {
43    match store.read_meta(FILE).await? {
44        Some(bytes) => Ok(serde_json::from_slice(&bytes)?),
45        None => Ok(Vec::new()),
46    }
47}
48
49async fn keep(store: &Store, issued: &[Issued]) -> Result<(), Error> {
50    store
51        .write_meta(FILE, serde_json::to_vec_pretty(issued)?)
52        .await
53}
54
55pub async fn create(store: &Store, name: &str) -> Result<String, Refusal> {
56    if !readable(name) {
57        return Err(Refusal::Unreadable);
58    }
59    let mut issued = issued(store).await?;
60    if issued.iter().any(|token| token.name == name) {
61        return Err(Refusal::Taken(name.to_owned()));
62    }
63
64    let mut secret = [0u8; 32];
65    getrandom::fill(&mut secret).expect("the operating system has a random number generator");
66    let token = format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(secret));
67
68    issued.push(Issued {
69        name: name.to_owned(),
70        created: time::OffsetDateTime::now_utc()
71            .unix_timestamp()
72            .unsigned_abs(),
73        hash: digest(&token),
74    });
75    keep(store, &issued).await?;
76
77    Ok(token)
78}
79
80pub async fn revoke(store: &Store, name: &str) -> Result<(), Refusal> {
81    let mut issued = issued(store).await?;
82    let before = issued.len();
83    issued.retain(|token| token.name != name);
84    if issued.len() == before {
85        return Err(Refusal::Unknown(name.to_owned()));
86    }
87
88    Ok(keep(store, &issued).await?)
89}
90
91impl Issued {
92    pub fn hash(&self) -> &str {
93        &self.hash
94    }
95}
96
97pub async fn holder(store: &Store, token: &str) -> Result<Option<Issued>, Error> {
98    if !token.starts_with(PREFIX) {
99        return Ok(None);
100    }
101    let hash = digest(token);
102
103    Ok(issued(store)
104        .await?
105        .into_iter()
106        .find(|issued| issued.hash == hash))
107}
108
109pub async fn still_issued(store: &Store, hash: &str) -> Result<bool, Error> {
110    Ok(issued(store)
111        .await?
112        .iter()
113        .any(|issued| issued.hash == hash))
114}