├── .gitignore ├── .travis.yml ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── examples ├── README.md ├── otp.rs ├── otp_async.rs ├── otp_custom.rs └── otp_with_proxy.rs └── src ├── async_verifier.rs ├── config.rs ├── lib.rs ├── sec.rs ├── sync_verifier.rs └── yubicoerror.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | *.fmt 5 | *.iml 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | allow_failures: 6 | - nightly 7 | os: 8 | - linux 9 | before_install: 10 | - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get -qq update; else brew update; fi 11 | - if [ $TRAVIS_OS_NAME = linux ]; then sudo apt-get install -y libusb-1.0-0-dev; else brew install libusb; fi 12 | script: 13 | - cargo update --verbose 14 | - cargo build --all --all-features --verbose 15 | - cargo test --verbose -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yubico" 3 | version = "0.11.0" 4 | authors = ["Flavio Oliveira ", "Pierre Larger "] 5 | edition = "2018" 6 | 7 | description = "Yubikey client API library" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["yubikey", "authentication", "encryption", "OTP", "Challenge-Response"] 10 | categories = ["authentication"] 11 | repository = "https://github.com/wisespace-io/yubico-rs" 12 | readme = "README.md" 13 | 14 | [badges] 15 | travis-ci = { repository = "wisespace-io/yubico-rs" } 16 | 17 | [lib] 18 | name = "yubico" 19 | path = "src/lib.rs" 20 | 21 | [dependencies] 22 | base64 = "0.13" 23 | futures = { version = "0.3", optional = true } 24 | hmac = "0.12" 25 | rand = "0.8" 26 | reqwest = { version = "0.11", features = ["blocking"], default-features = false } 27 | sha1 = "0.10" 28 | threadpool = "1.7" 29 | form_urlencoded = "1" 30 | 31 | [dev-dependencies] 32 | tokio = { version = "1.1", features = ["macros"] } 33 | futures = "0.3" 34 | 35 | [features] 36 | default = ["online-tokio", "native-tls"] 37 | online-tokio = ["futures"] 38 | rustls-tls = ["reqwest/rustls-tls"] 39 | native-tls = ["reqwest/native-tls"] 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine as base 2 | RUN apk update \ 3 | && apk add \ 4 | git \ 5 | gcc \ 6 | g++ \ 7 | openssl \ 8 | openssl-dev \ 9 | pkgconfig 10 | 11 | COPY . /src 12 | 13 | RUN rustup update 1.64 && rustup default 1.64 14 | 15 | RUN cd /src && \ 16 | RUSTFLAGS="-C target-feature=-crt-static" cargo build --release --example otp 17 | 18 | FROM alpine as tool 19 | 20 | RUN apk update && \ 21 | apk add \ 22 | libgcc \ 23 | pcsc-lite-dev 24 | 25 | COPY --from=base /src/target/release/examples/otp /usr/local/bin 26 | ENTRYPOINT [ "otp" ] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Flavio Oliveira 2 | Copyright (c) 2018 Pierre-Étienne Meunier 3 | 4 | Licensed under either of 5 | 6 | * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 7 | * MIT license (http://opensource.org/licenses/MIT) 8 | 9 | at your option. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yubico   [![Build Status]][travis] [![Latest Version]][crates.io] [![MIT licensed]][MIT] [![Apache-2.0 licensed]][APACHE] 2 | 3 | [Build Status]: https://travis-ci.org/wisespace-io/yubico-rs.png?branch=master 4 | [travis]: https://travis-ci.org/wisespace-io/yubico-rs 5 | [Latest Version]: https://img.shields.io/crates/v/yubico.svg 6 | [crates.io]: https://crates.io/crates/yubico 7 | [MIT licensed]: https://img.shields.io/badge/License-MIT-blue.svg 8 | [MIT]: ./LICENSE-MIT 9 | [Apache-2.0 licensed]: https://img.shields.io/badge/License-Apache%202.0-blue.svg 10 | [APACHE]: ./LICENSE-APACHE 11 | 12 | **Enables integration with the Yubico validation platform, so you can use Yubikey's one-time-password in your Rust application, allowing a user to authenticate via Yubikey.** 13 | 14 | --- 15 | 16 | ## Current features 17 | 18 | - [X] Synchronous Yubikey client API library, [validation protocol version 2.0](https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html). 19 | - [X] Asynchronous Yubikey client API library relying on [Tokio](https://github.com/tokio-rs/tokio) 20 | 21 | **Note:** The USB-related features have been moved to a sepatated repository, [yubico-manager](https://github.com/wisespace-io/yubico-manager) 22 | 23 | ## Usage 24 | 25 | Add this to your Cargo.toml 26 | 27 | ```toml 28 | [dependencies] 29 | yubico = "0.9" 30 | ``` 31 | 32 | The following are a list of Cargo features that can be enabled or disabled: 33 | 34 | - online-tokio (enabled by default): Provides integration to Tokio using futures. 35 | 36 | You can enable or disable them using the example below: 37 | 38 | ```toml 39 | [dependencies.yubico] 40 | version = "0.9" 41 | # don't include the default features (online-tokio) 42 | default-features = false 43 | # cherry-pick individual features 44 | features = [] 45 | ``` 46 | 47 | [Request your api key](https://upgrade.yubico.com/getapikey/). 48 | 49 | ### OTP with Default Servers 50 | 51 | ```rust 52 | extern crate yubico; 53 | 54 | use yubico::config::*; 55 | use yubico::verify; 56 | 57 | fn main() { 58 | let config = Config::default() 59 | .set_client_id("CLIENT_ID") 60 | .set_key("API_KEY"); 61 | 62 | match verify("OTP", config) { 63 | Ok(answer) => println!("{}", answer), 64 | Err(e) => println!("Error: {}", e), 65 | } 66 | } 67 | ``` 68 | 69 | ## OTP with custom API servers 70 | 71 | ```rust 72 | extern crate yubico; 73 | 74 | use yubico::verify; 75 | use yubico::config::*; 76 | 77 | fn main() { 78 | let config = Config::default() 79 | .set_client_id("CLIENT_ID") 80 | .set_key("API_KEY") 81 | .set_api_hosts(vec!["https://api.example.com/verify".into()]); 82 | 83 | match verify("OTP", config) { 84 | Ok(answer) => println!("{}", answer), 85 | Err(e) => println!("Error: {}", e), 86 | } 87 | } 88 | ``` 89 | 90 | ### Asynchronous OTP validation 91 | 92 | ```rust 93 | #![recursion_limit="128"] 94 | extern crate futures; 95 | extern crate tokio; 96 | extern crate yubico; 97 | 98 | use futures::future::Future; 99 | use yubico::verify_async; 100 | extern crate yubico; 101 | 102 | use std::io::stdin; 103 | use yubico::config::Config; 104 | 105 | fn main() { 106 | println!("Please plug in a yubikey and enter an OTP"); 107 | 108 | let client_id = std::env::var("YK_CLIENT_ID") 109 | .expect("Please set a value to the YK_CLIENT_ID environment variable."); 110 | 111 | let api_key = std::env::var("YK_API_KEY") 112 | .expect("Please set a value to the YK_API_KEY environment variable."); 113 | 114 | let otp = read_user_input(); 115 | 116 | let config = Config::default() 117 | .set_client_id(client_id) 118 | .set_key(api_key); 119 | 120 | tokio::run(verify_async(otp, config) 121 | .unwrap() 122 | .map(|_|{ 123 | println!("Valid OTP."); 124 | }) 125 | .map_err(|err|{ 126 | println!("Invalid OTP. Cause: {:?}", err); 127 | })) 128 | } 129 | 130 | fn read_user_input() -> String { 131 | let mut buf = String::new(); 132 | 133 | stdin() 134 | .read_line(&mut buf) 135 | .expect("Could not read user input."); 136 | 137 | buf 138 | } 139 | ``` 140 | 141 | ## Docker 142 | 143 | For convenience and reproducibility, a Docker image can be generated via the provided repo's Dockerfile. 144 | 145 | Build: 146 | ```bash 147 | $ docker build -t yubico-rs . 148 | ... 149 | Successfully built 983cc040c78e 150 | Successfully tagged yubico-rs:latest 151 | ``` 152 | 153 | Run: 154 | ```bash 155 | $ docker run --rm -it -e YK_CLIENT_ID=XXXXX -e YK_API_KEY=XXXXXXXXXXXXXX yubico-rs:latest 156 | Please plug in a yubikey and enter an OTP 157 | ccccccXXXXXXXXXXXXXXXXXXXX 158 | The OTP is valid. 159 | ``` 160 | 161 | ## Changelog 162 | 163 | - 0.10.0: Upgrade to `tokio` 1.1 and `reqwest` 0.11 164 | - 0.9.2: (Yanked) Dependencies update 165 | - 0.9.1: Set HTTP Proxy (Basic-auth is optional) 166 | - 0.9.0: Moving to `tokio` 0.2 and `reqwest` 0.10 167 | - 0.9.0-alpha.1: Moving to `futures` 0.3.0-alpha.19 168 | - 0.8: Rename the `sync` and `async` modules to `sync_verifier` and `async_verifier` to avoid the use of the `async` reserved keyword. 169 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## OTP (One Time Password) 4 | 5 | cargo run --release --example "otp" 6 | 7 | ## OTP (One Time Password) with a HTTP Proxy 8 | 9 | cargo run --release --example "otp_with_proxy" 10 | 11 | ## OTP (One Time Password) with Custom Servers 12 | 13 | cargo run --release --example "otp_custom" 14 | 15 | ## Asynchronous OTP check 16 | 17 | cargo run --release --example "otp_async" -------------------------------------------------------------------------------- /examples/otp.rs: -------------------------------------------------------------------------------- 1 | extern crate yubico; 2 | 3 | use std::io::stdin; 4 | use yubico::config::*; 5 | use yubico::verify; 6 | 7 | fn main() { 8 | println!("Please plug in a yubikey and enter an OTP"); 9 | let client_id = std::env::var("YK_CLIENT_ID") 10 | .expect("Please set a value to the YK_CLIENT_ID environment variable."); 11 | 12 | let api_key = std::env::var("YK_API_KEY") 13 | .expect("Please set a value to the YK_API_KEY environment variable."); 14 | 15 | let config = Config::default().set_client_id(client_id).set_key(api_key); 16 | 17 | let otp = read_user_input(); 18 | 19 | match verify(otp, config) { 20 | Ok(answer) => println!("{}", answer), 21 | Err(e) => println!("Error: {}", e), 22 | } 23 | } 24 | 25 | fn read_user_input() -> String { 26 | let mut buf = String::new(); 27 | stdin() 28 | .read_line(&mut buf) 29 | .expect("Could not read user input."); 30 | 31 | buf 32 | } 33 | -------------------------------------------------------------------------------- /examples/otp_async.rs: -------------------------------------------------------------------------------- 1 | extern crate futures; 2 | extern crate tokio; 3 | extern crate yubico; 4 | 5 | use yubico::verify_async; 6 | 7 | use futures::TryFutureExt; 8 | use std::io::stdin; 9 | use yubico::config::Config; 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<(), ()> { 13 | println!("Please plug in a yubikey and enter an OTP"); 14 | 15 | let client_id = std::env::var("YK_CLIENT_ID") 16 | .expect("Please set a value to the YK_CLIENT_ID environment variable."); 17 | 18 | let api_key = std::env::var("YK_API_KEY") 19 | .expect("Please set a value to the YK_API_KEY environment variable."); 20 | 21 | let otp = read_user_input(); 22 | 23 | let config = Config::default().set_client_id(client_id).set_key(api_key); 24 | 25 | verify_async(otp, config) 26 | .map_ok(|()| println!("Valid OTP.")) 27 | .map_err(|err| println!("Invalid OTP. Cause: {:?}", err)) 28 | .await 29 | } 30 | 31 | fn read_user_input() -> String { 32 | let mut buf = String::new(); 33 | 34 | stdin() 35 | .read_line(&mut buf) 36 | .expect("Could not read user input."); 37 | 38 | buf 39 | } 40 | -------------------------------------------------------------------------------- /examples/otp_custom.rs: -------------------------------------------------------------------------------- 1 | extern crate yubico; 2 | 3 | use yubico::config::*; 4 | use yubico::verify; 5 | 6 | fn main() { 7 | let config = Config::default() 8 | .set_client_id("CLIENT_ID") 9 | .set_key("API_KEY") 10 | .set_api_hosts(vec!["https://api.example.com/verify".into()]); 11 | 12 | match verify("OTP", config) { 13 | Ok(answer) => println!("{}", answer), 14 | Err(e) => println!("Error: {}", e), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/otp_with_proxy.rs: -------------------------------------------------------------------------------- 1 | extern crate yubico; 2 | 3 | use yubico::config::*; 4 | use yubico::verify; 5 | 6 | fn main() { 7 | println!("Please plug in a yubikey and enter an OTP"); 8 | 9 | let client_id = std::env::var("YK_CLIENT_ID") 10 | .expect("Please set a value to the YK_CLIENT_ID environment variable."); 11 | 12 | let api_key = std::env::var("YK_API_KEY") 13 | .expect("Please set a value to the YK_API_KEY environment variable."); 14 | 15 | let config = Config::default() 16 | .set_client_id(client_id) 17 | .set_key(api_key) 18 | .set_proxy_url("http://your_proxy"); 19 | 20 | match verify("OTP", config) { 21 | Ok(answer) => println!("{}", answer), 22 | Err(e) => println!("Error: {}", e), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/async_verifier.rs: -------------------------------------------------------------------------------- 1 | use reqwest::header::USER_AGENT; 2 | use reqwest::Client; 3 | 4 | use crate::config::Config; 5 | use crate::yubicoerror::YubicoError; 6 | use crate::{build_request, Request, Result}; 7 | use futures::stream::FuturesUnordered; 8 | use futures::StreamExt; 9 | use std::sync::Arc; 10 | 11 | pub async fn verify_async(otp: S, config: Config) -> Result<()> 12 | where 13 | S: Into, 14 | { 15 | AsyncVerifier::new(config)?.verify(otp).await 16 | } 17 | 18 | pub struct AsyncVerifier { 19 | client: Client, 20 | config: Config, 21 | } 22 | 23 | impl AsyncVerifier { 24 | pub fn new(config: Config) -> Result { 25 | let client = 26 | if config.proxy_url != "" && config.proxy_username == "" { 27 | AsyncVerifier::get_client_proxy(config.clone())? 28 | } else if config.proxy_url != "" && config.proxy_username != "" { 29 | AsyncVerifier::get_client_proxy_with_auth(config.clone())? 30 | } else { 31 | Client::builder().timeout(config.request_timeout).build()? 32 | }; 33 | 34 | Ok(AsyncVerifier { client, config }) 35 | } 36 | 37 | pub async fn verify(&self, otp: S) -> Result<()> 38 | where 39 | S: Into, 40 | { 41 | let request = Arc::new(build_request(otp, &self.config)?); // Arc because we need the future to be Send. 42 | 43 | let mut responses = FuturesUnordered::new(); 44 | self.config 45 | .api_hosts 46 | .iter() 47 | .for_each(|api_host| responses.push(self.request(request.clone(), api_host))); 48 | 49 | let mut errors = vec![]; 50 | 51 | while let Some(response) = responses.next().await { 52 | match response { 53 | Ok(()) => return Ok(()), 54 | Err(err @ YubicoError::ReplayedRequest) => errors.push(err), 55 | Err(YubicoError::HTTPStatusCode(code)) => { 56 | errors.push(YubicoError::HTTPStatusCode(code)) 57 | } 58 | Err(err) => return Err(err), 59 | } 60 | } 61 | 62 | Err(YubicoError::MultipleErrors(errors)) 63 | } 64 | 65 | async fn request(&self, request: Arc, api_host: &str) -> Result<()> { 66 | let url = request.build_url(api_host); 67 | let http_request = self 68 | .client 69 | .get(&url) 70 | .header(USER_AGENT, self.config.user_agent.clone()); 71 | 72 | let response = http_request.send().await?; 73 | let status_code = response.status(); 74 | 75 | if !status_code.is_success() { 76 | return Err(YubicoError::HTTPStatusCode(status_code)); 77 | } 78 | 79 | let text = response.text().await?; 80 | 81 | request.response_verifier.verify_response(text) 82 | } 83 | 84 | fn get_client_proxy(config: Config) -> Result { 85 | Ok(Client::builder() 86 | .timeout(config.request_timeout) 87 | .proxy(reqwest::Proxy::all(&config.proxy_url)?).build()?) 88 | } 89 | 90 | fn get_client_proxy_with_auth(config: Config) -> Result { 91 | let proxy = reqwest::Proxy::all(&config.proxy_url)? 92 | .basic_auth(&config.proxy_username, &config.proxy_password); 93 | Ok(Client::builder().timeout(config.request_timeout).proxy(proxy).build()?) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | use std::time::Duration; 3 | 4 | static API1_HOST: &str = "https://api.yubico.com/wsapi/2.0/verify"; 5 | static API2_HOST: &str = "https://api2.yubico.com/wsapi/2.0/verify"; 6 | static API3_HOST: &str = "https://api3.yubico.com/wsapi/2.0/verify"; 7 | static API4_HOST: &str = "https://api4.yubico.com/wsapi/2.0/verify"; 8 | static API5_HOST: &str = "https://api5.yubico.com/wsapi/2.0/verify"; 9 | 10 | #[derive(Clone, Debug, PartialEq)] 11 | pub enum Slot { 12 | Slot1, 13 | Slot2, 14 | } 15 | 16 | #[derive(Clone, Debug, PartialEq)] 17 | pub enum Mode { 18 | Sha1, 19 | Otp, 20 | } 21 | 22 | /// From the Validation Protocol documentation: 23 | /// 24 | /// A value 0 to 100 indicating percentage of syncing required by client, 25 | /// or strings "fast" or "secure" to use server-configured values; if 26 | /// absent, let the server decide. 27 | #[derive(Copy, Clone, Debug, PartialEq)] 28 | pub struct SyncLevel(u8); 29 | 30 | impl SyncLevel { 31 | pub fn fast() -> SyncLevel { 32 | SyncLevel(0) 33 | } 34 | 35 | pub fn secure() -> SyncLevel { 36 | SyncLevel(100) 37 | } 38 | 39 | pub fn custom(level: u8) -> SyncLevel { 40 | if level > 100 { 41 | SyncLevel(100) 42 | } else { 43 | SyncLevel(level) 44 | } 45 | } 46 | } 47 | 48 | impl Display for SyncLevel { 49 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 50 | write!(f, "{}", self.0) 51 | } 52 | } 53 | 54 | #[derive(Clone, Debug, PartialEq)] 55 | pub struct Config { 56 | pub client_id: String, 57 | pub key: Vec, 58 | pub api_hosts: Vec, 59 | pub user_agent: String, 60 | pub sync_level: SyncLevel, 61 | /// The timeout for HTTP requests. 62 | pub request_timeout: Duration, 63 | pub proxy_url: String, 64 | pub proxy_username: String, 65 | pub proxy_password: String, 66 | } 67 | 68 | #[allow(dead_code)] 69 | impl Config { 70 | pub fn default() -> Config { 71 | Config { 72 | client_id: String::new(), 73 | key: Vec::new(), 74 | api_hosts: build_hosts(), 75 | user_agent: "github.com/wisespace-io/yubico-rs".to_string(), 76 | sync_level: SyncLevel::secure(), 77 | request_timeout: Duration::from_secs(30), // Value taken from the reqwest crate. 78 | proxy_url: String::new(), 79 | proxy_username: String::new(), 80 | proxy_password: String::new(), 81 | } 82 | } 83 | 84 | pub fn set_client_id(mut self, client_id: C) -> Self 85 | where 86 | C: Into, 87 | { 88 | self.client_id = client_id.into(); 89 | self 90 | } 91 | 92 | pub fn set_key(mut self, key: K) -> Self 93 | where 94 | K: Into, 95 | { 96 | self.key = key.into().into_bytes(); 97 | self 98 | } 99 | 100 | pub fn set_api_hosts(mut self, hosts: Vec) -> Self { 101 | self.api_hosts = hosts; 102 | self 103 | } 104 | 105 | pub fn set_user_agent(mut self, user_agent: String) -> Self { 106 | self.user_agent = user_agent; 107 | self 108 | } 109 | 110 | pub fn set_sync_level(mut self, level: SyncLevel) -> Self { 111 | self.sync_level = level; 112 | self 113 | } 114 | 115 | pub fn set_request_timeout(mut self, timeout: Duration) -> Self { 116 | self.request_timeout = timeout; 117 | self 118 | } 119 | 120 | pub fn set_proxy_url

