Compare commits

..

No commits in common. "542ed7c97fb78d4b7d7fd362feb978f1acc3c11c" and "c890731092497be644de7db0f6a5821f5c03906c" have entirely different histories.

11 changed files with 601 additions and 586 deletions

View File

@ -1,26 +1,3 @@
# [5.0.1](https://github.com/constantoine/totp-rs/releases/tag/v5.0.1) (31/03/2023)
### Changes
- Normalize dependencies specifications since cargo uses range dependency by default.
### Special thanks
* [@bestia-dev](https://github.com/bestia-dev) for pointing out discrepancies in my dependency requirements.
# [5.0](https://github.com/constantoine/totp-rs/releases/tag/v5.0) (28/03/2023)
### Breaking changes.
- MSRV has been set to Rust `1.61`.
- Removed `SecretParseError::Utf8Error`.
### Changes
- Updated `base64` to `0.21`.
- Updated `url` to `2.3`.
- Updated `zeroize` to `1.6`.
### Note
This major release is a very small one, and is mostly here to respect semver. No major change was done, it is mostly maintenance and cleanup.
### Special thanks
* [@bestia-dev](https://github.com/bestia-dev) for opening #55.
# [4.2](https://github.com/constantoine/totp-rs/releases/tag/v4.2) (14/01/2023)
### Changes
- Optionnals parameters in generated URLs are no longer present if their're the default value. (#49)
@ -45,7 +22,7 @@ This major release is a very small one, and is mostly here to respect semver. No
- Default features have been set to none.
### Changes
- MSRV has been set to Rust `1.59`.
- MSRV have been set to Rust `1.59`.
- Updated `base64` crate to `0.20`.
### Breaking changes

View File

@ -1,8 +1,8 @@
[package]
name = "totp-rs"
version = "5.0.1"
version = "4.2.0"
authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
rust-version = "1.61"
rust-version = "1.59"
edition = "2021"
readme = "README.md"
license = "MIT"
@ -25,15 +25,15 @@ steam = []
[dependencies]
serde = { version = "1.0", features = ["derive"], optional = true }
sha2 = "0.10"
sha1 = "0.10"
hmac = "0.12"
base32 = "0.4"
urlencoding = { version = "2.1", optional = true}
url = { version = "2.3", optional = true }
constant_time_eq = "0.2"
qrcodegen = { version = "1.8", optional = true }
image = { version = "0.24", features = ["png"], optional = true, default-features = false}
base64 = { version = "0.21", optional = true }
rand = { version = "0.8", features = ["std_rng", "std"], optional = true, default-features = false }
zeroize = { version = "1.6", features = ["alloc", "derive"], optional = true }
sha2 = "~0.10.2"
sha1 = "~0.10.5"
hmac = "~0.12.1"
base32 = "~0.4"
urlencoding = { version = "^2.1.0", optional = true}
url = { version = "^2.2.2", optional = true }
constant_time_eq = "0.2.4"
qrcodegen = { version = "~1.8", optional = true }
image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false}
base64 = { version = "~0.20", optional = true }
rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false }
zeroize = { version = "1.5.7", features = ["alloc", "derive"], optional = true }

View File

@ -1,18 +1,16 @@
#[cfg(all(feature = "gen_secret", feature = "otpauth"))]
use totp_rs::{Algorithm, Secret, TOTP, LabeledTOTP};
use totp_rs::{Algorithm, Secret, TOTP};
#[cfg(all(feature = "gen_secret", feature = "otpauth"))]
fn main() {
let secret = Secret::generate_secret();
let totp = LabeledTOTP::new(
TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret.to_bytes().unwrap(),
).unwrap(),
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret.to_bytes().unwrap(),
None,
"account".to_string(),
)

View File

