1use 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}