Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
name = "totp-rs"
version = "5.7.0"
authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
rust-version = "1.66"
edition = "2021"
rust-version = "1.88"
edition = "2024"
readme = "README.md"
license = "MIT"
description = "RFC-compliant TOTP implementation with ease of use as a goal and additionnal QoL features."
Expand All @@ -18,10 +18,10 @@ rustdoc-args = ["--cfg", "docsrs"]

[features]
default = []
gen_secret = ["rand"]
otpauth = ["url", "urlencoding"]
qr = ["dep:qrcodegen-image", "otpauth"]
serde_support = ["serde"]
gen_secret = ["rand"]
serde_support = ["serde", "zeroize?/serde"]
steam = []

[dependencies]
Expand All @@ -32,7 +32,15 @@ hmac = "0.12"
base32 = "0.5"
urlencoding = { version = "2.1", optional = true}
url = { version = "2.4", optional = true }
constant_time_eq = "0.3"
rand = { version = "0.9", features = ["thread_rng"], optional = true, default-features = false }
zeroize = { version = "1.6", features = ["alloc", "derive"], optional = true }
qrcodegen-image = { version = "1.4", features = ["base64"], optional = true }
constant_time_eq = "0.4"
rand = { version = "0.10", features = ["thread_rng"], optional = true, default-features = false }
qrcodegen-image = { version = "1.5", features = ["base64"], optional = true }
zeroize = { version = "1.8", features = ["alloc", "derive"], optional = true }

[[example]]
name = "steam"
required-features = ["steam"]

[[example]]
name = "gen_secret"
required-features = ["gen_secret", "otpauth"]
36 changes: 13 additions & 23 deletions examples/gen_secret.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
#[cfg(all(feature = "gen_secret", feature = "otpauth"))]
use totp_rs::{Algorithm, Secret, Totp};
use totp_rs::{Builder, Totp};

#[cfg(all(feature = "gen_secret", feature = "otpauth"))]
#[cfg(not(feature = "otpauth"))]
fn main() {
let secret = Secret::generate_secret();
let totp: Totp = Builder::new().build().unwrap();

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()
)
println!("code: {}", totp.generate_current().unwrap())
}

#[cfg(not(all(feature = "gen_secret", feature = "otpauth")))]
fn main() {}
#[cfg(feature = "otpauth")]
fn main() {
let totp: Totp = Builder::new()
.with_account_name("Constantoine".to_string())
.build()
.unwrap();

println!("code: {}", totp.generate_current().unwrap())
}
29 changes: 0 additions & 29 deletions examples/rfc-6238.rs

This file was deleted.

81 changes: 0 additions & 81 deletions examples/secret.rs

This file was deleted.

28 changes: 18 additions & 10 deletions examples/steam.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
#[cfg(feature = "steam")]
use totp_rs::{Secret, Totp};
use base32;
use totp_rs::{Builder, 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 = Totp::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string());
let secret = base32::decode(
base32::Alphabet::Rfc4648 { padding: false },
"OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG",
)
.unwrap();
let secret_b32 = Secret::Raw(secret);

let totp_b32 = Builder::new_steam()
.with_secret(secret_b32.to_bytes().unwrap())
.with_account_name("user-account".to_string())
.build()
.unwrap();

println!(
"base32 {} ; raw {}",
Expand All @@ -19,12 +28,14 @@ fn main() {
);
}

#[cfg(feature = "steam")]
#[cfg(not(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());
let totp_b32 = Builder::new_steam()
.with_secret(secret_b32.to_bytes().unwrap())
.build()
.unwrap();

println!(
"base32 {} ; raw {}",
Expand All @@ -36,6 +47,3 @@ fn main() {
totp_b32.generate_current().unwrap()
);
}

#[cfg(not(feature = "steam"))]
fn main() {}
25 changes: 13 additions & 12 deletions examples/ttl.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use totp_rs::{Algorithm, Totp};
use totp_rs::Builder;