@ -1,8 +1,24 @@
use totp_rs::{Rfc6238, TOTP};
#[cfg(feature = "otpauth")]
fn main() {
let mut rfc = Rfc6238::with_defaults("totp-sercret-123".as_bytes().to_vec()).unwrap();
// optional, set digits, issuer, account_name
rfc.digits(8).unwrap();
rfc.issuer("issuer".to_string());
rfc.account_name("user-account".to_string());
// create a TOTP from rfc
let totp = TOTP::from_rfc6238(rfc).unwrap();
let code = totp.generate_current().unwrap();
println!("code: {}", code);
}
#[cfg(not(feature = "otpauth"))]
fn main() {
let mut rfc = Rfc6238::with_defaults("totp-sercret-123".into()).unwrap();
// optional, set digits, issuer, account_name
rfc.digits(8).unwrap();

View File

@ -1,5 +1,6 @@
use totp_rs::{Algorithm, Secret, TOTP, LabeledTOTP};
use totp_rs::{Algorithm, Secret, TOTP};
#[cfg(feature = "otpauth")]
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
@ -9,12 +10,9 @@ fn main() {
1,
30,
secret_b32.to_bytes().unwrap(),
Some("issuer".to_string()),
"user-account".to_string(),
)
.and_then(|totp| LabeledTOTP::new(
totp,
"issuer".to_string(),
"user-account".to_string(),
))
.unwrap();
println!(
@ -39,12 +37,9 @@ fn main() {
1,
30,
secret_raw.to_bytes().unwrap(),
Some("issuer".to_string()),
"user-account".to_string(),
)
.and_then(|totp| LabeledTOTP::new(
totp,
"issuer".to_string(),
"user-account".to_string(),
))
.unwrap();
println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded());
@ -53,3 +48,34 @@ fn main() {
totp_raw.generate_current().unwrap()
);
}
#[cfg(not(feature = "otpauth"))]
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()).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()).unwrap();
println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded());
println!(
"code from raw secret:\t{}",
totp_raw.generate_current().unwrap()
);
}

View File

