otp/
lib.rs

1//! OTP — a one time password code generator library
2use config::Config;
3
4pub mod config;
5mod secrets;
6pub mod totp;
7
8#[cfg(feature = "rsa_stoken")]
9use crate::config::TotpOptions;
10#[cfg(feature = "rsa_stoken")]
11use stoken::{self, chrono::Utc};
12
13use crate::totp::TokenAlgorithm;
14use std::iter::FromIterator;
15use std::path::Path;
16use std::{
17    error::Error,
18    fmt::{Display, Formatter, Result as FmtResult},
19    result::Result,
20};
21
22#[derive(Debug)]
23pub struct TotpError<'a>(&'a str);
24
25impl<'a> Display for TotpError<'a> {
26    fn fmt(&self, f: &mut Formatter) -> FmtResult {
27        write!(f, "{}", self.0)
28    }
29}
30
31impl<'a> Error for TotpError<'a> {}
32
33impl<'a> TotpError<'a> {
34    pub fn of(msg: &'a str) -> Self {
35        TotpError(msg)
36    }
37}
38
39#[derive(Debug)]
40pub struct TotpConfigError(String);
41
42impl Display for TotpConfigError {
43    fn fmt(&self, f: &mut Formatter) -> FmtResult {
44        write!(f, "{}", self.0)
45    }
46}
47
48impl Error for TotpConfigError {}
49
50pub type TotpResult<T> = Result<T, Box<dyn Error>>;
51
52#[cfg(feature = "rsa_stoken")]
53fn stoken(name: &str, options: &TotpOptions) -> TotpResult<String> {
54    let token = stoken::export::import(secrets::get_secret(name, options)?.to_string())
55        .ok_or(TotpError("Unable to import secret as an RSA stoken secret"))?;
56    Ok(stoken::generate(token, Utc::now()))
57}
58
59pub fn token(name: &str, config: Config) -> TotpResult<String> {
60    let options = config.lookup(name)?;
61
62    match config.lookup(name)?.algorithm() {
63        TokenAlgorithm::TotpSha1 => totp::standard_totp(name, options),
64        #[cfg(feature = "rsa_stoken")]
65        TokenAlgorithm::SToken => stoken(name, options),
66    }
67}
68
69pub fn add_totp_secret<P: AsRef<Path>>(
70    config: Config,
71    config_dir: P,
72    name: &str,
73    secret: String,
74) -> TotpResult<()> {
75    base32::decode(base32::Alphabet::Rfc4648 { padding: false }, &secret)
76        .expect("Invalid base32 OTP secret");
77
78    add_secret(&config, config_dir, name, secret, TokenAlgorithm::TotpSha1).map(|_| ())
79}
80
81#[cfg(feature = "ras_stoken")]
82pub fn add_stoken<P: AsRef<Path>>(
83    config: &Config,
84    config_dir: P,
85    name: &str,
86    rsa_token_file: P,
87    pin: &str,
88) -> TotpResult<()> {
89    let token = stoken::read_file(rsa_token_file);
90    let token = stoken::RSAToken::from_xml(token, pin);
91    let exported_token = stoken::export::export(token).expect("Unable to export RSA Token");
92    add_secret(
93        &config,
94        config_dir,
95        name,
96        exported_token,
97        TokenAlgorithm::SToken,
98    )?;
99
100    Ok(())
101}
102
103pub fn add_secret<P: AsRef<Path>>(
104    config: &Config,
105    config_dir: P,
106    name: &str,
107    secret: String,
108    algorithm: TokenAlgorithm,
109) -> TotpResult<Config> {
110    let totp_options = secrets::store_secret(name, &secret, algorithm)?;
111    let mut config: Config = config.clone();
112    config.insert(name.to_string(), totp_options);
113    let string = toml::to_string(&config)?;
114
115    config::ensure_config_dir(&config_dir)?;
116
117    std::fs::write(config_dir.as_ref().join("config.toml"), string)?;
118    Ok(config)
119}
120
121pub fn list_secrets(config: Config, _prefix: Option<String>) -> TotpResult<Vec<String>> {
122    Ok(Vec::from_iter(config.codes().keys().cloned()))
123}
124
125pub fn delete_secret<P: AsRef<Path>>(
126    mut config: Config,
127    config_dir: P,
128    name: String,
129) -> TotpResult<()> {
130    config.remove(&name);
131    let string = toml::to_string(&config).expect("unable to write config to TOML");
132    config::ensure_config_dir(&config_dir)?;
133    std::fs::write(config_dir.as_ref().join("config.toml"), string)?;
134    Ok(())
135}
136
137#[cfg(feature = "keychain")]
138pub fn migrate_secrets_to_keychain<P: AsRef<Path>>(
139    config: Config,
140    config_dir: P,
141) -> TotpResult<Config> {
142    let mut new_codes = config.clone();
143    for (name, value) in config.codes().iter() {
144        println!("Migrating {}", name);
145        let secret = secrets::get_secret(name, value)?;
146        new_codes = add_secret(
147            &new_codes,
148            config_dir.as_ref(),
149            name,
150            secret,
151            value.algorithm(),
152        )?;
153    }
154
155    Ok(new_codes)
156}