const GOOD_SECRET: &[u8] = "TestSecretSuperSecret".as_bytes();

#[cfg(not(feature = "otpauth"))]
fn main() {
let totp = Totp::new(Algorithm::SHA1, 6, 1, 30, "my-secret".as_bytes().to_vec()).unwrap();
let totp = Builder::new()
.with_secret(GOOD_SECRET.into())
.build()
.unwrap();

loop {
println!(
Expand All @@ -17,16 +22,12 @@ fn main() {

#[cfg(feature = "otpauth")]
fn main() {
let totp = Totp::new(
Algorithm::SHA1,
6,
1,
30,
"my-secret".as_bytes().to_vec(),
Some("Github".to_string()),
"constantoine@github.com".to_string(),
)
.unwrap();
let totp = Builder::new()
.with_account_name("constantoine@github.com".to_string())
.with_issuer(Some("Github".to_string()))
.with_secret(GOOD_SECRET.into())
.build()
.unwrap();

loop {
println!(
Expand Down
102 changes: 102 additions & 0 deletions src/algorithm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use hmac::Mac;
type HmacSha1 = hmac::Hmac<sha1::Sha1>;
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
type HmacSha512 = hmac::Hmac<sha2::Sha512>;

#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};

use std::error::Error;
use std::fmt;
use std::str::FromStr;

/// Alphabet for Steam tokens.
#[cfg(feature = "steam")]
pub(super) const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";

/// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A)
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", serde(try_from = "String", into = "String"))]
pub enum Algorithm {
/// HMAC-SHA1 is the default algorithm of most TOTP implementations.
/// Some will outright silently ignore the algorithm parameter to force using SHA1, leading to confusion.
SHA1,
/// HMAC-SHA256. Supported in theory according to [yubico](https://docs.yubico.com/yesdk/users-manual/application-oath/uri-string-format.html).
/// Ignored in practice by most.
SHA256,
/// HMAC-SHA512. Supported in theory according to [yubico](https://docs.yubico.com/yesdk/users-manual/application-oath/uri-string-format.html).
/// Ignored in practice by most.
SHA512,
#[cfg(feature = "steam")]
#[cfg_attr(docsrs, doc(cfg(feature = "steam")))]
/// Steam TOTP token algorithm.
Steam,
}

impl Default for Algorithm {
fn default() -> Self {
Algorithm::SHA1
}
}

impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Algorithm::SHA1 => f.write_str("SHA1"),
Algorithm::SHA256 => f.write_str("SHA256"),
Algorithm::SHA512 => f.write_str("SHA512"),
#[cfg(feature = "steam")]
Algorithm::Steam => f.write_str("SHA1"),
}
}
}

impl From<Algorithm> for String {
fn from(value: Algorithm) -> Self {
value.to_string()
}
}

impl TryFrom<String> for Algorithm {
type Error = Box<dyn Error>;

fn try_from(value: String) -> Result<Self, Self::Error> {
Self::from_str(&value)
}
}

impl FromStr for Algorithm {
type Err = Box<dyn Error>;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"SHA1" => Ok(Self::SHA1),
"SHA256" => Ok(Self::SHA256),
"SHA512" => Ok(Self::SHA512),
#[cfg(feature = "steam")]
"STEAM" => Ok(Self::Steam),
_ => Err(From::from(format!("Unknown feature: {}", s))),
}
}
}

impl Algorithm {
fn hash<D>(mut digest: D, data: &[u8]) -> Vec<u8>
where
D: Mac,
{
digest.update(data);
digest.finalize().into_bytes().to_vec()
}

pub(crate) fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
match self {
Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
Algorithm::SHA256 => Algorithm::hash(HmacSha256::new_from_slice(key).unwrap(), data),
Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data),
#[cfg(feature = "steam")]
Algorithm::Steam => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
}
}
}
Loading
Loading