@ -1,12 +1,15 @@
#[cfg(feature = "steam")]
use totp_rs::{Secret, LabeledTOTP};
use totp_rs::{Secret, TOTP};
#[cfg(feature = "steam")]
#[cfg(feature = "otpauth")]
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string());
let totp_b32 = TOTP::new_steam(
secret_b32.to_bytes().unwrap(),
"user-account".to_string(),
);
println!(
"base32 {} ; raw {}",
@ -24,7 +27,7 @@ fn main() {
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap());
let totp_b32 = TOTP::new_steam(secret_b32.to_bytes().unwrap());
println!(
"base32 {} ; raw {}",
@ -38,4 +41,6 @@ fn main() {
}
#[cfg(not(feature = "steam"))]
fn main() {}
fn main() {
}

View File

@ -1,5 +1,21 @@
use totp_rs::{Algorithm, TOTP, LabeledTOTP};
use totp_rs::{Algorithm, TOTP};
#[cfg(not(feature = "otpauth"))]
fn main() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "my-secret".as_bytes().to_vec()).unwrap();
loop {
println!(
"code {}\t ttl {}\t valid until: {}",
totp.generate_current().unwrap(),
totp.ttl().unwrap(),
totp.next_step_current().unwrap()
);
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
#[cfg(feature = "otpauth")]
fn main() {
let totp = TOTP::new(
Algorithm::SHA1,
@ -7,12 +23,9 @@ fn main() {
1,
30,
"my-secret".as_bytes().to_vec(),
Some("Github".to_string()),
"constantoine@github.com".to_string(),
)
.and_then(|totp| LabeledTOTP::new(
totp,
"Github".to_string(),
"constantoine@github.com".to_string(),
))
.unwrap();
loop {

View File

@ -1,31 +1,45 @@
#[cfg(feature = "steam")]
use crate::{Algorithm, TOTP, LabeledTOTP};
use crate::{Algorithm, TOTP};
#[cfg(feature = "steam")]
impl LabeledTOTP {
impl TOTP {
#[cfg(feature = "otpauth")]
/// Will create a new instance of TOTP using the Steam algorithm 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, LabeledTOTP};
/// use totp_rs::{Secret, TOTP};
/// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".into());
/// let totp = LabeledTOTP::new_steam(secret.to_bytes().unwrap(), "username".into());
/// let totp = TOTP::new_steam(secret.to_bytes().unwrap(), "username".into());
/// ```
pub fn new_steam(secret: Vec<u8>, account_name: String) -> Self {
pub fn new_steam(secret: Vec<u8>, account_name: String) -> TOTP {
Self::new_unchecked(
TOTP::new_unchecked(
Algorithm::Steam,
5,
1,
30,
secret,
),
"Steam".to_owned(),
Algorithm::Steam,
5,
1,
30,
secret,
Some("Steam".into()),
account_name,
)
}
#[cfg(not(feature = "otpauth"))]
/// Will create a new instance of TOTP using the Steam algorithm 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};
/// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".to_string());
/// let totp = TOTP::new_steam(secret.to_bytes().unwrap());
/// ```
pub fn new_steam(secret: Vec<u8>) -> TOTP {
Self::new_unchecked(Algorithm::Steam, 5, 1, 30, secret)
}
}
#[cfg(all(test, feature = "steam"))]
@ -36,7 +50,7 @@ mod test {
#[test]
#[cfg(feature = "otpauth")]
fn get_url_steam() {
let totp = LabeledTOTP::new_steam("TestSecretSuperSecret".into(), "constantoine".into());
let totp = TOTP::new_steam("TestSecretSuperSecret".into(), "constantoine".into());
let url = totp.get_url();
assert_eq!(url.as_str(), "otpauth://steam/Steam:constantoine?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=5&algorithm=SHA1&issuer=Steam");
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
use crate::Algorithm;
use crate::LabeledTOTP;
use crate::TotpUrlError;
use crate::TOTP;
@ -78,6 +77,15 @@ pub struct Rfc6238 {
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
secret: Vec<u8>,
#[cfg(feature = "otpauth")]
/// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your service/website.
/// Not mandatory, but strongly recommended!
issuer: Option<String>,
#[cfg(feature = "otpauth")]
/// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your user's account.
account_name: String,
}
impl Rfc6238 {
@ -88,6 +96,27 @@ impl Rfc6238 {
/// will return a [Rfc6238Error](enum.Rfc6238Error.html) when
/// - `digits` is lower than 6 or higher than 8
/// - `secret` is smaller than 128 bits (16 characters)
#[cfg(feature = "otpauth")]
pub fn new(
digits: usize,
secret: Vec<u8>,
issuer: Option<String>,
account_name: String,
) -> Result<Rfc6238, Rfc6238Error> {
assert_digits(&digits)?;
assert_secret_length(secret.as_ref())?;
Ok(Rfc6238 {
algorithm: Algorithm::SHA1,
digits,
skew: 1,
step: 30,
secret,
issuer,
account_name,
})
}
#[cfg(not(feature = "otpauth"))]
pub fn new(digits: usize, secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
assert_digits(&digits)?;
assert_secret_length(secret.as_ref())?;
@ -109,6 +138,12 @@ impl Rfc6238 {
/// will return a [Rfc6238Error](enum.Rfc6238Error.html) when
/// - `digits` is lower than 6 or higher than 8
/// - `secret` is smaller than 128 bits (16 characters)
#[cfg(feature = "otpauth")]
pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
Rfc6238::new(6, secret, Some("".to_string()), "".to_string())
}
#[cfg(not(feature = "otpauth"))]
pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
Rfc6238::new(6, secret)
}
@ -119,8 +154,21 @@ impl Rfc6238 {
self.digits = value;
Ok(())
}
#[cfg(feature = "otpauth")]
/// Set the `issuer`
pub fn issuer(&mut self, value: String) {
self.issuer = Some(value);
}
#[cfg(feature = "otpauth")]
/// Set the `account_name`
pub fn account_name(&mut self, value: String) {
self.account_name = value;
}
}
#[cfg(not(feature = "otpauth"))]
impl TryFrom<Rfc6238> for TOTP {
type Error = TotpUrlError;
@ -130,7 +178,8 @@ impl TryFrom<Rfc6238> for TOTP {
}
}
impl TryFrom<Rfc6238> for LabeledTOTP {
#[cfg(feature = "otpauth")]
impl TryFrom<Rfc6238> for TOTP {
type Error = TotpUrlError;
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
@ -141,21 +190,35 @@ impl TryFrom<Rfc6238> for LabeledTOTP {
rfc.skew,
rfc.step,
rfc.secret,
).map(LabeledTOTP::from)
rfc.issuer,
rfc.account_name,
)
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "otpauth")]
use crate::TotpUrlError;
use super::{Rfc6238, TOTP};
#[cfg(not(feature = "otpauth"))]
use super::Rfc6238Error;
#[cfg(not(feature = "otpauth"))]
use crate::Secret;
const GOOD_SECRET: &str = "01234567890123456789";
#[cfg(feature = "otpauth")]
const ISSUER: Option<&str> = None;
#[cfg(feature = "otpauth")]
const ACCOUNT: &str = "valid-account";
#[cfg(feature = "otpauth")]
const INVALID_ACCOUNT: &str = ":invalid-account";
#[test]
#[cfg(not(feature = "otpauth"))]
fn new_rfc_digits() {
for x in 0..=20 {
let rfc = Rfc6238::new(x, GOOD_SECRET.into());
@ -169,6 +232,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn new_rfc_secret() {
let mut secret = String::from("");
for _ in 0..=20 {
@ -191,6 +255,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn rfc_to_totp_ok() {
let rfc = Rfc6238::new(8, GOOD_SECRET.into()).unwrap();
let totp = TOTP::try_from(rfc);
@ -204,6 +269,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn rfc_to_totp_ok_2() {
let rfc = Rfc6238::with_defaults(
Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string())
@ -221,6 +287,51 @@ mod tests {
}
#[test]
#[cfg(feature = "otpauth")]
fn rfc_to_totp_fail() {
let rfc = Rfc6238::new(
8,
GOOD_SECRET.as_bytes().to_vec(),
ISSUER.map(str::to_string),
INVALID_ACCOUNT.to_string(),
)
.unwrap();
let totp = TOTP::try_from(rfc);
assert!(totp.is_err());
assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)))
}
#[test]
#[cfg(feature = "otpauth")]
fn rfc_to_totp_ok() {
let rfc = Rfc6238::new(
8,
GOOD_SECRET.as_bytes().to_vec(),
ISSUER.map(str::to_string),
ACCOUNT.to_string(),
)
.unwrap();
let totp = TOTP::try_from(rfc);
assert!(totp.is_ok());
}
#[test]
#[cfg(feature = "otpauth")]
fn rfc_with_default_set_values() {
let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
let ok = rfc.digits(8);
assert!(ok.is_ok());
assert_eq!(rfc.account_name, "");
assert_eq!(rfc.issuer, Some("".to_string()));
rfc.issuer("Github".to_string());
rfc.account_name("constantoine".to_string());
assert_eq!(rfc.account_name, "constantoine");
assert_eq!(rfc.issuer, Some("Github".to_string()));
assert_eq!(rfc.digits, 8)
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn rfc_with_default_set_values() {
let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
let fail = rfc.digits(4);
@ -233,6 +344,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn digits_error() {
let error = crate::Rfc6238Error::InvalidDigits(9);
assert_eq!(
@ -242,6 +354,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn secret_length_error() {
let error = Rfc6238Error::SecretTooSmall(120);
assert_eq!(

View File

@ -78,24 +78,16 @@
//! ```
use base32::{self, Alphabet};
use std::string::FromUtf8Error;
use constant_time_eq::constant_time_eq;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecretParseError {
ParseBase32,
Utf8Error(FromUtf8Error),
}
impl std::fmt::Display for SecretParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SecretParseError::ParseBase32 => write!(f, "Could not decode base32 secret."),
}
}
}
impl std::error::Error for Secret {}
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
pub enum Secret {