totp-rs-5.5.1/.cargo_vcs_info.json0000644000000001360000000000100124540ustar { "git": { "sha1": "57cbb0013e9a5395cd2c04e6e8f5e4741c296988" }, "path_in_vcs": "" }totp-rs-5.5.1/.github/workflows/rust.yml000064400000000000000000000040301046102023000163560ustar 00000000000000name: Rust on: push: branches: [ master ] pull_request: branches: [ master ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --all-features test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install llvm-tools-preview run: rustup component add llvm-tools-preview - name: Download grcov run: wget https://github.com/mozilla/grcov/releases/download/v0.8.11/grcov-x86_64-unknown-linux-gnu.tar.bz2 - name: Decompress grcov run: tar xvf grcov-x86_64-unknown-linux-gnu.tar.bz2 - name: Create coverage output dir run: mkdir -p target/coverage - name: All features run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --all-features - name: No feature run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test - name: otpauth feature run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --features=otpauth - name: gen_secret feature run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --features=gen_secret - name: otpauth+gensecret feature run: CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test --features=gen_secret,otpauth - name: Create coverage file run: ./grcov . --binary-path ./target/debug/deps/ -s . -t cobertura --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/cobertura.xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: token: ${{secrets.CODECOV_TOKEN}} - name: Archive code coverage results uses: actions/upload-artifact@v1 with: name: code-coverage-report path: target/coverage/cobertura.xml totp-rs-5.5.1/.github/workflows/security.yml000064400000000000000000000004111046102023000172270ustar 00000000000000name: cargo-audit on: schedule: - cron: '0 0 * * *' workflow_dispatch: jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} totp-rs-5.5.1/.gitignore000064400000000000000000000000751046102023000132360ustar 00000000000000.idea /target Cargo.lock .vscode/settings.json cobertura.xml totp-rs-5.5.1/CHANGELOG.md000064400000000000000000000236141046102023000130630ustar 00000000000000# [5.5.0](https://github.com/constantoine/totp-rs/releases/tag/v5.5.0) (19/01/2024) ### Changes - Documentation now indicates required feature. ### Special thanks * [@AntonnMal](https://github.com/AntonnMal) for his work on #64. # [5.4.0](https://github.com/constantoine/totp-rs/releases/tag/v5.4.0) (04/10/2023) ### Changes - `SecretParseError` now implements `std::error::Error`. ### Special thanks * [@FliegendeWurst](https://github.com/FliegendeWurst) for their work on #62. # [5.3.0](https://github.com/constantoine/totp-rs/releases/tag/v5.3.0) (10/09/2023) ### What's new - Creation of a new `qrcodegen-image` subcrate to handle image creation, as the wrapper is actually nice and could be used in placed not related to `totp-rs`. (#61) ### Changes - `TOTP::get_qr` was deprecated in favour of `TOTP::get_qr_base64` and `TOTP::get_qr_png`. ### Special thanks * [@tmpfs](https://github.com/tmpfs) for their work on #60 and implementation in #61. # [5.2.0](https://github.com/constantoine/totp-rs/releases/tag/v5.2.0) (10/08/2023) ### Changes - Updated `url` crate to `2.4`. # [5.1.0](https://github.com/constantoine/totp-rs/releases/tag/v5.1.0) (15/07/2023) ### What's new - Added some more documentation. ### Fix - Removed unnecessary allocation for `Secret.Display` for the `Raw` variant. # [5.0.2](https://github.com/constantoine/totp-rs/releases/tag/v5.0.1) (15/05/2023) ### Fix - Fix skew overflowing if value is over 128. ### Special thanks * [@carl-wallace](https://github.com/carl-wallace) for discovering #58. # [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) ### Fix - The issuer part of the Path when using the couple Issuer:AccountName wasn't cut correctly if the `:` was URL-encoded. (#50) ### Special thanks * [@timvisee](https://github.com/timvisee) for their work on #49 and discovering the bug leading to #50. # [4.1](https://github.com/constantoine/totp-rs/releases/tag/v4.1) (06/01/2023) ### What's new - Add a "steam" feature which adds support for steam's non-standard totp. - Add `_unchecked` variants for `TOTP::new` and `TOTP::from_url`, which skip certain checks like key_size and digit numbers. ### Special thanks * [@colemickens](colemickens) for opening #45. * [@timvisee](https://github.com/timvisee) for their work on #47 and #48, implementing an idea from #44 and working on #45. # [4.0](https://github.com/constantoine/totp-rs/releases/tag/v4.0) (29/12/2022) ### What's new - Default features have been set to none. ### Changes - MSRV has been set to Rust `1.59`. - Updated `base64` crate to `0.20`. ### Breaking changes - This was a relic from the beggining of the library, but `TOTP` is no longer generic. In my opinion, while having been used in the past for some historical reasons, the generic was mostly useless as almost everyone just used bytes as a secret, prevented us from doing some work like the `zeroize` feature, and overall made it more complex to new users than it needed to be. ### Special thanks * [@tmpfs](https://github.com/tmpfs) for the work done on #40. * [@timvisee](https://github.com/timvisee) for their feedback on #40. ## Note This is the last release for 2022. This project has thus far been a wild ride. Originally intended for a non-profit organization, it gained traction outside of it, and soon became one the projects I'm the most proud of. It has been a pleasure learning from amazing people, and getting precious feedback from real life users. The open-source community has always been a special place to me, and being able to put in the hours to finally give something back has been, is, an amazing opportunity. The year 2023 should see a lot less of breaking changes, as the library slowly approaches a form most users can happily use. This doesn't mean the library will stop being maintained, but I (hopefully) will stop breaking your stuff so often. As always for every new realease, please report any issue encountered while updating totp-rs to `4.0.0`. # [3.1](https://github.com/constantoine/totp-rs/releases/tag/v3.1) (03/11/2022) ### What's new - `get_qr()` now returns a `String` as an error. - `TOTP` now implements `core::fmt::Display` - `Rfc6238Error` and `TotpUrlError` now implement `std::error::Error` ### CI - Add better coverage thanks to `llvm-tools-preview` and `grcov` ### Style - Finally `cargo fmt`'d the whole repo ### Special thanks * [@tmpfs](https://github.com/tmpfs) for making me notice #41. # [3.0.1](https://github.com/constantoine/totp-rs/releases/tag/v3.0.1) (13/08/2022) ### Fixes * `TotpUrlError` was unexported. This is now fixed. (#29) * `base32` was reexported instead. It is now private, and will need to be an explicit dependency for the user to encore/decode base32 data. ### Changes * `Secret` comparison is now done in constant time. ### Special thanks * [@alexanderkja](https://github.com/alexanderkjall) for discovering #29. # [3.0](https://github.com/constantoine/totp-rs/releases/tag/v3.0) (09/08/2022) ### New features * Secret handling is now less error prone thanks to #25 * Totp now implements the `Default` trait, which will generate a strong secret, and have sane default values according to RFC-6238 like #26 * `Rfc6238` struct is exposed for easy Totp building * `Totp.ttl` convenience method will tell remaining validity time of token (not taking skew into account) ### New dependency * [gen_secret] uses `rand` to generate a secret ### Breaking * TotpUrlError now contain a string explaining. Inspired by #23 * Totp fields `issuer` and `account_name` won't be present anymore if feature `otpauth` isn't enabled * The secret and digits field will now be validated for SecretSize (>= 128 bits) ### Special thanks * [@sacovo](https://github.com/sacovo)for opening #23, from which the TotpUrlError rework was inspired * [@steven89](https://github.com/steven89) for the tremendous work and back and forth provided with #24 #25 and #26 ## Note This has been, I think, the update containing the most work. While a lot of unit testing have been done, and test cases added, coverage seems to have dropped. Please report any issue encountered while updating totp-rs to 3.0.0 # [2.1](https://github.com/constantoine/totp-rs/releases/tag/v2.1) (16/06/2022) ### New dependency * [otpauth] now uses `urlencoding`, which has no dependencies, to url-encode and url-decode values. Because doing this with the `url` library was kind of awkward. ### Fixes * Bug where your issuer would be incorrectly prefixed with a /, and comparison with the issuer parameter would fail. * Bug where the issuer and account name in path would not be correctly url decoded in path, but correctly decoded in url query. ### Special thanks @wyhaya for discovering the first problem in #21 # [2.0](https://github.com/constantoine/totp-rs/releases/tag/v2.0) (30/05/2022) ### What changed - `issuer` and `account_name` are now members of TOTP, and thus are not used anymore as function parameters for methods - `from_url()` now extracts issuer and label - Method `get_url()` now needs `otpauth` feature - Method `get_url()` now produces more correct urls - Methods `next_step(time: u64)` and `next_step_current` will return the timestamp of the next step's start - Feature `qr` enables feature `otpauth` ### Special thanks - @wyhaya for giving ideas and feedback for this release # [1.4](https://github.com/constantoine/totp-rs/releases/tag/v1.4) (06/05/2022) ## What's changed * Added url dependency for `otpauth` feature, which adds a `from_url` function to parse a `TOTP` object from url. Thanks to @wyhaya (https://github.com/constantoine/totp-rs/pull/19) # [1.3](https://github.com/constantoine/totp-rs/releases/tag/v1.3) (06/05/2022) ## What's changed * Added helper functions `generate_current` and `check_current`. Thanks to @wyhaya (https://github.com/constantoine/totp-rs/pull/17) * Clarified output format of get_qr in the docs # [1.2.1](https://github.com/constantoine/totp-rs/releases/tag/v1.2.1) (05/05/2022) ## What's changed * Disabled default image features to only enable png # [1.2](https://github.com/constantoine/totp-rs/releases/tag/v1.2) (05/05/2022) ## What's changed * Bumped "image" version to 0.24 * Removed "qrcode" library, which was abandoned years ago, to "qrcodegen", which is actively maintained # [1.1](https://github.com/constantoine/totp-rs/releases/tag/v1.1) (24/04/2022) ## What's changed * Mitigated possible timing attack as noticed per @gleb-chipiga in https://github.com/constantoine/totp-rs/issues/13 * Added PartialEq support for TOTP and PartialEq + Eq support for Algorithm, suggestion from @gleb-chipiga in https://github.com/constantoine/totp-rs/issues/14 # [1.0](https://github.com/constantoine/totp-rs/releases/tag/v1.0) (15/04/2022) ## What's Changed * Fixed wrongful results using hmac-256 and hmac-512 thanks to @ironhaven extensive researches within RFC's in https://github.com/constantoine/totp-rs/pull/12 ## What's coming next - The currently used "qrcode" library is abandonned. Preliminary work showed it was not compatible woth newer versions of the "image" library - I'd like to take that opportunity to rethink the way the "qr" feature is presentedtotp-rs-5.5.1/Cargo.lock0000644000000303170000000000100104330ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base32" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bytemuck" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "constant_time_eq" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" [[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "fdeflate" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest", ] [[package]] name = "idna" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] name = "image" version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" dependencies = [ "bytemuck", "byteorder", "color_quant", "num-traits", "png", ] [[package]] name = "libc" version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", "simd-adler32", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "png" version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" dependencies = [ "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "qrcodegen" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" [[package]] name = "qrcodegen-image" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca9a0acbf30b8b049036ec117cf982b64f08d9375632736f207859979432a99f" dependencies = [ "base64", "image", "qrcodegen", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "serde" version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tinyvec" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "totp-rs" version = "5.5.1" dependencies = [ "base32", "constant_time_eq", "hmac", "qrcodegen-image", "rand", "serde", "sha1", "sha2", "url", "urlencoding", "zeroize", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", "syn", ] totp-rs-5.5.1/Cargo.toml0000644000000036540000000000100104620ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.61" name = "totp-rs" version = "5.5.1" authors = ["Cleo Rebert "] description = "RFC-compliant TOTP implementation with ease of use as a goal and additionnal QoL features." homepage = "https://github.com/constantoine/totp-rs" readme = "README.md" keywords = [ "authentication", "2fa", "totp", "hmac", "otp", ] categories = [ "authentication", "web-programming", ] license = "MIT" repository = "https://github.com/constantoine/totp-rs" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.base32] version = "0.4" [dependencies.constant_time_eq] version = "0.2" [dependencies.hmac] version = "0.12" [dependencies.qrcodegen-image] version = "1.0" features = ["base64"] optional = true [dependencies.rand] version = "0.8" features = [ "std_rng", "std", ] optional = true default-features = false [dependencies.serde] version = "1.0" features = ["derive"] optional = true [dependencies.sha1] version = "0.10" [dependencies.sha2] version = "0.10" [dependencies.url] version = "2.4" optional = true [dependencies.urlencoding] version = "2.1" optional = true [dependencies.zeroize] version = "1.6" features = [ "alloc", "derive", ] optional = true [features] default = [] gen_secret = ["rand"] otpauth = [ "url", "urlencoding", ] qr = [ "dep:qrcodegen-image", "otpauth", ] serde_support = ["serde"] steam = [] totp-rs-5.5.1/Cargo.toml.orig000064400000000000000000000024571046102023000141430ustar 00000000000000[package] name = "totp-rs" version = "5.5.1" authors = ["Cleo Rebert "] rust-version = "1.61" edition = "2021" readme = "README.md" license = "MIT" description = "RFC-compliant TOTP implementation with ease of use as a goal and additionnal QoL features." repository = "https://github.com/constantoine/totp-rs" homepage = "https://github.com/constantoine/totp-rs" keywords = ["authentication", "2fa", "totp", "hmac", "otp"] categories = ["authentication", "web-programming"] [workspace] members = [ "qrcodegen-image" ] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] default = [] otpauth = ["url", "urlencoding"] qr = ["dep:qrcodegen-image", "otpauth"] serde_support = ["serde"] gen_secret = ["rand"] 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.4", optional = true } constant_time_eq = "0.2" rand = { version = "0.8", features = ["std_rng", "std"], optional = true, default-features = false } zeroize = { version = "1.6", features = ["alloc", "derive"], optional = true } qrcodegen-image = { version = "1.0", features = ["base64"], optional = true, path = "qrcodegen-image" } totp-rs-5.5.1/LICENSE000064400000000000000000000020621046102023000122510ustar 00000000000000MIT License Copyright (c) 2020-2022 Cléo Rebert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. totp-rs-5.5.1/README.md000064400000000000000000000155521046102023000125330ustar 00000000000000# totp-rs ![Build Status](https://github.com/constantoine/totp-rs/workflows/Rust/badge.svg) [![docs](https://docs.rs/totp-rs/badge.svg)](https://docs.rs/totp-rs) [![](https://img.shields.io/crates/v/totp-rs.svg)](https://crates.io/crates/totp-rs) [![codecov](https://codecov.io/gh/constantoine/totp-rs/branch/master/graph/badge.svg?token=Q50RAIFVWZ)](https://codecov.io/gh/constantoine/totp-rs) [![cargo-audit](https://github.com/constantoine/totp-rs/actions/workflows/security.yml/badge.svg)](https://github.com/constantoine/totp-rs/actions/workflows/security.yml) This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as lightweight as possible to ensure small binaries and short compilation time. It now supports parsing [otpauth URLs](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) into a totp object, with sane default values. Be aware that some authenticator apps will accept the `SHA256` and `SHA512` algorithms but silently fallback to `SHA1` which will make the `check()` function fail due to mismatched algorithms. ## Features --- ### qr With optional feature "qr", you can use it to generate a base64 png qrcode. This will enable feature `otpauth`. ### otpauth With optional feature "otpauth", support parsing the TOTP parameters from an `otpauth` URL, and generating an `otpauth` URL. It adds 2 fields to `TOTP`. ### serde_support With optional feature "serde_support", library-defined types `TOTP` and `Algorithm` and will be Deserialize-able and Serialize-able. ### gen_secret With optional feature "gen_secret", a secret will be generated for you to store in database. ### zeroize Securely zero secret information when the TOTP struct is dropped. ### steam Add support for Steam TOTP tokens. # Examples ## Summary 0. [Understanding Secret](#understanding-secret) 1. [Generate a token](#generate-a-token) 2. [Enable qrcode generation](#with-qrcode-generation) 3. [Enable serde support](#with-serde-support) 4. [Enable otpauth url support](#with-otpauth-url-support) 5. [Enable gen_secret support](#with-gensecret) 6. [With RFC-6238 compliant default](#with-rfc-6238-compliant-default) 7. [New TOTP from steam secret](#new-totp-from-steam-secret) ### Understanding Secret --- This new type was added as a disambiguation between Raw and already base32 encoded secrets. ```Rust Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()) ``` Is equivalent to ```Rust Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) ``` ### Generate a token --- Add it to your `Cargo.toml`: ```toml [dependencies] totp-rs = "^5.0" ``` You can then do something like: ```Rust use std::time::SystemTime; use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()).to_bytes().unwrap(), ).unwrap(); let token = totp.generate_current().unwrap(); println!("{}", token); } ``` Which is equivalent to: ```Rust use std::time::SystemTime; use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), ).unwrap(); let token = totp.generate_current().unwrap(); println!("{}", token); } ``` ### With qrcode generation --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^5.3" features = ["qr"] ``` You can then do something like: ```Rust use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), Some("Github".to_string()), "constantoine@github.com".to_string(), ).unwrap(); let qr_code = totp.get_qr_base64()?; println!("{}", qr_code); } ``` ### With serde support --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^5.0" features = ["serde_support"] ``` ### With otpauth url support --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^5.0" features = ["otpauth"] ``` You can then do something like: ```Rust use totp_rs::TOTP; fn main() { let otpauth = "otpauth://totp/GitHub:constantoine@github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&issuer=GitHub"; let totp = TOTP::from_url(otpauth).unwrap(); println!("{}", totp.generate_current().unwrap()); } ``` ### With gen_secret --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^5.3" features = ["gen_secret"] ``` You can then do something like: ```Rust use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::default().to_bytes().unwrap(), Some("Github".to_string()), "constantoine@github.com".to_string(), ).unwrap(); let qr_code = totp.get_qr_base64()?; println!("{}", qr_code); } ``` Which is equivalent to ```Rust use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap(), Some("Github".to_string()), "constantoine@github.com".to_string(), ).unwrap(); let qr_code = totp.get_qr_base64()?; println!("{}", qr_code); } ``` ### With RFC-6238 compliant default --- You can do something like this ```Rust use totp_rs::{Algorithm, TOTP, Secret, Rfc6238}; fn main () { let mut rfc = Rfc6238::with_defaults( Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), ) .unwrap(); // optional, set digits rfc.digits(8).unwrap(); // create a TOTP from rfc let totp = TOTP::from_rfc6238(rfc).unwrap(); let code = totp.generate_current().unwrap(); println!("code: {}", code); } ``` With `gen_secret` feature, you can go even further and have all values by default and a secure secret. Note: With `otpauth` feature, `TOTP.issuer` will be `None`, and `TOTP.account_name` will be `""`. Be sure to set those fields before generating an URL/QRCode ```Rust fn main() { let totp = TOTP::default(); let code = totp.generate_current().unwrap(); println!("code: {}", code); } ``` ### New TOTP from steam secret --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^5.3" features = ["steam"] ``` You can then do something like: ```Rust use totp_rs::{TOTP, Secret}; fn main() { let totp = TOTP::new_steam( Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), ).unwrap(); let code = totp.generate_current().unwrap(); println!("code: {}", code); } ```totp-rs-5.5.1/examples/gen_secret.rs000064400000000000000000000012001046102023000155370ustar 00000000000000#[cfg(all(feature = "gen_secret", feature = "otpauth"))] use totp_rs::{Algorithm, Secret, TOTP}; #[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(), None, "account".to_string(), ) .unwrap(); println!( "secret raw: {} ; secret base32 {} ; code: {}", secret, secret.to_encoded(), totp.generate_current().unwrap() ) } #[cfg(not(all(feature = "gen_secret", feature = "otpauth")))] fn main() {} totp-rs-5.5.1/examples/rfc-6238.rs000064400000000000000000000015321046102023000146030ustar 00000000000000use 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(); // create a TOTP from rfc let totp = TOTP::from_rfc6238(rfc).unwrap(); let code = totp.generate_current().unwrap(); println!("code: {}", code); } totp-rs-5.5.1/examples/secret.rs000064400000000000000000000044551046102023000147250ustar 00000000000000use totp_rs::{Algorithm, Secret, TOTP}; #[cfg(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(), Some("issuer".to_string()), "user-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(), Some("issuer".to_string()), "user-account".to_string(), ) .unwrap(); println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded()); println!( "code from raw secret:\t{}", 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() ); } totp-rs-5.5.1/examples/steam.rs000064400000000000000000000020751046102023000145450ustar 00000000000000#[cfg(feature = "steam")] 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 = TOTP::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string()); println!( "base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap() ); println!( "code from base32:\t{}", totp_b32.generate_current().unwrap() ); } #[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()); println!( "base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap() ); println!( "code from base32:\t{}", totp_b32.generate_current().unwrap() ); } #[cfg(not(feature = "steam"))] fn main() {} totp-rs-5.5.1/examples/ttl.rs000064400000000000000000000017721046102023000142420ustar 00000000000000use 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, 6, 1, 30, "my-secret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .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)); } } totp-rs-5.5.1/src/custom_providers.rs000064400000000000000000000042201046102023000160060ustar 00000000000000#[cfg(feature = "steam")] use crate::{Algorithm, TOTP}; #[cfg(feature = "steam")] #[cfg_attr(docsrs, doc(cfg(feature = "steam")))] 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)` /// /// # Example /// /// ```rust /// use totp_rs::{Secret, TOTP}; /// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".into()); /// let totp = TOTP::new_steam(secret.to_bytes().unwrap(), "username".into()); /// ``` pub fn new_steam(secret: Vec, account_name: String) -> TOTP { Self::new_unchecked( 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)` /// /// # Example /// /// ```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) -> TOTP { Self::new_unchecked(Algorithm::Steam, 5, 1, 30, secret) } } #[cfg(all(test, feature = "steam"))] mod test { #[cfg(feature = "otpauth")] use super::*; #[test] #[cfg(feature = "otpauth")] fn get_url_steam() { 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"); } } totp-rs-5.5.1/src/lib.rs000064400000000000000000001311741046102023000131560ustar 00000000000000//! This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as low-dependency as possible to ensure small binaries and short compilation time //! //! Be aware that some authenticator apps will accept the `SHA256` //! and `SHA512` algorithms but silently fallback to `SHA1` which will //! make the `check()` function fail due to mismatched algorithms. //! //! Use the `SHA1` algorithm to avoid this problem. //! //! # Examples //! //! ```rust //! # #[cfg(feature = "otpauth")] { //! use std::time::SystemTime; //! use totp_rs::{Algorithm, TOTP, Secret}; //! //! let totp = TOTP::new( //! Algorithm::SHA1, //! 6, //! 1, //! 30, //! Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()).to_bytes().unwrap(), //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); //! let token = totp.generate_current().unwrap(); //! println!("{}", token); //! # } //! ``` //! //! ```rust //! # #[cfg(feature = "qr")] { //! use totp_rs::{Algorithm, TOTP}; //! //! let totp = TOTP::new( //! Algorithm::SHA1, //! 6, //! 1, //! 30, //! "supersecret_topsecret".as_bytes().to_vec(), //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); //! let url = totp.get_url(); //! println!("{}", url); //! let code = totp.get_qr_base64().unwrap(); //! println!("{}", code); //! # } //! ``` // enable `doc_cfg` feature for `docs.rs`. #![cfg_attr(docsrs, feature(doc_cfg))] mod custom_providers; mod rfc; mod secret; mod url_error; #[cfg(feature = "qr")] pub use qrcodegen_image; pub use rfc::{Rfc6238, Rfc6238Error}; pub use secret::{Secret, SecretParseError}; pub use url_error::TotpUrlError; use constant_time_eq::constant_time_eq; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; use core::fmt; #[cfg(feature = "otpauth")] use url::{Host, Url}; use hmac::Mac; use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; type HmacSha1 = hmac::Hmac; type HmacSha256 = hmac::Hmac; type HmacSha512 = hmac::Hmac; /// Alphabet for Steam tokens. #[cfg(feature = "steam")] 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))] pub enum Algorithm { SHA1, SHA256, SHA512, #[cfg(feature = "steam")] #[cfg_attr(docsrs, doc(cfg(feature = "steam")))] /// Steam TOTP token algorithm Steam, } impl std::default::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 Algorithm { fn hash(mut digest: D, data: &[u8]) -> Vec where D: Mac, { digest.update(data); digest.finalize().into_bytes().to_vec() } fn sign(&self, key: &[u8], data: &[u8]) -> Vec { 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), } } } fn system_time() -> Result { let t = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); Ok(t) } /// TOTP holds informations as to how to generate an auth code and validate it. Its [secret](struct.TOTP.html#structfield.secret) field is sensitive data, treat it accordingly #[derive(Debug, Clone)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub struct TOTP { /// SHA-1 is the most widespread algorithm used, and for totp pursposes, SHA-1 hash collisions are [not a problem](https://tools.ietf.org/html/rfc4226#appendix-B.2) as HMAC-SHA-1 is not impacted. It's also the main one cited in [rfc-6238](https://tools.ietf.org/html/rfc6238#section-3) even though the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) permits the use of SHA-1, SHA-256 and SHA-512. Not all clients support other algorithms then SHA-1 #[cfg_attr(feature = "zeroize", zeroize(skip))] pub algorithm: Algorithm, /// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits pub digits: usize, /// Number of steps allowed as network delay. 1 would mean one step before current step and one step after are valids. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. Anything more is sketchy, and anyone recommending more is, by definition, ugly and stupid pub skew: 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: Vec, #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(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! pub issuer: Option, #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(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. pub account_name: String, } impl PartialEq for TOTP { /// Will not check for issuer and account_name equality /// As they aren't taken in account for token generation/token checking fn eq(&self, other: &Self) -> bool { if self.algorithm != other.algorithm { return false; } if self.digits != other.digits { return false; } if self.skew != other.skew { return false; } if self.step != other.step { return false; } constant_time_eq(self.secret.as_ref(), other.secret.as_ref()) } } #[cfg(feature = "otpauth")] impl core::fmt::Display for TOTP { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "digits: {}; step: {}; alg: {}; issuer: <{}>({})", self.digits, self.step, self.algorithm, self.issuer.clone().unwrap_or_else(|| "None".to_string()), self.account_name ) } } #[cfg(not(feature = "otpauth"))] impl core::fmt::Display for TOTP { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "digits: {}; step: {}; alg: {}", self.digits, self.step, self.algorithm, ) } } #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] // because `Default` is implemented regardless of `otpauth` feature we don't specify it here #[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))] impl Default for TOTP { fn default() -> Self { return TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap(), ) .unwrap(); } } #[cfg(all(feature = "gen_secret", feature = "otpauth"))] #[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))] impl Default for TOTP { fn default() -> Self { TOTP::new( Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap(), None, "".to_string(), ) .unwrap() } } impl TOTP { #[cfg(feature = "otpauth")] /// 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)` /// * `digits`: MUST be between 6 & 8 /// * `secret`: Must have bitsize of at least 128 /// * `account_name`: Must not contain `:` /// * `issuer`: Must not contain `:` /// /// # Example /// /// ```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(); /// ``` /// /// # Errors /// /// Will return an error if the `digit` or `secret` size is invalid or if `issuer` or `label` contain the character ':' pub fn new( algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec, issuer: Option, account_name: String, ) -> Result { crate::rfc::assert_digits(&digits)?; crate::rfc::assert_secret_length(secret.as_ref())?; if issuer.is_some() && issuer.as_ref().unwrap().contains(':') { return Err(TotpUrlError::Issuer(issuer.as_ref().unwrap().to_string())); } if account_name.contains(':') { return Err(TotpUrlError::AccountName(account_name)); } Ok(Self::new_unchecked( algorithm, digits, skew, step, secret, issuer, account_name, )) } #[cfg(feature = "otpauth")] /// 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. This is unchecked and does not check the `digits` and `secret` size /// /// # Description /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)` /// /// # Example /// /// ```rust /// use totp_rs::{Secret, TOTP, Algorithm}; /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string()); /// let totp = TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string()); /// ``` pub fn new_unchecked( algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec, issuer: Option, account_name: String, ) -> TOTP { TOTP { algorithm, digits, skew, step, secret, issuer, account_name, } } #[cfg(not(feature = "otpauth"))] /// 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)` /// * `digits`: MUST be between 6 & 8 /// * `secret`: Must have bitsize of at least 128 /// /// # Example /// /// ```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()).unwrap(); /// ``` /// /// # Errors /// /// Will return an error if the `digit` or `secret` size is invalid pub fn new( algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec, ) -> Result { crate::rfc::assert_digits(&digits)?; crate::rfc::assert_secret_length(secret.as_ref())?; Ok(Self::new_unchecked(algorithm, digits, skew, step, secret)) } #[cfg(not(feature = "otpauth"))] /// 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. This is unchecked and does not check the `digits` and `secret` size /// /// # Description /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)` /// /// # Example /// /// ```rust /// use totp_rs::{Secret, TOTP, Algorithm}; /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string()); /// let totp = TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap()); /// ``` pub fn new_unchecked( algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec, ) -> TOTP { TOTP { algorithm, digits, skew, step, secret, } } /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct /// /// # Errors /// /// Will return an error in case issuer or label contain the character ':' pub fn from_rfc6238(rfc: Rfc6238) -> Result { TOTP::try_from(rfc) } /// Will sign the given timestamp pub fn sign(&self, time: u64) -> Vec { self.algorithm.sign( self.secret.as_ref(), (time / self.step).to_be_bytes().as_ref(), ) } /// Will generate a token given the provided timestamp in seconds pub fn generate(&self, time: u64) -> String { let result: &[u8] = &self.sign(time); let offset = (result.last().unwrap() & 15) as usize; #[allow(unused_mut)] let mut result = u32::from_be_bytes(result[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff; match self.algorithm { Algorithm::SHA1 | Algorithm::SHA256 | Algorithm::SHA512 => format!( "{1:00$}", self.digits, result % 10_u32.pow(self.digits as u32) ), #[cfg(feature = "steam")] Algorithm::Steam => (0..self.digits) .map(|_| { let c = STEAM_CHARS .chars() .nth(result as usize % STEAM_CHARS.len()) .unwrap(); result /= STEAM_CHARS.len() as u32; c }) .collect(), } } /// Returns the timestamp of the first second for the next step /// given the provided timestamp in seconds pub fn next_step(&self, time: u64) -> u64 { let step = time / self.step; (step + 1) * self.step } /// Returns the timestamp of the first second of the next step /// According to system time pub fn next_step_current(&self) -> Result { let t = system_time()?; Ok(self.next_step(t)) } /// Give the ttl (in seconds) of the current token pub fn ttl(&self) -> Result { let t = system_time()?; Ok(self.step - (t % self.step)) } /// Generate a token from the current system time pub fn generate_current(&self) -> Result { let t = system_time()?; Ok(self.generate(t)) } /// Will check if token is valid given the provided timestamp in seconds, accounting [skew](struct.TOTP.html#structfield.skew) pub fn check(&self, token: &str, time: u64) -> bool { let basestep = time / self.step - (self.skew as u64); for i in 0..(self.skew as u16) * 2 + 1 { let step_time = (basestep + (i as u64)) * self.step; if constant_time_eq(self.generate(step_time).as_bytes(), token.as_bytes()) { return true; } } false } /// Will check if token is valid by current system time, accounting [skew](struct.TOTP.html#structfield.skew) pub fn check_current(&self, token: &str) -> Result { let t = system_time()?; Ok(self.check(token, t)) } /// Will return the base32 representation of the secret, which might be useful when users want to manually add the secret to their authenticator pub fn get_secret_base32(&self) -> String { base32::encode( base32::Alphabet::RFC4648 { padding: false }, self.secret.as_ref(), ) } /// Generate a TOTP from the standard otpauth URL #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))] pub fn from_url>(url: S) -> Result { let (algorithm, digits, skew, step, secret, issuer, account_name) = Self::parts_from_url(url)?; TOTP::new(algorithm, digits, skew, step, secret, issuer, account_name) } /// Generate a TOTP from the standard otpauth URL, using `TOTP::new_unchecked` internally #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))] pub fn from_url_unchecked>(url: S) -> Result { let (algorithm, digits, skew, step, secret, issuer, account_name) = Self::parts_from_url(url)?; Ok(TOTP::new_unchecked( algorithm, digits, skew, step, secret, issuer, account_name, )) } /// Parse the TOTP parts from the standard otpauth URL #[cfg(feature = "otpauth")] fn parts_from_url>( url: S, ) -> Result<(Algorithm, usize, u8, u64, Vec, Option, String), TotpUrlError> { let mut algorithm = Algorithm::SHA1; let mut digits = 6; let mut step = 30; let mut secret = Vec::new(); let mut issuer: Option = None; let mut account_name: String; let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?; if url.scheme() != "otpauth" { return Err(TotpUrlError::Scheme(url.scheme().to_string())); } match url.host() { Some(Host::Domain("totp")) => {} #[cfg(feature = "steam")] Some(Host::Domain("steam")) => { algorithm = Algorithm::Steam; } _ => { return Err(TotpUrlError::Host(url.host().unwrap().to_string())); } } let path = url.path().trim_start_matches('/'); let path = urlencoding::decode(path) .map_err(|_| TotpUrlError::AccountNameDecoding(path.to_string()))? .to_string(); if path.contains(':') { let parts = path.split_once(':').unwrap(); issuer = Some(parts.0.to_owned()); account_name = parts.1.to_owned(); } else { account_name = path; } account_name = urlencoding::decode(account_name.as_str()) .map_err(|_| TotpUrlError::AccountName(account_name.to_string()))? .to_string(); for (key, value) in url.query_pairs() { match key.as_ref() { #[cfg(feature = "steam")] "algorithm" if algorithm == Algorithm::Steam => { // Do not change used algorithm if this is Steam } "algorithm" => { algorithm = match value.as_ref() { "SHA1" => Algorithm::SHA1, "SHA256" => Algorithm::SHA256, "SHA512" => Algorithm::SHA512, _ => return Err(TotpUrlError::Algorithm(value.to_string())), } } "digits" => { digits = value .parse::() .map_err(|_| TotpUrlError::Digits(value.to_string()))?; } "period" => { step = value .parse::() .map_err(|_| TotpUrlError::Step(value.to_string()))?; } "secret" => { secret = base32::decode( base32::Alphabet::RFC4648 { padding: false }, value.as_ref(), ) .ok_or_else(|| TotpUrlError::Secret(value.to_string()))?; } #[cfg(feature = "steam")] "issuer" if value.to_lowercase() == "steam" => { algorithm = Algorithm::Steam; digits = 5; issuer = Some(value.into()); } "issuer" => { let param_issuer: String = value.into(); if issuer.is_some() && param_issuer.as_str() != issuer.as_ref().unwrap() { return Err(TotpUrlError::IssuerMistmatch( issuer.as_ref().unwrap().to_string(), param_issuer, )); } issuer = Some(param_issuer); #[cfg(feature = "steam")] if issuer == Some("Steam".into()) { algorithm = Algorithm::Steam; } } _ => {} } } #[cfg(feature = "steam")] if algorithm == Algorithm::Steam { digits = 5; step = 30; issuer = Some("Steam".into()); } if secret.is_empty() { return Err(TotpUrlError::Secret("".to_string())); } Ok((algorithm, digits, 1, step, secret, issuer, account_name)) } /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes /// /// Label and issuer will be URL-encoded if needed be /// Secret will be base 32'd without padding, as per RFC. #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))] pub fn get_url(&self) -> String { #[allow(unused_mut)] let mut host = "totp"; #[cfg(feature = "steam")] if self.algorithm == Algorithm::Steam { host = "steam"; } let account_name = urlencoding::encode(self.account_name.as_str()).to_string(); let mut params = vec![format!("secret={}", self.get_secret_base32())]; if self.digits != 6 { params.push(format!("digits={}", self.digits)); } if self.algorithm != Algorithm::SHA1 { params.push(format!("algorithm={}", self.algorithm)); } let label = if let Some(issuer) = &self.issuer { let issuer = urlencoding::encode(issuer); params.push(format!("issuer={}", issuer)); format!("{}:{}", issuer, account_name) } else { account_name }; if self.step != 30 { params.push(format!("period={}", self.step)); } format!("otpauth://{}/{}?{}", host, label, params.join("&")) } } #[cfg(feature = "qr")] #[cfg_attr(docsrs, doc(cfg(feature = "qr")))] impl TOTP { #[deprecated( since = "5.3.0", note = "get_qr was forcing the use of png as a base64. Use get_qr_base64 or get_qr_png instead. Will disappear in 6.0." )] pub fn get_qr(&self) -> Result { let url = self.get_url(); qrcodegen_image::draw_base64(&url) } /// Will return a qrcode to automatically add a TOTP as a base64 string. Needs feature `qr` to be enabled! /// Result will be in the form of a string containing a base64-encoded png, which you can embed in HTML without needing /// To store the png as a file. /// /// # Errors /// /// This will return an error in case the URL gets too long to encode into a QR code. /// This would require the get_url method to generate an url bigger than 2000 characters, /// Which would be too long for some browsers anyway. /// /// It will also return an error in case it can't encode the qr into a png. /// This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly pub fn get_qr_base64(&self) -> Result { let url = self.get_url(); qrcodegen_image::draw_base64(&url) } /// Will return a qrcode to automatically add a TOTP as a byte array. Needs feature `qr` to be enabled! /// Result will be in the form of a png file as bytes. /// /// # Errors /// /// This will return an error in case the URL gets too long to encode into a QR code. /// This would require the get_url method to generate an url bigger than 2000 characters, /// Which would be too long for some browsers anyway. /// /// It will also return an error in case it can't encode the qr into a png. /// This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly pub fn get_qr_png(&self) -> Result, String> { let url = self.get_url(); qrcodegen_image::draw_png(&url) } } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(feature = "gen_secret")] fn default_values() { let totp = TOTP::default(); assert_eq!(totp.algorithm, Algorithm::SHA1); assert_eq!(totp.digits, 6); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 30) } #[test] #[cfg(feature = "otpauth")] fn new_wrong_issuer() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github:".to_string()), "constantoine@github.com".to_string(), ); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::Issuer(_))); } #[test] #[cfg(feature = "otpauth")] fn new_wrong_account_name() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine:github.com".to_string(), ); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] #[cfg(feature = "otpauth")] fn new_wrong_account_name_no_issuer() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), None, "constantoine:github.com".to_string(), ); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] #[cfg(feature = "otpauth")] fn comparison_ok() { let reference = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let test = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); assert_eq!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_algo() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_digits() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_skew() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_step() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_secret() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret".into()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha1_without_issuer() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), None, "constantoine@github.com".to_string(), ) .unwrap(); let url = totp.get_url(); assert_eq!( url.as_str(), "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha1() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&issuer=Github"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha256() { let totp = TOTP::new( Algorithm::SHA256, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA256&issuer=Github"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha512() { let totp = TOTP::new( Algorithm::SHA512, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA512&issuer=Github"); } #[test] #[cfg(all(feature = "otpauth", feature = "gen_secret"))] fn ttl() { let secret = Secret::default(); let totp_rfc = Rfc6238::with_defaults(secret.to_bytes().unwrap()).unwrap(); let totp = TOTP::from_rfc6238(totp_rfc); assert!(totp.is_ok()); } #[test] #[cfg(feature = "otpauth")] fn ttl_ok() { let totp = TOTP::new( Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); assert!(totp.ttl().is_ok()); } #[test] #[cfg(not(feature = "otpauth"))] fn returns_base32() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_eq!( totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ); } #[test] #[cfg(not(feature = "otpauth"))] fn generate_token() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_eq!(totp.generate(1000).as_str(), "659761"); } #[test] #[cfg(not(feature = "otpauth"))] fn generate_token_current() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); let time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); assert_eq!( totp.generate(time).as_str(), totp.generate_current().unwrap() ); } #[test] #[cfg(not(feature = "otpauth"))] fn generates_token_sha256() { let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_eq!(totp.generate(1000).as_str(), "076417"); } #[test] #[cfg(not(feature = "otpauth"))] fn generates_token_sha512() { let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_eq!(totp.generate(1000).as_str(), "473536"); } #[test] #[cfg(not(feature = "otpauth"))] fn checks_token() { let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap(); assert!(totp.check("659761", 1000)); } #[test] #[cfg(not(feature = "otpauth"))] fn checks_token_big_skew() { let totp = TOTP::new(Algorithm::SHA1, 6, 255, 1, "TestSecretSuperSecret".into()).unwrap(); assert!(totp.check("659761", 1000)); } #[test] #[cfg(not(feature = "otpauth"))] fn checks_token_current() { let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap(); assert!(totp .check_current(&totp.generate_current().unwrap()) .unwrap()); assert!(!totp.check_current("bogus").unwrap()); } #[test] #[cfg(not(feature = "otpauth"))] fn checks_token_with_skew() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert!( totp.check("174269", 1000) && totp.check("659761", 1000) && totp.check("260393", 1000) ); } #[test] #[cfg(not(feature = "otpauth"))] fn next_step() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap(); assert!(totp.next_step(0) == 30); assert!(totp.next_step(29) == 30); assert!(totp.next_step(30) == 60); } #[test] #[cfg(not(feature = "otpauth"))] fn next_step_current() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap(); let t = system_time().unwrap(); assert!(totp.next_step_current().unwrap() == totp.next_step(t)); } #[test] #[cfg(feature = "otpauth")] fn from_url_err() { assert!(TOTP::from_url("otpauth://hotp/123").is_err()); assert!(TOTP::from_url("otpauth://totp/GitHub:test").is_err()); assert!(TOTP::from_url( "otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256" ) .is_err()); assert!(TOTP::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err()) } #[test] #[cfg(feature = "otpauth")] fn from_url_default() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ") .unwrap(); assert_eq!( totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); assert_eq!(totp.algorithm, Algorithm::SHA1); assert_eq!(totp.digits, 6); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 30); } #[test] #[cfg(feature = "otpauth")] fn from_url_query() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); assert_eq!(totp.algorithm, Algorithm::SHA256); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_sha512() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap(); assert_eq!( totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); assert_eq!(totp.algorithm, Algorithm::SHA512); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_to_url() { let totp = TOTP::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); } #[test] #[cfg(feature = "otpauth")] fn from_url_unknown_param() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar").unwrap(); assert_eq!( totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); assert_eq!(totp.algorithm, Algorithm::SHA256); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_issuer_special() { let totp = TOTP::from_url("otpauth://totp/Github%40:constantoine%40github.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github@".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); assert_eq!(totp.issuer.as_ref().unwrap(), "Github@"); } #[test] #[cfg(feature = "otpauth")] fn from_url_account_name_issuer() { let totp = TOTP::from_url("otpauth://totp/Github:constantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine".to_string(), ) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); assert_eq!(totp.account_name, "constantoine"); assert_eq!(totp.issuer.as_ref().unwrap(), "Github"); } #[test] #[cfg(feature = "otpauth")] fn from_url_account_name_issuer_encoded() { let totp = TOTP::from_url("otpauth://totp/Github%3Aconstantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine".to_string(), ) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); assert_eq!(totp.account_name, "constantoine"); assert_eq!(totp.issuer.as_ref().unwrap(), "Github"); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_issuer() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); assert_eq!(totp.algorithm, Algorithm::SHA256); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); assert_eq!(totp.issuer.as_ref().unwrap(), "GitHub"); } #[test] #[cfg(feature = "otpauth")] fn from_url_wrong_scheme() { let totp = TOTP::from_url("http://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); assert!(totp.is_err()); let err = totp.unwrap_err(); assert!(matches!(err, TotpUrlError::Scheme(_))); } #[test] #[cfg(feature = "otpauth")] fn from_url_wrong_algo() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=MD5"); assert!(totp.is_err()); let err = totp.unwrap_err(); assert!(matches!(err, TotpUrlError::Algorithm(_))); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_different_issuers() { let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); assert!(totp.is_err()); assert!(matches!( totp.unwrap_err(), TotpUrlError::IssuerMistmatch(_, _) )); } #[test] #[cfg(feature = "qr")] fn generates_qr() { use qrcodegen_image::qrcodegen; use sha2::{Digest, Sha512}; let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let url = totp.get_url(); let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium) .expect("could not generate qr"); let data = qrcodegen_image::draw_canvas(qr).into_raw(); // Create hash from image let hash_digest = Sha512::digest(data); assert_eq!( format!("{:x}", hash_digest).as_str(), "fbb0804f1e4f4c689d22292c52b95f0783b01b4319973c0c50dd28af23dbbbe663dce4eb05a7959086d9092341cb9f103ec5a9af4a973867944e34c063145328" ); } #[test] #[cfg(feature = "qr")] fn generates_qr_base64_ok() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let qr = totp.get_qr_base64(); assert!(qr.is_ok()); } #[test] #[cfg(feature = "qr")] fn generates_qr_png_ok() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); let qr = totp.get_qr_png(); assert!(qr.is_ok()); } } totp-rs-5.5.1/src/rfc.rs000064400000000000000000000274511046102023000131640ustar 00000000000000use crate::Algorithm; use crate::TotpUrlError; use crate::TOTP; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; /// Error returned when input is not compliant to [rfc-6238](https://tools.ietf.org/html/rfc6238). #[derive(Debug, Eq, PartialEq)] pub enum Rfc6238Error { /// Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. InvalidDigits(usize), /// The length of the shared secret MUST be at least 128 bits. SecretTooSmall(usize), } impl std::error::Error for Rfc6238Error {} impl std::fmt::Display for Rfc6238Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Rfc6238Error::InvalidDigits(digits) => write!( f, "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed", digits, ), Rfc6238Error::SecretTooSmall(bits) => write!( f, "The length of the shared secret MUST be at least 128 bits. {} bits is not enough", bits, ), } } } pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> { if !(&6..=&8).contains(&digits) { Err(Rfc6238Error::InvalidDigits(*digits)) } else { Ok(()) } } pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> { if secret.as_ref().len() < 16 { Err(Rfc6238Error::SecretTooSmall(secret.as_ref().len() * 8)) } else { Ok(()) } } /// [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options to create a [TOTP](struct.TOTP.html) /// /// # Example /// ``` /// use totp_rs::{Rfc6238, TOTP}; /// /// let mut rfc = Rfc6238::with_defaults( /// "totp-sercret-123".as_bytes().to_vec() /// ).unwrap(); /// /// // optional, set digits, issuer, account_name /// rfc.digits(8).unwrap(); /// /// let totp = TOTP::from_rfc6238(rfc).unwrap(); /// ``` #[derive(Debug, Clone)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Rfc6238 { /// SHA-1 algorithm: Algorithm, /// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits. digits: usize, /// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. skew: u8, /// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds. 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, #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(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, #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(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 { /// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html). /// /// # Errors /// /// 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, issuer: Option, account_name: String, ) -> Result { 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) -> Result { assert_digits(&digits)?; assert_secret_length(secret.as_ref())?; Ok(Rfc6238 { algorithm: Algorithm::SHA1, digits, skew: 1, step: 30, secret, }) } /// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html), /// with a default value of 6 for `digits`, None `issuer` and an empty account. /// /// # Errors /// /// 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) -> Result { Rfc6238::new(6, secret, Some("".to_string()), "".to_string()) } #[cfg(not(feature = "otpauth"))] pub fn with_defaults(secret: Vec) -> Result { Rfc6238::new(6, secret) } /// Set the `digits`. pub fn digits(&mut self, value: usize) -> Result<(), Rfc6238Error> { assert_digits(&value)?; self.digits = value; Ok(()) } #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))] /// Set the `issuer`. pub fn issuer(&mut self, value: String) { self.issuer = Some(value); } #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(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 for TOTP { type Error = TotpUrlError; /// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config. fn try_from(rfc: Rfc6238) -> Result { TOTP::new(rfc.algorithm, rfc.digits, rfc.skew, rfc.step, rfc.secret) } } #[cfg(feature = "otpauth")] impl TryFrom for TOTP { type Error = TotpUrlError; /// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config. fn try_from(rfc: Rfc6238) -> Result { TOTP::new( rfc.algorithm, rfc.digits, rfc.skew, rfc.step, rfc.secret, 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()); if !(6..=8).contains(&x) { assert!(rfc.is_err()); assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_))); } else { assert!(rfc.is_ok()); } } } #[test] #[cfg(not(feature = "otpauth"))] fn new_rfc_secret() { let mut secret = String::from(""); for _ in 0..=20 { secret = format!("{}{}", secret, "0"); let rfc = Rfc6238::new(6, secret.as_bytes().to_vec()); let rfc_default = Rfc6238::with_defaults(secret.as_bytes().to_vec()); if secret.len() < 16 { assert!(rfc.is_err()); assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_))); assert!(rfc_default.is_err()); assert!(matches!( rfc_default.unwrap_err(), Rfc6238Error::SecretTooSmall(_) )); } else { assert!(rfc.is_ok()); assert!(rfc_default.is_ok()); } } } #[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); assert!(totp.is_ok()); let otp = totp.unwrap(); assert_eq!(&otp.secret, GOOD_SECRET.as_bytes()); assert_eq!(otp.algorithm, crate::Algorithm::SHA1); assert_eq!(otp.digits, 8); assert_eq!(otp.skew, 1); assert_eq!(otp.step, 30) } #[test] #[cfg(not(feature = "otpauth"))] fn rfc_to_totp_ok_2() { let rfc = Rfc6238::with_defaults( Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) .to_bytes() .unwrap(), ) .unwrap(); let totp = TOTP::try_from(rfc); assert!(totp.is_ok()); let otp = totp.unwrap(); assert_eq!(otp.algorithm, crate::Algorithm::SHA1); assert_eq!(otp.digits, 6); assert_eq!(otp.skew, 1); assert_eq!(otp.step, 30) } #[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); assert!(fail.is_err()); assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_))); assert_eq!(rfc.digits, 6); let ok = rfc.digits(8); assert!(ok.is_ok()); assert_eq!(rfc.digits, 8) } #[test] #[cfg(not(feature = "otpauth"))] fn digits_error() { let error = crate::Rfc6238Error::InvalidDigits(9); assert_eq!( error.to_string(), "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 9 digits is not allowed".to_string() ) } #[test] #[cfg(not(feature = "otpauth"))] fn secret_length_error() { let error = Rfc6238Error::SecretTooSmall(120); assert_eq!( error.to_string(), "The length of the shared secret MUST be at least 128 bits. 120 bits is not enough" .to_string() ) } } totp-rs-5.5.1/src/secret.rs000064400000000000000000000204211046102023000136650ustar 00000000000000//! Representation of a secret either a "raw" \[u8\] or "base 32" encoded String //! //! # Examples //! //! - Create a TOTP from a "raw" secret //! ``` //! # #[cfg(not(feature = "otpauth"))] { //! 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(), //! ).unwrap(); //! //! println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); //! # } //! ``` //! //! - Create a TOTP from a base32 encoded secret //! ``` //! # #[cfg(not(feature = "otpauth"))] { //! 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(), //! ).unwrap(); //! //! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); //! # } //! //! ``` //! - Create a TOTP from a Generated Secret //! ``` //! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] { //! use totp_rs::{Secret, TOTP, Algorithm}; //! //! let secret_b32 = Secret::default(); //! let totp_b32 = TOTP::new( //! Algorithm::SHA1, //! 6, //! 1, //! 30, //! secret_b32.to_bytes().unwrap(), //! ).unwrap(); //! //! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); //! # } //! ``` //! - Create a TOTP from a Generated Secret 2 //! ``` //! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] { //! use totp_rs::{Secret, TOTP, Algorithm}; //! //! let secret_b32 = Secret::generate_secret(); //! let totp_b32 = TOTP::new( //! Algorithm::SHA1, //! 6, //! 1, //! 30, //! secret_b32.to_bytes().unwrap(), //! ).unwrap(); //! //! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); //! # } //! ``` use base32::{self, Alphabet}; use constant_time_eq::constant_time_eq; /// Different ways secret parsing failed. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SecretParseError { /// Invalid base32 input. ParseBase32, } impl std::error::Error for SecretParseError {} 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 {} /// Shared secret between client and server to validate token against/generate token from. #[derive(Debug, Clone, Eq)] #[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] pub enum Secret { /// Non-encoded "raw" secret. Raw(Vec), /// Base32 encoded secret. Encoded(String), } impl PartialEq for Secret { /// Will check that to_bytes() returns the same. /// One secret can be Raw, and the other Encoded. fn eq(&self, other: &Self) -> bool { constant_time_eq(&self.to_bytes().unwrap(), &other.to_bytes().unwrap()) } } #[cfg(feature = "gen_secret")] #[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))] impl Default for Secret { fn default() -> Self { Secret::generate_secret() } } impl Secret { /// Get the inner String value as a Vec of bytes. pub fn to_bytes(&self) -> Result, 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 { 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(), } } /// 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")] #[cfg_attr(docsrs, doc(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) => { for b in bytes { write!(f, "{:02x}", b)?; } Ok(()) } 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); 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); 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] fn secret_from_string() { let raw: Secret = Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()); let encoded: Secret = Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()); assert_eq!(raw.to_encoded(), encoded); assert_eq!(raw, encoded.to_raw().unwrap()); } #[test] #[cfg(feature = "gen_secret")] fn secret_gen_secret() { let sec = Secret::generate_secret(); assert!(matches!(sec, Secret::Raw(_))); assert_eq!(sec.to_bytes().unwrap().len(), 20); } #[test] #[cfg(feature = "gen_secret")] fn secret_gen_default() { let sec = Secret::default(); assert!(matches!(sec, Secret::Raw(_))); assert_eq!(sec.to_bytes().unwrap().len(), 20); } #[test] #[cfg(feature = "gen_secret")] fn secret_empty() { let non_ascii = vec![240, 159, 146, 150]; let sec = Secret::Encoded(std::str::from_utf8(&non_ascii).unwrap().to_owned()); let to_r = sec.to_raw(); assert!(to_r.is_err()); let to_b = sec.to_bytes(); assert!(to_b.is_err()); } } totp-rs-5.5.1/src/url_error.rs000064400000000000000000000166201046102023000144210ustar 00000000000000#[cfg(feature = "otpauth")] use url::ParseError; use crate::Rfc6238Error; /// Errors returned mostly upon decoding URL. #[derive(Debug, Eq, PartialEq)] pub enum TotpUrlError { /// Couldn't decode URL. #[cfg(feature = "otpauth")] #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))] Url(ParseError), /// Invalid scheme. Scheme(String), /// Invalid host. Host(String), /// Wrong base32 input. Secret(String), /// Invalid secret size. (Too short?) SecretSize(usize), /// Unknown algorithm. Algorithm(String), /// Characters should only be digits. Digits(String), /// Digits should be between 6 and 8. DigitsNumber(usize), /// Couldn't decode step into a number. Step(String), /// Issuer contains invalid character `:`. Issuer(String), /// Couldn't decode issuer. IssuerDecoding(String), /// Issuers should be the same. IssuerMistmatch(String, String), /// Account name contains invalid character `:` or couldn't be decoded. AccountName(String), /// Couldn't parse account name. AccountNameDecoding(String), } impl std::error::Error for TotpUrlError {} impl std::fmt::Display for TotpUrlError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TotpUrlError::AccountName(name) => write!( f, "Account Name can't contain a colon. \"{}\" contains a colon", name ), TotpUrlError::AccountNameDecoding(name) => write!( f, "Couldn't URL decode \"{}\"", name ), TotpUrlError::Algorithm(algo) => write!( f, "Algorithm can only be SHA1, SHA256 or SHA512, not \"{}\"", algo ), TotpUrlError::Digits(digits) => write!( f, "Could not parse \"{}\" as a number.", digits, ), TotpUrlError::DigitsNumber(digits) => write!( f, "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed", digits, ), TotpUrlError::Host(host) => write!( f, "Host should be totp, not \"{}\"", host ), TotpUrlError::Issuer(issuer) => write!( f, "Issuer can't contain a colon. \"{}\" contains a colon", issuer ), TotpUrlError::IssuerDecoding(issuer) => write!( f, "Couldn't URL decode \"{}\"", issuer ), TotpUrlError::IssuerMistmatch(path_issuer, issuer) => write!( f, "An issuer \"{}\" could be retrieved from the path, but a different issuer \"{}\" was found in the issuer URL parameter", path_issuer, issuer, ), TotpUrlError::Scheme(scheme) => write!( f, "Scheme should be otpauth, not \"{}\"", scheme ), TotpUrlError::Secret(secret) => write!( f, "Secret \"{}\" is not a valid non-padded base32 string", secret, ), TotpUrlError::SecretSize(bits) => write!( f, "The length of the shared secret MUST be at least 128 bits. {} bits is not enough", bits, ), TotpUrlError::Step(step) => write!( f, "Could not parse \"{}\" as a number.", step, ), #[cfg(feature = "otpauth")] TotpUrlError::Url(e) => write!( f, "Error parsing URL: {}", e ) } } } impl From for TotpUrlError { fn from(e: Rfc6238Error) -> Self { match e { Rfc6238Error::InvalidDigits(digits) => TotpUrlError::DigitsNumber(digits), Rfc6238Error::SecretTooSmall(bits) => TotpUrlError::SecretSize(bits), } } } #[cfg(test)] mod tests { use crate::TotpUrlError; #[test] fn account_name() { let error = TotpUrlError::AccountName("Laziz:".to_string()); assert_eq!( error.to_string(), "Account Name can't contain a colon. \"Laziz:\" contains a colon" ) } #[test] fn account_name_decoding() { let error = TotpUrlError::AccountNameDecoding("Laz&iz".to_string()); assert_eq!( error.to_string(), "Couldn't URL decode \"Laz&iz\"".to_string() ) } #[test] fn algorithm() { let error = TotpUrlError::Algorithm("SIKE".to_string()); assert_eq!( error.to_string(), "Algorithm can only be SHA1, SHA256 or SHA512, not \"SIKE\"".to_string() ) } #[test] fn digits() { let error = TotpUrlError::Digits("six".to_string()); assert_eq!( error.to_string(), "Could not parse \"six\" as a number.".to_string() ) } #[test] fn digits_number() { let error = TotpUrlError::DigitsNumber(5); assert_eq!(error.to_string(), "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 5 digits is not allowed".to_string()) } #[test] fn host() { let error = TotpUrlError::Host("hotp".to_string()); assert_eq!( error.to_string(), "Host should be totp, not \"hotp\"".to_string() ) } #[test] fn issuer() { let error = TotpUrlError::Issuer("Iss:uer".to_string()); assert_eq!( error.to_string(), "Issuer can't contain a colon. \"Iss:uer\" contains a colon".to_string() ) } #[test] fn issuer_decoding() { let error = TotpUrlError::IssuerDecoding("iss&uer".to_string()); assert_eq!( error.to_string(), "Couldn't URL decode \"iss&uer\"".to_string() ) } #[test] fn issuer_mismatch() { let error = TotpUrlError::IssuerMistmatch("Google".to_string(), "Github".to_string()); assert_eq!(error.to_string(), "An issuer \"Google\" could be retrieved from the path, but a different issuer \"Github\" was found in the issuer URL parameter".to_string()) } #[test] fn scheme() { let error = TotpUrlError::Scheme("https".to_string()); assert_eq!( error.to_string(), "Scheme should be otpauth, not \"https\"".to_string() ) } #[test] fn secret() { let error = TotpUrlError::Secret("YoLo".to_string()); assert_eq!( error.to_string(), "Secret \"YoLo\" is not a valid non-padded base32 string".to_string() ) } #[test] fn secret_size() { let error = TotpUrlError::SecretSize(112); assert_eq!( error.to_string(), "The length of the shared secret MUST be at least 128 bits. 112 bits is not enough" .to_string() ) } #[test] #[cfg(feature = "otpauth")] fn step() { let error = TotpUrlError::Url(url::ParseError::EmptyHost); assert_eq!( error.to_string(), "Error parsing URL: empty host".to_string() ) } }