commit
6b9f13f6a2
|
@ -19,6 +19,7 @@ default = []
|
|||
otpauth = ["url", "urlencoding"]
|
||||
qr = ["qrcodegen", "image", "base64", "otpauth"]
|
||||
serde_support = ["serde"]
|
||||
gen_secret = ["rand"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
@ -27,8 +28,9 @@ sha-1 = "~0.10.0"
|
|||
hmac = "~0.12.1"
|
||||
base32 = "~0.4"
|
||||
urlencoding = { version = "^2.1.0", optional = true}
|
||||
url = { version = "^2.2.2", optional = true }
|
||||
url = { version = "^2.2.2", optional = true }
|
||||
constant_time_eq = "~0.2.1"
|
||||
qrcodegen = { version = "~1.8", optional = true }
|
||||
image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false}
|
||||
base64 = { version = "~0.13", optional = true }
|
||||
base64 = { version = "~0.13", optional = true }
|
||||
rand = { version = "~0.8.5", optional = true }
|
|
@ -0,0 +1,26 @@
|
|||
#[cfg(not(feature = "gen_secret"))]
|
||||
compile_error!("requires feature gen_secret");
|
||||
|
||||
use totp_rs::{Secret, TOTP, Algorithm};
|
||||
|
||||
fn main () {
|
||||
|
||||
let secret = Secret::generate_secret();
|
||||
|
||||
let totp = TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret.to_bytes().unwrap(),
|
||||
None,
|
||||
"account".to_string(),
|
||||
).unwrap();
|
||||
|
||||
println!(
|
||||
"secret raw: {} ; secret base32 {} ; code: {}",
|
||||
secret,
|
||||
secret.to_encoded(),
|
||||
totp.generate_current().unwrap()
|
||||
)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use totp_rs::{Secret, TOTP, Algorithm};
|
||||
|
||||
fn main () {
|
||||
// create TOTP from base32 secret
|
||||
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
|
||||
let totp_b32 = TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret_b32.to_bytes().unwrap(),
|
||||
None,
|
||||
"account".to_string(),
|
||||
).unwrap();
|
||||
|
||||
println!("base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap());
|
||||
println!("code from base32:\t{}", totp_b32.generate_current().unwrap());
|
||||
|
||||
// create TOTP from raw binary value
|
||||
let secret = [
|
||||
0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
|
||||
0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
|
||||
];
|
||||
let secret_raw = Secret::Raw(secret.to_vec());
|
||||
let totp_raw = TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret_raw.to_bytes().unwrap(),
|
||||
None,
|
||||
"account".to_string(),
|
||||
).unwrap();
|
||||
|
||||
println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded());
|
||||
println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap());
|
||||
}
|
17
src/lib.rs
17
src/lib.rs
|
@ -45,12 +45,14 @@
|
|||
//! # }
|
||||
//! ```
|
||||
|
||||
mod secret;
|
||||
mod rfc;
|
||||
|
||||
mod url_error;
|
||||
use url_error::TotpUrlError;
|
||||
|
||||
pub use secret::{Secret, SecretParseError};
|
||||
use url_error::TotpUrlError;
|
||||
pub use rfc::{Rfc6238, Rfc6238Error};
|
||||
pub use base32;
|
||||
|
||||
use constant_time_eq::constant_time_eq;
|
||||
|
||||
|
@ -137,6 +139,8 @@ pub struct TOTP<T = Vec<u8>> {
|
|||
/// Duration in seconds of a step. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds
|
||||
pub step: u64,
|
||||
/// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended
|
||||
///
|
||||
/// non-encoded value
|
||||
pub secret: T,
|
||||
/// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:`
|
||||
/// For example, the name of your service/website.
|
||||
|
@ -171,6 +175,13 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
|||
/// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values
|
||||
///
|
||||
/// # Description
|
||||
/// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
|
||||
///
|
||||
/// ```rust
|
||||
/// use totp_rs::{Secret, TOTP, Algorithm};
|
||||
/// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string());
|
||||
/// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string()).unwrap();
|
||||
/// ```
|
||||
/// * `digits`: MUST be between 6 & 8
|
||||
/// * `secret`: Must have bitsize of at least 128
|
||||
/// * `account_name`: Must not contain `:`
|
||||
|
@ -621,7 +632,7 @@ mod tests {
|
|||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test").is_err());
|
||||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256").is_err());
|
||||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err())
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
use std::string::FromUtf8Error;
|
||||
use base32::{self, Alphabet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SecretParseError {
|
||||
ParseBase32,
|
||||
Utf8Error(FromUtf8Error),
|
||||
}
|
||||
|
||||
/// Representation of a secret either a "raw" \[u8\] or "base 32" encoded String
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// - Create a TOTP from a "raw" secret
|
||||
/// ```
|
||||
/// use totp_rs::{Secret, TOTP, Algorithm};
|
||||
///
|
||||
/// let secret = [
|
||||
/// 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
|
||||
/// 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
|
||||
/// ];
|
||||
/// let secret_raw = Secret::Raw(secret.to_vec());
|
||||
/// let totp_raw = TOTP::new(
|
||||
/// Algorithm::SHA1,
|
||||
/// 6,
|
||||
/// 1,
|
||||
/// 30,
|
||||
/// secret_raw.to_bytes().unwrap(),
|
||||
/// None,
|
||||
/// "account".to_string(),
|
||||
/// ).unwrap();
|
||||
///
|
||||
/// println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// - Create a TOTP from a base32 encoded secret
|
||||
/// ```
|
||||
/// use totp_rs::{Secret, TOTP, Algorithm};
|
||||
///
|
||||
/// let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
|
||||
/// let totp_b32 = TOTP::new(
|
||||
/// Algorithm::SHA1,
|
||||
/// 6,
|
||||
/// 1,
|
||||
/// 30,
|
||||
/// secret_b32.to_bytes().unwrap(),
|
||||
/// None,
|
||||
/// "account".to_string(),
|
||||
/// ).unwrap();
|
||||
///
|
||||
/// println!("code from base32:\t{}", totp_b32.generate_current().unwrap());
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Secret {
|
||||
/// represent a non-encoded "raw" secret
|
||||
Raw(Vec<u8>),
|
||||
/// represent a base32 encoded secret
|
||||
Encoded(String),
|
||||
}
|
||||
|
||||
impl Secret {
|
||||
|
||||
/// Get the inner String value as a Vec of bytes
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, SecretParseError> {
|
||||
match self {
|
||||
Secret::Raw(s) => Ok(s.to_vec()),
|
||||
Secret::Encoded(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) {
|
||||
Some(bytes) => Ok(bytes),
|
||||
None => Err(SecretParseError::ParseBase32),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to transform a `Secret::Encoded` into a `Secret::Raw`
|
||||
pub fn to_raw(&self) -> Result<Self, SecretParseError> {
|
||||
match self {
|
||||
Secret::Raw(_) => Ok(self.clone()),
|
||||
Secret::Encoded(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) {
|
||||
Some(buf) => Ok(Secret::Raw(buf)),
|
||||
None => Err(SecretParseError::ParseBase32),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to transforms a `Secret::Raw` into a `Secret::Encoded`
|
||||
pub fn to_encoded(&self) -> Self {
|
||||
match self {
|
||||
Secret::Raw(s) => Secret::Encoded(base32::encode(
|
||||
Alphabet::RFC4648 { padding: false },
|
||||
&s,
|
||||
)),
|
||||
Secret::Encoded(_) => self.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ⚠️ requires feature `gen_secret`
|
||||
///
|
||||
/// Generate a CSPRNG binary value of 160 bits,
|
||||
/// the recomended size from [rfc-4226](https://www.rfc-editor.org/rfc/rfc4226#section-4)
|
||||
///
|
||||
/// > The length of the shared secret MUST be at least 128 bits.
|
||||
/// > This document RECOMMENDs a shared secret length of 160 bits.
|
||||
///
|
||||
/// ⚠️ The generated secret is not guaranteed to be a valid UTF-8 sequence
|
||||
#[cfg(feature = "gen_secret")]
|
||||
pub fn generate_secret() -> Secret {
|
||||
use rand::Rng;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut secret: [u8; 20] = Default::default();
|
||||
rng.fill(&mut secret);
|
||||
Secret::Raw(secret.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Secret {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Secret::Raw(bytes) => {
|
||||
let mut s: String = String::new();
|
||||
for b in bytes {
|
||||
s = format!("{}{:02x}", &s, &b);
|
||||
}
|
||||
write!(f, "{}", s)
|
||||
},
|
||||
Secret::Encoded(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Secret;
|
||||
|
||||
const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG";
|
||||
const BYTES: [u8; 23] = [
|
||||
0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
|
||||
0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
|
||||
];
|
||||
const BYTES_DISPLAY: &str = "706c61696e2d737472696e672d7365637265742d313233";
|
||||
|
||||
#[test]
|
||||
fn secret_display() {
|
||||
let base32_str = String::from(BASE32);
|
||||
let secret_raw = Secret::Raw(BYTES.to_vec());
|
||||
let secret_base32 = Secret::Encoded(base32_str.clone());
|
||||
println!("{}", secret_raw);
|
||||
assert_eq!(secret_raw.to_string(), BYTES_DISPLAY.to_string());
|
||||
assert_eq!(secret_base32.to_string(), BASE32.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_convert_base32_raw() {
|
||||
let base32_str = String::from(BASE32);
|
||||
let secret_raw = Secret::Raw(BYTES.to_vec());
|
||||
let secret_base32 = Secret::Encoded(base32_str.clone());
|
||||
|
||||
assert_eq!(&secret_raw.to_encoded(), &secret_base32);
|
||||
assert_eq!(&secret_raw.to_raw().unwrap(), &secret_raw);
|
||||
|
||||
assert_eq!(&secret_base32.to_raw().unwrap(), &secret_raw);
|
||||
assert_eq!(&secret_base32.to_encoded(), &secret_base32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_as_bytes() {
|
||||
let base32_str = String::from(BASE32);
|
||||
assert_eq!(Secret::Raw(BYTES.to_vec()).to_bytes().unwrap(), BYTES.to_vec());
|
||||
assert_eq!(Secret::Encoded(base32_str).to_bytes().unwrap(), BYTES.to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "gen_secret")]
|
||||
fn secret_gen_secret() {
|
||||
match Secret::generate_secret() {
|
||||
Secret::Raw(secret) => assert_eq!(secret.len(), 20),
|
||||
Secret::Encoded(_) => panic!("should be raw"),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue