totp_rs/
secret.rs

1//! Representation of a secret either a "raw" \[u8\] or "base 32" encoded String
2//!
3//! # Examples
4//!
5//! - Create a TOTP from a "raw" secret
6//! ```
7//! # #[cfg(not(feature = "otpauth"))] {
8//! use totp_rs::{Secret, TOTP, Algorithm};
9//!
10//! let secret = [
11//!     0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
12//!     0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
13//! ];
14//! let secret_raw = Secret::Raw(secret.to_vec());
15//! let totp_raw = TOTP::new(
16//!     Algorithm::SHA1,
17//!     6,
18//!     1,
19//!     30,
20//!     secret_raw.to_bytes().unwrap(),
21//! ).unwrap();
22//!
23//! println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap());
24//! # }
25//! ```
26//!
27//! - Create a TOTP from a base32 encoded secret
28//! ```
29//! # #[cfg(not(feature = "otpauth"))] {
30//! use totp_rs::{Secret, TOTP, Algorithm};
31//!
32//! let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
33//! let totp_b32 = TOTP::new(
34//!     Algorithm::SHA1,
35//!     6,
36//!     1,
37//!     30,
38//!     secret_b32.to_bytes().unwrap(),
39//! ).unwrap();
40//!
41//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap());
42//! # }
43//!
44//! ```
45//! - Create a TOTP from a Generated Secret
46//! ```
47//! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] {
48//! use totp_rs::{Secret, TOTP, Algorithm};
49//!
50//! let secret_b32 = Secret::default();
51//! let totp_b32 = TOTP::new(
52//!     Algorithm::SHA1,
53//!     6,
54//!     1,
55//!     30,
56//!     secret_b32.to_bytes().unwrap(),
57//! ).unwrap();
58//!
59//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap());
60//! # }
61//! ```
62//! - Create a TOTP from a Generated Secret 2
63//! ```
64//! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] {
65//! use totp_rs::{Secret, TOTP, Algorithm};
66//!
67//! let secret_b32 = Secret::generate_secret();
68//! let totp_b32 = TOTP::new(
69//!     Algorithm::SHA1,
70//!     6,
71//!     1,
72//!     30,
73//!     secret_b32.to_bytes().unwrap(),
74//! ).unwrap();
75//!
76//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap());
77//! # }
78//! ```
79
80use base32::{self, Alphabet};
81
82use constant_time_eq::constant_time_eq;
83
84/// Different ways secret parsing failed.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum SecretParseError {
87    /// Invalid base32 input.
88    ParseBase32,
89}
90
91impl std::error::Error for SecretParseError {}
92
93impl std::fmt::Display for SecretParseError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            SecretParseError::ParseBase32 => write!(f, "Could not decode base32 secret."),
97        }
98    }
99}
100
101impl std::error::Error for Secret {}
102
103/// Shared secret between client and server to validate token against/generate token from.
104#[derive(Debug, Clone, Eq)]
105#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
106pub enum Secret {
107    /// Non-encoded "raw" secret.
108    Raw(Vec<u8>),
109    /// Base32 encoded secret.
110    Encoded(String),
111}
112
113impl PartialEq for Secret {
114    /// Will check that to_bytes() returns the same.
115    /// One secret can be Raw, and the other Encoded.
116    fn eq(&self, other: &Self) -> bool {
117        constant_time_eq(&self.to_bytes().unwrap(), &other.to_bytes().unwrap())
118    }
119}
120
121#[cfg(feature = "gen_secret")]
122#[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
123impl Default for Secret {
124    fn default() -> Self {
125        Secret::generate_secret()
126    }
127}
128
129impl Secret {
130    /// Get the inner String value as a Vec of bytes.
131    pub fn to_bytes(&self) -> Result<Vec<u8>, SecretParseError> {
132        match self {
133            Secret::Raw(s) => Ok(s.to_vec()),
134            Secret::Encoded(s) => match base32::decode(Alphabet::Rfc4648 { padding: false }, s) {
135                Some(bytes) => Ok(bytes),
136                None => Err(SecretParseError::ParseBase32),
137            },
138        }
139    }
140
141    /// Try to transform a `Secret::Encoded` into a `Secret::Raw`
142    pub fn to_raw(&self) -> Result<Self, SecretParseError> {
143        match self {
144            Secret::Raw(_) => Ok(self.clone()),
145            Secret::Encoded(s) => match base32::decode(Alphabet::Rfc4648 { padding: false }, s) {
146                Some(buf) => Ok(Secret::Raw(buf)),
147                None => Err(SecretParseError::ParseBase32),
148            },
149        }
150    }
151
152    /// Try to transforms a `Secret::Raw` into a `Secret::Encoded`.
153    pub fn to_encoded(&self) -> Self {
154        match self {
155            Secret::Raw(s) => {
156                Secret::Encoded(base32::encode(Alphabet::Rfc4648 { padding: false }, s))
157            }
158            Secret::Encoded(_) => self.clone(),
159        }
160    }
161
162    /// Generate a CSPRNG binary value of 160 bits,
163    /// the recomended size from [rfc-4226](https://www.rfc-editor.org/rfc/rfc4226#section-4).
164    ///
165    /// > The length of the shared secret MUST be at least 128 bits.
166    /// > This document RECOMMENDs a shared secret length of 160 bits.
167    ///
168    /// ⚠️ The generated secret is not guaranteed to be a valid UTF-8 sequence.
169    #[cfg(feature = "gen_secret")]
170    #[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
171    pub fn generate_secret() -> Secret {
172        use rand::Rng;
173
174        let mut rng = rand::rng();
175        let mut secret: [u8; 20] = Default::default();
176        rng.fill(&mut secret[..]);
177        Secret::Raw(secret.to_vec())
178    }
179}
180
181impl std::fmt::Display for Secret {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            Secret::Raw(bytes) => {
185                for b in bytes {
186                    write!(f, "{:02x}", b)?;
187                }
188                Ok(())
189            }
190            Secret::Encoded(s) => write!(f, "{}", s),
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::Secret;
198
199    const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG";
200    const BYTES: [u8; 23] = [
201        0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
202        0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
203    ];
204    const BYTES_DISPLAY: &str = "706c61696e2d737472696e672d7365637265742d313233";
205
206    #[test]
207    fn secret_display() {
208        let base32_str = String::from(BASE32);
209        let secret_raw = Secret::Raw(BYTES.to_vec());
210        let secret_base32 = Secret::Encoded(base32_str);
211        println!("{}", secret_raw);
212        assert_eq!(secret_raw.to_string(), BYTES_DISPLAY.to_string());
213        assert_eq!(secret_base32.to_string(), BASE32.to_string());
214    }
215
216    #[test]
217    fn secret_convert_base32_raw() {
218        let base32_str = String::from(BASE32);
219        let secret_raw = Secret::Raw(BYTES.to_vec());
220        let secret_base32 = Secret::Encoded(base32_str);
221
222        assert_eq!(&secret_raw.to_encoded(), &secret_base32);
223        assert_eq!(&secret_raw.to_raw().unwrap(), &secret_raw);
224
225        assert_eq!(&secret_base32.to_raw().unwrap(), &secret_raw);
226        assert_eq!(&secret_base32.to_encoded(), &secret_base32);
227    }
228
229    #[test]
230    fn secret_as_bytes() {
231        let base32_str = String::from(BASE32);
232        assert_eq!(
233            Secret::Raw(BYTES.to_vec()).to_bytes().unwrap(),
234            BYTES.to_vec()
235        );
236        assert_eq!(
237            Secret::Encoded(base32_str).to_bytes().unwrap(),
238            BYTES.to_vec()
239        );
240    }
241
242    #[test]
243    fn secret_from_string() {
244        let raw: Secret = Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec());
245        let encoded: Secret = Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string());
246        assert_eq!(raw.to_encoded(), encoded);
247        assert_eq!(raw, encoded.to_raw().unwrap());
248    }
249
250    #[test]
251    #[cfg(feature = "gen_secret")]
252    fn secret_gen_secret() {
253        let sec = Secret::generate_secret();
254
255        assert!(matches!(sec, Secret::Raw(_)));
256        assert_eq!(sec.to_bytes().unwrap().len(), 20);
257    }
258
259    #[test]
260    #[cfg(feature = "gen_secret")]
261    fn secret_gen_default() {
262        let sec = Secret::default();
263
264        assert!(matches!(sec, Secret::Raw(_)));
265        assert_eq!(sec.to_bytes().unwrap().len(), 20);
266    }
267
268    #[test]
269    #[cfg(feature = "gen_secret")]
270    fn secret_empty() {
271        let non_ascii = vec![240, 159, 146, 150];
272        let sec = Secret::Encoded(std::str::from_utf8(&non_ascii).unwrap().to_owned());
273
274        let to_r = sec.to_raw();
275
276        assert!(to_r.is_err());
277
278        let to_b = sec.to_bytes();
279
280        assert!(to_b.is_err());
281    }
282}