(mut self, proxy_url: P) -> Self 121 | where 122 | P: Into, 123 | { 124 | self.proxy_url = proxy_url.into(); 125 | self 126 | } 127 | 128 | pub fn set_proxy_username(mut self, proxy_username: U) -> Self 129 | where 130 | U: Into, 131 | { 132 | self.proxy_username = proxy_username.into(); 133 | self 134 | } 135 | 136 | pub fn set_proxy_password

(mut self, proxy_password: P) -> Self 137 | where 138 | P: Into, 139 | { 140 | self.proxy_password = proxy_password.into(); 141 | self 142 | } 143 | } 144 | 145 | fn build_hosts() -> Vec { 146 | let mut hosts: Vec = Vec::new(); 147 | 148 | hosts.push(API1_HOST.to_string()); 149 | hosts.push(API2_HOST.to_string()); 150 | hosts.push(API3_HOST.to_string()); 151 | hosts.push(API4_HOST.to_string()); 152 | hosts.push(API5_HOST.to_string()); 153 | 154 | hosts 155 | } 156 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "online-tokio")] 2 | pub mod async_verifier; 3 | pub mod config; 4 | mod sec; 5 | pub mod sync_verifier; 6 | pub mod yubicoerror; 7 | 8 | use std::collections::BTreeMap; 9 | 10 | use base64::{decode, encode}; 11 | use config::Config; 12 | use rand::distributions::Alphanumeric; 13 | use rand::rngs::OsRng; 14 | use rand::Rng; 15 | use yubicoerror::YubicoError; 16 | 17 | #[cfg(feature = "online-tokio")] 18 | pub use async_verifier::verify_async; 19 | pub use sync_verifier::verify; 20 | 21 | type Result = ::std::result::Result; 22 | 23 | #[derive(Clone)] 24 | pub struct Request { 25 | query: String, 26 | response_verifier: ResponseVerifier, 27 | } 28 | 29 | impl Request { 30 | fn build_url(&self, for_api_host: &str) -> String { 31 | format!("{}?{}", for_api_host, self.query) 32 | } 33 | } 34 | 35 | #[derive(Clone)] 36 | pub struct ResponseVerifier { 37 | otp: String, 38 | nonce: String, 39 | key: Vec, 40 | } 41 | 42 | impl ResponseVerifier { 43 | fn verify_response(&self, raw_response: String) -> Result<()> { 44 | let response_map: BTreeMap = build_response_map(raw_response); 45 | 46 | let status: &str = &*response_map.get("status").unwrap(); 47 | 48 | if let "OK" = status { 49 | // Signature located in the response must match the signature we will build 50 | let signature_response: &str = &*response_map 51 | .get("h") 52 | .ok_or_else(|| YubicoError::InvalidResponse)?; 53 | verify_signature(signature_response, response_map.clone(), &self.key)?; 54 | 55 | // Check if "otp" in the response is the same as the "otp" supplied in the request. 56 | let otp_response: &str = &*response_map 57 | .get("otp") 58 | .ok_or_else(|| YubicoError::InvalidResponse)?; 59 | if !self.otp.eq(otp_response) { 60 | return Err(YubicoError::OTPMismatch); 61 | } 62 | 63 | // Check if "nonce" in the response is the same as the "nonce" supplied in the request. 64 | let nonce_response: &str = &*response_map 65 | .get("nonce") 66 | .ok_or_else(|| YubicoError::InvalidResponse)?; 67 | if !self.nonce.eq(nonce_response) { 68 | return Err(YubicoError::NonceMismatch); 69 | } 70 | 71 | Ok(()) 72 | } else { 73 | // Check the status of the operation 74 | match status { 75 | "BAD_OTP" => Err(YubicoError::BadOTP), 76 | "REPLAYED_OTP" => Err(YubicoError::ReplayedOTP), 77 | "BAD_SIGNATURE" => Err(YubicoError::BadSignature), 78 | "MISSING_PARAMETER" => Err(YubicoError::MissingParameter), 79 | "NO_SUCH_CLIENT" => Err(YubicoError::NoSuchClient), 80 | "OPERATION_NOT_ALLOWED" => Err(YubicoError::OperationNotAllowed), 81 | "BACKEND_ERROR" => Err(YubicoError::BackendError), 82 | "NOT_ENOUGH_ANSWERS" => Err(YubicoError::NotEnoughAnswers), 83 | "REPLAYED_REQUEST" => Err(YubicoError::ReplayedRequest), 84 | _ => Err(YubicoError::UnknownStatus), 85 | } 86 | } 87 | } 88 | } 89 | 90 | fn build_request(otp: S, config: &Config) -> Result 91 | where 92 | S: Into, 93 | { 94 | let str_otp = otp.into(); 95 | 96 | // A Yubikey can be configured to add line ending chars, or not. 97 | let str_otp = str_otp.trim().to_string(); 98 | 99 | if printable_characters(&str_otp) { 100 | let nonce: String = generate_nonce(); 101 | let mut query = form_urlencoded::Serializer::new(String::new()); 102 | query.append_pair("id", &config.client_id); 103 | query.append_pair("nonce", &nonce); 104 | query.append_pair("otp", &str_otp); 105 | query.append_pair("sl", &config.sync_level.to_string()); 106 | 107 | let query = query.finish(); 108 | match sec::build_signature(&config.key, query.as_bytes()) { 109 | Ok(signature) => { 110 | // Recover the query 111 | let mut query = form_urlencoded::Serializer::new(query); 112 | 113 | // Base 64 encode the resulting value according to RFC 4648 114 | let encoded_signature = encode(&signature.into_bytes()); 115 | 116 | // Append the value under key h to the message. 117 | query.append_pair("h", &encoded_signature); 118 | 119 | let verifier = ResponseVerifier { 120 | otp: str_otp, 121 | nonce, 122 | key: config.key.clone(), 123 | }; 124 | 125 | let request = Request { 126 | query: query.finish(), 127 | response_verifier: verifier, 128 | }; 129 | 130 | Ok(request) 131 | } 132 | Err(error) => Err(error), 133 | } 134 | } else { 135 | Err(YubicoError::BadOTP) 136 | } 137 | } 138 | 139 | // Recommendation is that clients only check that the input consists of 32-48 printable characters 140 | fn printable_characters(otp: &str) -> bool { 141 | for c in otp.chars() { 142 | if !c.is_ascii() { 143 | return false; 144 | } 145 | } 146 | otp.len() > 32 && otp.len() < 48 147 | } 148 | 149 | fn generate_nonce() -> String { 150 | OsRng{} 151 | .sample_iter(&Alphanumeric) 152 | .map(char::from) 153 | .take(40) 154 | .collect() 155 | } 156 | 157 | // Remove the signature itself from the values over for verification. 158 | // Sort the key/value pairs. 159 | fn verify_signature( 160 | signature_response: &str, 161 | mut response_map: BTreeMap, 162 | key: &[u8], 163 | ) -> Result<()> { 164 | response_map.remove("h"); 165 | 166 | let mut query = String::new(); 167 | for (key, value) in response_map { 168 | let param = format!("{}={}&", key, value); 169 | query.push_str(param.as_ref()); 170 | } 171 | query.pop(); // remove last & 172 | 173 | let decoded_signature = &decode(signature_response).unwrap()[..]; 174 | sec::verify_signature(key, query.as_bytes(), decoded_signature) 175 | } 176 | 177 | fn build_response_map(result: String) -> BTreeMap { 178 | let mut parameters = BTreeMap::new(); 179 | for line in result.lines() { 180 | let param: Vec<&str> = line.splitn(2, '=').collect(); 181 | if param.len() > 1 { 182 | parameters.insert(param[0].to_string(), param[1].to_string()); 183 | } 184 | } 185 | parameters 186 | } 187 | -------------------------------------------------------------------------------- /src/sec.rs: -------------------------------------------------------------------------------- 1 | use crate::yubicoerror::YubicoError; 2 | use base64::decode; 3 | use hmac::{digest::CtOutput, Hmac, Mac}; 4 | use sha1::Sha1; 5 | 6 | type HmacSha1 = Hmac; 7 | 8 | // 1. Apply the HMAC-SHA-1 algorithm on the line as an octet string using the API key as key 9 | pub fn build_signature( 10 | key: &[u8], 11 | input: &[u8], 12 | ) -> Result, YubicoError> { 13 | let decoded_key = decode(key)?; 14 | 15 | let mut hmac = match HmacSha1::new_from_slice(&decoded_key) { 16 | Ok(h) => h, 17 | Err(_) => return Err(YubicoError::InvalidKeyLength), 18 | }; 19 | hmac.update(input); 20 | Ok(hmac.finalize()) 21 | } 22 | 23 | pub fn verify_signature( 24 | key: &[u8], 25 | input: &[u8], 26 | expected: &[u8], 27 | ) -> Result<(), YubicoError> { 28 | let decoded_key = decode(key)?; 29 | 30 | let mut hmac = match HmacSha1::new_from_slice(&decoded_key) { 31 | Ok(h) => h, 32 | Err(_) => return Err(YubicoError::InvalidKeyLength), 33 | }; 34 | hmac.update(input); 35 | hmac.verify_slice(expected).map_err(|_| YubicoError::SignatureMismatch) 36 | } 37 | -------------------------------------------------------------------------------- /src/sync_verifier.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::sync::mpsc::{channel, Sender}; 3 | 4 | use reqwest::header::USER_AGENT; 5 | use threadpool::ThreadPool; 6 | 7 | use crate::build_request; 8 | use crate::config::Config; 9 | use crate::yubicoerror::YubicoError; 10 | use crate::Request; 11 | use crate::Result; 12 | use reqwest::blocking::Client; 13 | use std::sync::Arc; 14 | 15 | pub fn verify(otp: S, config: Config) -> Result 16 | where 17 | S: Into, 18 | { 19 | Verifier::new(config)?.verify(otp) 20 | } 21 | 22 | pub struct Verifier { 23 | config: Config, 24 | thread_pool: ThreadPool, 25 | client: Arc, 26 | } 27 | 28 | impl Verifier { 29 | pub fn new(config: Config) -> Result { 30 | let number_of_hosts = config.api_hosts.len(); 31 | let client = 32 | if config.proxy_url != "" && config.proxy_username == "" { 33 | Verifier::get_client_proxy(config.clone())? 34 | } else if config.proxy_url != "" && config.proxy_username != "" { 35 | Verifier::get_client_proxy_with_auth(config.clone())? 36 | } else { 37 | Client::builder().timeout(config.request_timeout).build()? 38 | }; 39 | 40 | Ok(Verifier { 41 | config, 42 | thread_pool: ThreadPool::new(number_of_hosts), 43 | client: Arc::new(client), 44 | }) 45 | } 46 | 47 | pub fn verify(&self, otp: S) -> Result 48 | where 49 | S: Into, 50 | { 51 | let request = build_request(otp, &self.config)?; 52 | 53 | let number_of_hosts = self.config.api_hosts.len(); 54 | let (tx, rx) = channel(); 55 | 56 | for api_host in &self.config.api_hosts { 57 | let tx = tx.clone(); 58 | let request = request.clone(); 59 | let url = request.build_url(api_host); 60 | let user_agent = self.config.user_agent.to_string(); 61 | let client = self.client.clone(); 62 | 63 | self.thread_pool.execute(move || { 64 | process(&client, tx, url, &request, user_agent); 65 | }); 66 | } 67 | 68 | let mut success = false; 69 | let mut results: Vec> = Vec::new(); 70 | for _ in 0..number_of_hosts { 71 | match rx.recv() { 72 | Ok(Response::Signal(result)) => match result { 73 | Ok(_) => { 74 | results.truncate(0); 75 | success = true; 76 | } 77 | Err(_) => { 78 | results.push(result); 79 | } 80 | }, 81 | Err(e) => { 82 | results.push(Err(YubicoError::ChannelError(e))); 83 | break; 84 | } 85 | } 86 | } 87 | 88 | if success { 89 | Ok("The OTP is valid.".into()) 90 | } else { 91 | results.pop().ok_or_else(|| YubicoError::InvalidOtp)? 92 | } 93 | } 94 | 95 | fn get_client_proxy(config: Config) -> Result { 96 | Ok(Client::builder() 97 | .timeout(config.request_timeout) 98 | .proxy(reqwest::Proxy::all(&config.proxy_url)?).build()?) 99 | } 100 | 101 | fn get_client_proxy_with_auth(config: Config) -> Result { 102 | let proxy = reqwest::Proxy::all(&config.proxy_url)? 103 | .basic_auth(&config.proxy_username, &config.proxy_password); 104 | Ok(Client::builder().timeout(config.request_timeout).proxy(proxy).build()?) 105 | } 106 | } 107 | 108 | enum Response { 109 | Signal(Result), 110 | } 111 | 112 | fn process( 113 | client: &Client, 114 | sender: Sender, 115 | url: String, 116 | request: &Request, 117 | user_agent: String, 118 | ) { 119 | match get(client, url, user_agent) { 120 | Ok(raw_response) => { 121 | let result = request 122 | .response_verifier 123 | .verify_response(raw_response) 124 | .map(|()| "The OTP is valid.".to_owned()); 125 | sender.send(Response::Signal(result)).unwrap(); 126 | } 127 | Err(e) => { 128 | sender.send(Response::Signal(Err(e))).unwrap(); 129 | } 130 | } 131 | } 132 | 133 | pub fn get(client: &Client, url: String, user_agent: String) -> Result { 134 | let mut response = client 135 | .get(url.as_str()) 136 | .header(USER_AGENT, user_agent) 137 | .send()?; 138 | 139 | let mut data = String::new(); 140 | response.read_to_string(&mut data)?; 141 | 142 | Ok(data) 143 | } 144 | -------------------------------------------------------------------------------- /src/yubicoerror.rs: -------------------------------------------------------------------------------- 1 | use base64::DecodeError as base64Error; 2 | use std::error::Error as StdError; 3 | use std::fmt; 4 | use std::io::Error as ioError; 5 | use std::sync::mpsc::RecvError as channelError; 6 | 7 | #[derive(Debug)] 8 | pub enum YubicoError { 9 | ConfigurationError(reqwest::Error), 10 | Network(reqwest::Error), 11 | HTTPStatusCode(reqwest::StatusCode), 12 | IOError(ioError), 13 | ChannelError(channelError), 14 | DecodeError(base64Error), 15 | #[cfg(feature = "online-tokio")] 16 | MultipleErrors(Vec), 17 | BadOTP, 18 | ReplayedOTP, 19 | BadSignature, 20 | MissingParameter, 21 | NoSuchClient, 22 | OperationNotAllowed, 23 | BackendError, 24 | NotEnoughAnswers, 25 | ReplayedRequest, 26 | UnknownStatus, 27 | OTPMismatch, 28 | NonceMismatch, 29 | SignatureMismatch, 30 | InvalidKeyLength, 31 | InvalidResponse, 32 | InvalidOtp, 33 | } 34 | 35 | impl fmt::Display for YubicoError { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | match *self { 38 | YubicoError::ConfigurationError(ref err) => write!(f, "Configuration error: {}", err), 39 | YubicoError::Network(ref err) => write!(f, "Connectivity error: {}", err), 40 | YubicoError::HTTPStatusCode(code) => write!(f, "Error found: {}", code), 41 | YubicoError::IOError(ref err) => write!(f, "IO error: {}", err), 42 | YubicoError::ChannelError(ref err) => write!(f, "Channel error: {}", err), 43 | YubicoError::DecodeError(ref err) => write!(f, "Decode error: {}", err), 44 | #[cfg(feature = "online-tokio")] 45 | YubicoError::MultipleErrors(ref errs) => { 46 | write!(f, "Multiple errors. ")?; 47 | 48 | for err in errs { 49 | write!(f, "{} ", err)?; 50 | } 51 | 52 | Ok(()) 53 | } 54 | YubicoError::BadOTP => write!(f, "The OTP has invalid format."), 55 | YubicoError::ReplayedOTP => write!(f, "The OTP has already been seen by the service."), 56 | YubicoError::BadSignature => write!(f, "The HMAC signature verification failed."), 57 | YubicoError::MissingParameter => write!(f, "The request lacks a parameter."), 58 | YubicoError::NoSuchClient => write!(f, "The request id does not exist."), 59 | YubicoError::OperationNotAllowed => { 60 | write!(f, "The request id is not allowed to verify OTPs.") 61 | } 62 | YubicoError::BackendError => write!( 63 | f, 64 | "Unexpected error in our server. Please contact us if you see this error." 65 | ), 66 | YubicoError::NotEnoughAnswers => write!( 67 | f, 68 | "Server could not get requested number of syncs during before timeout" 69 | ), 70 | YubicoError::ReplayedRequest => { 71 | write!(f, "Server has seen the OTP/Nonce combination before") 72 | } 73 | YubicoError::UnknownStatus => { 74 | write!(f, "Unknown status sent by the OTP validation server") 75 | } 76 | YubicoError::OTPMismatch => write!(f, "OTP mismatch, It may be an attack attempt"), 77 | YubicoError::NonceMismatch => write!(f, "Nonce mismatch, It may be an attack attempt"), 78 | YubicoError::SignatureMismatch => { 79 | write!(f, "Signature mismatch, It may be an attack attempt") 80 | } 81 | YubicoError::InvalidKeyLength => { 82 | write!(f, "Invalid key length encountered while building signature") 83 | } 84 | YubicoError::InvalidResponse => { 85 | write!(f, "Invalid response from the validation server") 86 | } 87 | YubicoError::InvalidOtp => write!(f, "Invalid OTP"), 88 | } 89 | } 90 | } 91 | 92 | impl StdError for YubicoError { 93 | fn cause(&self) -> Option<&dyn StdError> { 94 | match *self { 95 | YubicoError::Network(ref err) => Some(err), 96 | YubicoError::HTTPStatusCode(_) => None, 97 | YubicoError::IOError(ref err) => Some(err), 98 | YubicoError::ChannelError(ref err) => Some(err), 99 | YubicoError::DecodeError(ref err) => Some(err), 100 | #[cfg(feature = "online-tokio")] 101 | YubicoError::MultipleErrors(ref errs) => match errs.first() { 102 | Some(err) => Some(err), 103 | None => None, 104 | }, 105 | _ => None, 106 | } 107 | } 108 | } 109 | 110 | impl From for YubicoError { 111 | fn from(err: reqwest::Error) -> YubicoError { 112 | YubicoError::Network(err) 113 | } 114 | } 115 | 116 | impl From for YubicoError { 117 | fn from(err: reqwest::StatusCode) -> YubicoError { 118 | YubicoError::HTTPStatusCode(err) 119 | } 120 | } 121 | 122 | impl From for YubicoError { 123 | fn from(err: ioError) -> YubicoError { 124 | YubicoError::IOError(err) 125 | } 126 | } 127 | 128 | impl From for YubicoError { 129 | fn from(err: channelError) -> YubicoError { 130 | YubicoError::ChannelError(err) 131 | } 132 | } 133 | 134 | impl From for YubicoError { 135 | fn from(err: base64Error) -> YubicoError { 136 | YubicoError::DecodeError(err) 137 | } 138 | } 139 | --------------------------------------------------------------------------------