totp-rs-3.0.1/.cargo_vcs_info.json0000644000000001360000000000100124450ustar { "git": { "sha1": "26416df28baf00e9cc7248b867d9a8eb824ccc47" }, "path_in_vcs": "" }totp-rs-3.0.1/.github/workflows/rust.yml000064400000000000000000000022561046102023000163570ustar 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: All features run: cargo test --all-features - name: No feature run: cargo test - name: otpauth feature run: cargo test --features=otpauth - name: gen_secret feature run: cargo test --features=gen_secret - name: otpauth+gensecret feature run: cargo test --features=gen_secret,otpauth coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: version: '0.19.0' args: --all-features --out=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: cobertura.xml totp-rs-3.0.1/.github/workflows/security.yml000064400000000000000000000004111046102023000172200ustar 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-3.0.1/.gitignore000064400000000000000000000000751046102023000132270ustar 00000000000000.idea /target Cargo.lock .vscode/settings.json cobertura.xml totp-rs-3.0.1/Cargo.lock0000644000000303150000000000100104220ustar # 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 = "adler32" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[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.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] [[package]] name = "bytemuck" version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[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.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e31aa570361918e61453e3b5377976b23e4599e8bb5b840380ecd3a20e691d2" [[package]] name = "cpufeatures" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" 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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", "typenum", ] [[package]] name = "deflate" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" dependencies = [ "adler32", ] [[package]] name = "digest" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", "subtle", ] [[package]] name = "form_urlencoded" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", ] [[package]] name = "generic-array" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 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.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", "unicode-normalization", ] [[package]] name = "image" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" dependencies = [ "bytemuck", "byteorder", "color_quant", "num-iter", "num-rational", "num-traits", "png", ] [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "matches" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "miniz_oxide" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-iter" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "png" version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ "bitflags", "crc32fast", "deflate", "miniz_oxide", ] [[package]] name = "ppv-lite86" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ "unicode-ident", ] [[package]] name = "qrcodegen" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" [[package]] name = "quote" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 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.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "serde" version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha-1" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "sha2" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" 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.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "totp-rs" version = "3.0.1" dependencies = [ "base32", "base64", "constant_time_eq", "hmac", "image", "qrcodegen", "rand", "serde", "sha-1", "sha2", "url", "urlencoding", ] [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] [[package]] name = "url" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", "matches", "percent-encoding", ] [[package]] name = "urlencoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" [[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" totp-rs-3.0.1/Cargo.toml0000644000000037370000000000100104550ustar # 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" name = "totp-rs" version = "3.0.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" resolver = "2" [package.metadata.docs.rs] features = [ "qr", "serde_support", "otpauth", ] [dependencies.base32] version = "~0.4" [dependencies.base64] version = "~0.13" optional = true [dependencies.constant_time_eq] version = "~0.2.1" [dependencies.hmac] version = "~0.12.1" [dependencies.image] version = "~0.24.2" features = ["png"] optional = true default-features = false [dependencies.qrcodegen] version = "~1.8" optional = true [dependencies.rand] version = "~0.8.5" features = [ "std_rng", "std", ] optional = true default-features = false [dependencies.serde] version = "1.0" features = ["derive"] optional = true [dependencies.sha-1] version = "~0.10.0" [dependencies.sha2] version = "~0.10.2" [dependencies.url] version = "^2.2.2" optional = true [dependencies.urlencoding] version = "^2.1.0" optional = true [features] default = [] gen_secret = ["rand"] otpauth = [ "url", "urlencoding", ] qr = [ "qrcodegen", "image", "base64", "otpauth", ] serde_support = ["serde"] totp-rs-3.0.1/Cargo.toml.orig000064400000000000000000000024001046102023000141200ustar 00000000000000[package] name = "totp-rs" version = "3.0.1" authors = ["Cleo Rebert "] 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"] [package.metadata.docs.rs] features = [ "qr", "serde_support", "otpauth" ] [features] default = [] otpauth = ["url", "urlencoding"] qr = ["qrcodegen", "image", "base64", "otpauth"] serde_support = ["serde"] gen_secret = ["rand"] [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } sha2 = "~0.10.2" sha-1 = "~0.10.0" hmac = "~0.12.1" base32 = "~0.4" urlencoding = { version = "^2.1.0", optional = true} url = { version = "^2.2.2", optional = true } constant_time_eq = "~0.2.1" qrcodegen = { version = "~1.8", optional = true } image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false} base64 = { version = "~0.13", optional = true } rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false }totp-rs-3.0.1/LICENSE000064400000000000000000000020621046102023000122420ustar 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-3.0.1/README.md000064400000000000000000000142361046102023000125220ustar 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. # Examples ## Summarry 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) ### 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 = "^3.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 = "^3.0" 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 code = totp.get_qr()?; println!("{}", code); } ``` ### With serde support --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^3.0" features = ["serde_support"] ``` ### With otpauth url support --- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] version = "^3.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 = "^3.0" 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 code = totp.get_qr()?; println!("{}", 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 code = totp.get_qr()?; println!("{}", 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(); println!("code: {}", code); } ```totp-rs-3.0.1/examples/gen_secret.rs000064400000000000000000000011751046102023000155430ustar 00000000000000#[cfg(all(feature = "gen_secret", feature = "otpauth"))] use totp_rs::{Secret, TOTP, Algorithm}; #[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-3.0.1/examples/rfc-6238.rs000064400000000000000000000015341046102023000145760ustar 00000000000000use totp_rs::{Rfc6238, TOTP}; #[cfg(feature = "otpauth")] fn main () { let mut rfc = Rfc6238::with_defaults( "totp-sercret-123" ).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" ).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-3.0.1/examples/secret.rs000064400000000000000000000043561046102023000147160ustar 00000000000000use totp_rs::{Secret, TOTP, Algorithm}; #[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-3.0.1/examples/ttl.rs000064400000000000000000000020241046102023000142220ustar 00000000000000use totp_rs::{Algorithm, TOTP}; #[cfg(not(feature = "otpauth"))] fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, "my-secret".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)); } } #[cfg(feature = "otpauth")] fn main() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, "my-secret".to_string(), 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-3.0.1/src/lib.rs000064400000000000000000000761151046102023000131520ustar 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", //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); //! let url = totp.get_url(); //! println!("{}", url); //! let code = totp.get_qr().unwrap(); //! println!("{}", code); //! # } //! ``` mod secret; mod rfc; mod url_error; pub use secret::{Secret, SecretParseError}; pub use url_error::TotpUrlError; pub use rfc::{Rfc6238, Rfc6238Error}; use base32; use constant_time_eq::constant_time_eq; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; use core::fmt; #[cfg(feature = "qr")] use {base64, image::Luma, qrcodegen}; #[cfg(feature = "otpauth")] use url::{Host, Url}; #[cfg(feature = "otpauth")] use urlencoding; use hmac::Mac; use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; type HmacSha1 = hmac::Hmac; type HmacSha256 = hmac::Hmac; type HmacSha512 = hmac::Hmac; /// 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, } 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"), } } } 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), } } } 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))] 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 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: T, #[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")] /// 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(all(feature = "gen_secret", not(feature = "otpauth")))] 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"))] impl Default for TOTP { fn default() -> Self { return 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)` /// /// ```rust /// use totp_rs::{Secret, TOTP, Algorithm}; /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string()); /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string()).unwrap(); /// ``` /// * `digits`: MUST be between 6 & 8 /// * `secret`: Must have bitsize of at least 128 /// * `account_name`: Must not contain `:` /// * `issuer`: Must not contain `:` /// /// # Errors /// /// Will return an error in case issuer or label contain the character ':' pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: T, issuer: Option, account_name: String) -> Result, TotpUrlError> { 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(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)` /// /// ```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(); /// ``` /// * `digits`: MUST be between 6 & 8 /// * `secret`: Must have bitsize of at least 128 /// /// # Errors /// /// Will return an error in case issuer or label contain the character ':' pub fn new(algorithm: Algorithm, digits: usize,skew: u8, step: u64, secret: T) -> Result, TotpUrlError> { crate::rfc::assert_digits(&digits)?; crate::rfc::assert_secret_length(secret.as_ref())?; Ok(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, TotpUrlError> { 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; let result = u32::from_be_bytes(result[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff; format!( "{1:00$}", self.digits, result % 10_u32.pow(self.digits as u32) ) } /// 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 * 2 + 1 { let step_time = (basestep + (i as u64)) * (self.step as u64); 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")] pub fn from_url>(url: S) -> Result>, TotpUrlError> { let url = Url::parse(url.as_ref()).map_err(|err| TotpUrlError::Url(err))?; if url.scheme() != "otpauth" { return Err(TotpUrlError::Scheme(url.scheme().to_string())); } if url.host() != Some(Host::Domain("totp")) { return Err(TotpUrlError::Host(url.host().unwrap().to_string())); } 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 path = url.path().trim_start_matches('/'); if path.contains(':') { let parts = path.split_once(':').unwrap(); issuer = Some(urlencoding::decode(parts.0.to_owned().as_str()).map_err(|_| TotpUrlError::IssuerDecoding(parts.0.to_owned().to_string()))?.to_string()); account_name = parts.1.trim_start_matches(':').to_owned(); } else { account_name = path.to_owned(); } 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() { "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(TotpUrlError::Secret(value.to_string()))?; } "issuer" => { let param_issuer = value.parse::().map_err(|_| TotpUrlError::Issuer(value.to_string()))?; 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); } _ => {} } } if secret.is_empty() { return Err(TotpUrlError::Secret("".to_string())); } TOTP::new(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")] pub fn get_url(&self) -> String { let label: String; let account_name: String = urlencoding::encode(self.account_name.as_str()).to_string(); if self.issuer.is_some() { let issuer: String = urlencoding::encode(self.issuer.as_ref().unwrap().as_str()).to_string(); label = format!("{0}:{1}?issuer={0}&", issuer, account_name); } else { label = format!("{}?", account_name); } format!( "otpauth://totp/{}secret={}&digits={}&algorithm={}", label, self.get_secret_base32(), self.digits.to_string(), self.algorithm, ) } /// 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 #[cfg(feature = "qr")] pub fn get_qr(&self) -> Result> { use image::ImageEncoder; let url = self.get_url(); let mut vec = Vec::new(); let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?; let size = qr.size() as u32; // "+ 8 * 8" is here to add padding (the white border around the QRCode) // As some QRCode readers don't work without padding let image_size = size * 8 + 8 * 8; let mut canvas = image::GrayImage::new(image_size, image_size); // Draw the border for x in 0..image_size { for y in 0..image_size { if (y < 8*4 || y >= image_size - 8*4) || (x < 8*4 || x >= image_size - 8*4) { canvas.put_pixel(x, y, Luma([255])); } } } // The QR inside the white border for x_qr in 0..size { for y_qr in 0..size { // The canvas is a grayscale image without alpha. Hence it's only one 8-bits byte longs // This clever trick to one-line the value was achieved with advanced mathematics // And deep understanding of Boolean algebra. let val = !qr.get_module(x_qr as i32, y_qr as i32) as u8 * 255; // Multiply coordinates by width of pixels // And take into account the 8*4 padding on top and left side let x_start = x_qr * 8 + 8*4; let y_start = y_qr * 8 + 8*4; // Draw a 8-pixels-wide square for x_img in x_start..x_start + 8 { for y_img in y_start..y_start + 8 { canvas.put_pixel( x_img, y_img, Luma([val]), ); } } } } // Encode the canvas into a PNG let encoder = image::codecs::png::PngEncoder::new(&mut vec); encoder.write_image( &image::ImageBuffer::from(canvas).into_raw(), image_size, image_size, image::ColorType::L8, )?; Ok(base64::encode(vec)) } } #[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", 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", 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", 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", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", 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").unwrap(); let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_digits() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_skew() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_step() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] #[cfg(not(feature = "otpauth"))] fn comparison_different_secret() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret").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, 1, "TestSecretSuperSecret", None, "constantoine@github.com".to_string()).unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha1() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", 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?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha256() { let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", 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?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA256"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha512() { let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret", 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?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA512"); } #[test] #[cfg(not(feature = "otpauth"))] fn returns_base32() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").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").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").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").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").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").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").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").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").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").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_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, 1, "TestSecretSuperSecret", 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_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, 1, "TestSecretSuperSecret", Some("Github@".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); assert_eq!(totp.issuer.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.unwrap(), "GitHub"); } #[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 sha1::{Digest, Sha1}; let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let qr = totp.get_qr().unwrap(); // Create hash from image let hash_digest = Sha1::digest(qr.as_bytes()); assert_eq!( format!("{:x}", hash_digest).as_str(), "3028f00bf1bd2898ce4d73b234ba087d3c5172f9" ); } } totp-rs-3.0.1/src/rfc.rs000064400000000000000000000246621046102023000131560ustar 00000000000000use crate::Algorithm; use crate::TotpUrlError; use crate::TOTP; #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; /// Data 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::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" /// ).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: T, #[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")] /// 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: T, issuer: Option, account_name: String, ) -> Result, Rfc6238Error> { assert_digits(&digits)?; assert_secret_length(secret.as_ref())?; Ok(Rfc6238 { algorithm: Algorithm::SHA1, digits, skew: 1, step: 30, secret, issuer, account_name, }) } #[cfg(not(feature = "otpauth"))] pub fn new( digits: usize, secret: T, ) -> Result, Rfc6238Error> { 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: T) -> Result, Rfc6238Error> { Rfc6238::new(6, secret, Some("".to_string()), "".to_string()) } #[cfg(not(feature = "otpauth"))] pub fn with_defaults(secret: T) -> Result, Rfc6238Error> { 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")] /// Set the `issuer` pub fn issuer(&mut self, value: String) { self.issuer = Some(value); } #[cfg(feature = "otpauth")] /// Set the `account_name` pub fn account_name(&mut self, value: String) { self.account_name = value; } } #[cfg(not(feature = "otpauth"))] impl> TryFrom> 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.to_string(), ); 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.clone(), ); let rfc_default = Rfc6238::with_defaults(secret.clone()); 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.to_string(), ) .unwrap(); let totp = TOTP::try_from(rfc); assert!(totp.is_ok()); let otp = totp.unwrap(); assert_eq!(&otp.secret, GOOD_SECRET); 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.to_string(), 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.to_string(), ISSUER.map(str::to_string), ACCOUNT.to_string(), ) .unwrap(); let totp = TOTP::try_from(rfc); assert!(!totp.is_err()); } #[test] #[cfg(not(feature = "otpauth"))] fn rfc_with_default_set_values() { let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.to_string()).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) } } totp-rs-3.0.1/src/secret.rs000064400000000000000000000163251046102023000136660ustar 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 std::string::FromUtf8Error; use base32::{self, Alphabet}; use constant_time_eq::constant_time_eq; #[derive(Debug, Clone, PartialEq, Eq)] pub enum SecretParseError { ParseBase32, Utf8Error(FromUtf8Error), } #[derive(Debug, Clone, Eq)] pub enum Secret { /// represent a non-encoded "raw" secret Raw(Vec), /// represent a 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")] impl Default for Secret { fn default() -> Self { return 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(), } } /// ⚠️ requires feature `gen_secret` /// /// Generate a CSPRNG binary value of 160 bits, /// the recomended size from [rfc-4226](https://www.rfc-editor.org/rfc/rfc4226#section-4) /// /// > The length of the shared secret MUST be at least 128 bits. /// > This document RECOMMENDs a shared secret length of 160 bits. /// /// ⚠️ The generated secret is not guaranteed to be a valid UTF-8 sequence #[cfg(feature = "gen_secret")] pub fn generate_secret() -> Secret { use rand::Rng; let mut rng = rand::thread_rng(); let mut secret: [u8; 20] = Default::default(); rng.fill(&mut secret[..]); Secret::Raw(secret.to_vec()) } } impl std::fmt::Display for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Secret::Raw(bytes) => { let mut s: String = String::new(); for b in bytes { s = format!("{}{:02x}", &s, &b); } write!(f, "{}", s) }, Secret::Encoded(s) => write!(f, "{}", s), } } } #[cfg(test)] mod tests { use super::Secret; const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"; const BYTES: [u8; 23] = [ 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, ]; const BYTES_DISPLAY: &str = "706c61696e2d737472696e672d7365637265742d313233"; #[test] fn secret_display() { let base32_str = String::from(BASE32); let secret_raw = Secret::Raw(BYTES.to_vec()); let secret_base32 = Secret::Encoded(base32_str); 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() { match Secret::generate_secret() { Secret::Raw(secret) => assert_eq!(secret.len(), 20), Secret::Encoded(_) => panic!("should be raw"), } } } totp-rs-3.0.1/src/url_error.rs000064400000000000000000000145471046102023000144200ustar 00000000000000#[cfg(feature = "otpauth")] use url::ParseError; use crate::Rfc6238Error; #[derive(Debug, Eq, PartialEq)] pub enum TotpUrlError { #[cfg(feature = "otpauth")] Url(ParseError), Scheme(String), Host(String), Secret(String), SecretSize(usize), Algorithm(String), Digits(String), DigitsNumber(usize), Step(String), Issuer(String), IssuerDecoding(String), IssuerMistmatch(String, String), AccountName(String), AccountNameDecoding(String), } 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()) } }