Compare commits

...

9 Commits

Author SHA1 Message Date
Michael Pfaff 542ed7c97f
Allow use without allocation, decouple issuer and account_name from otpauth feature 2023-04-29 13:39:17 -04:00
Cléo REBERT 81c773f471
Fix changelog
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-31 16:18:26 +02:00
Cléo REBERT 606078ee1d
Normalize dependency requirements
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-31 16:10:37 +02:00
Cléo REBERT 360b4e7611
Updated changelog
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-28 14:37:03 +02:00
Cléo Rebert f0b8934763
Merge pull request #56 from constantoine/55-add-error-display-trait-for-secretparseerror
Fix #55
2023-03-28 14:31:33 +02:00
Cléo REBERT f7d0f136d1
Updated changelog
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-28 11:04:15 +02:00
Cléo REBERT 08c24dae96
Removed default feature
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-28 11:03:04 +02:00
Cléo REBERT c2ba6d190b
Replace deprecated base64 call
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-28 10:58:39 +02:00
Cléo REBERT e4e055dedd
5.0
Signed-off-by: Cléo REBERT <cleo.rebert-ext@treezor.com>
2023-03-28 10:47:52 +02:00
11 changed files with 586 additions and 601 deletions

View File

@ -1,3 +1,26 @@
# [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)
@ -22,7 +45,7 @@
- Default features have been set to none.
### Changes
- MSRV have been set to Rust `1.59`.
- MSRV has 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 = "4.2.0"
version = "5.0.1"
authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
rust-version = "1.59"
rust-version = "1.61"
edition = "2021"
readme = "README.md"
license = "MIT"
@ -25,15 +25,15 @@ steam = []
[dependencies]
serde = { version = "1.0", features = ["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 }
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 }

View File

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

View File

@ -1,24 +1,8 @@
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,6 +1,5 @@
use totp_rs::{Algorithm, Secret, TOTP};
use totp_rs::{Algorithm, Secret, TOTP, LabeledTOTP};
#[cfg(feature = "otpauth")]
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
@ -10,9 +9,12 @@ 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!(
@ -37,9 +39,12 @@ 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());
@ -48,34 +53,3 @@ 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,15 +1,12 @@
#[cfg(feature = "steam")]
use totp_rs::{Secret, TOTP};
use totp_rs::{Secret, LabeledTOTP};
#[cfg(feature = "steam")]
#[cfg(feature = "otpauth")]
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
let totp_b32 = TOTP::new_steam(
secret_b32.to_bytes().unwrap(),
"user-account".to_string(),
);
let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string());
println!(
"base32 {} ; raw {}",
@ -27,7 +24,7 @@ fn main() {
fn main() {
// create TOTP from base32 secret
let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"));
let totp_b32 = TOTP::new_steam(secret_b32.to_bytes().unwrap());
let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap());
println!(
"base32 {} ; raw {}",
@ -41,6 +38,4 @@ fn main() {
}
#[cfg(not(feature = "steam"))]
fn main() {
}
fn main() {}

View File

@ -1,21 +1,5 @@
use totp_rs::{Algorithm, TOTP};
use totp_rs::{Algorithm, TOTP, LabeledTOTP};
#[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,
@ -23,9 +7,12 @@ 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,45 +1,31 @@
#[cfg(feature = "steam")]
use crate::{Algorithm, TOTP};
use crate::{Algorithm, TOTP, LabeledTOTP};
#[cfg(feature = "steam")]
impl TOTP {
#[cfg(feature = "otpauth")]
impl LabeledTOTP {
/// 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};
/// use totp_rs::{Secret, LabeledTOTP};
/// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".into());
/// let totp = TOTP::new_steam(secret.to_bytes().unwrap(), "username".into());
/// let totp = LabeledTOTP::new_steam(secret.to_bytes().unwrap(), "username".into());
/// ```
pub fn new_steam(secret: Vec<u8>, account_name: String) -> TOTP {
pub fn new_steam(secret: Vec<u8>, account_name: String) -> Self {
Self::new_unchecked(
Algorithm::Steam,
5,
1,
30,
secret,
Some("Steam".into()),
TOTP::new_unchecked(
Algorithm::Steam,
5,
1,
30,
secret,
),
"Steam".to_owned(),
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"))]
@ -50,7 +36,7 @@ mod test {
#[test]
#[cfg(feature = "otpauth")]
fn get_url_steam() {
let totp = TOTP::new_steam("TestSecretSuperSecret".into(), "constantoine".into());
let totp = LabeledTOTP::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,4 +1,5 @@
use crate::Algorithm;
use crate::LabeledTOTP;
use crate::TotpUrlError;
use crate::TOTP;
@ -77,15 +78,6 @@ 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 {
@ -96,27 +88,6 @@ 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())?;
@ -138,12 +109,6 @@ 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)
}
@ -154,21 +119,8 @@ 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;
@ -178,8 +130,7 @@ impl TryFrom<Rfc6238> for TOTP {
}
}
#[cfg(feature = "otpauth")]
impl TryFrom<Rfc6238> for TOTP {
impl TryFrom<Rfc6238> for LabeledTOTP {
type Error = TotpUrlError;
/// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config
@ -190,35 +141,21 @@ impl TryFrom<Rfc6238> for TOTP {
rfc.skew,
rfc.step,
rfc.secret,
rfc.issuer,
rfc.account_name,
)
).map(LabeledTOTP::from)
}
}
#[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());
@ -232,7 +169,6 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn new_rfc_secret() {
let mut secret = String::from("");
for _ in 0..=20 {
@ -255,7 +191,6 @@ 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);
@ -269,7 +204,6 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn rfc_to_totp_ok_2() {
let rfc = Rfc6238::with_defaults(
Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string())
@ -287,51 +221,6 @@ 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);
@ -344,7 +233,6 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn digits_error() {
let error = crate::Rfc6238Error::InvalidDigits(9);
assert_eq!(
@ -354,7 +242,6 @@ mod tests {
}
#[test]
#[cfg(not(feature = "otpauth"))]
fn secret_length_error() {
let error = Rfc6238Error::SecretTooSmall(120);
assert_eq!(

View File

@ -78,16 +78,24 @@
//! ```
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 {