Skip to content

Commit cf42455

Browse files
author
Flavio Oliveira
committed
Not fully implemented yet. Missing response signature verification
0 parents  commit cf42455

File tree

6 files changed

+411
-0
lines changed

6 files changed

+411
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
target
2+
Cargo.lock
3+
4+
# IDEA
5+
*.iml
6+
.idea*.*
7+
.idea/*

Cargo.toml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "yubico"
3+
version = "0.1.0"
4+
authors = ["Flavio Oliveira <[email protected]>"]
5+
6+
description = "Yubikey client API library"
7+
license = "MIT"
8+
keywords = ["HMS", "yubikey", "authentication", "encryption", "OTP"]
9+
10+
repository = "https://github.com/wisespace-io/yubico-rs"
11+
readme = "README.md"
12+
13+
[lib]
14+
name = "yubico"
15+
path = "src/lib.rs"
16+
17+
[dependencies]
18+
url = "1.2.3"
19+
hyper = "0.9"
20+
rand = "0.3.15"
21+
base64 = "^0.2"
22+
threadpool = "1.3"
23+
rust-crypto = "^0.2"

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2016 Flavio Oliveira
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/wisespace-io/yubico-rs/blob/master/LICENSE)
2+
[![](https://meritbadge.herokuapp.com/yubico)](https://crates.io/crates/yubico)
3+
4+
# Yubico
5+
Yubikey client API library, [validation protocol version 2.0](https://developers.yubico.com/yubikey-val/Validation_Protocol_V2.0.html).
6+
7+
Enables integration with the Yubico validation platform, so you can use Yubikey's one-time-password in your Rust application,
8+
allowing a user to authenticate via Yubikey.
9+
10+
IMPORTANT: WIP, signature verification hasn't been implemented yet.
11+
12+
# Usage
13+
14+
Add this to your Cargo.toml
15+
16+
```toml
17+
[dependencies]
18+
yubico = "0.1"
19+
```
20+
21+
[Request your api key](https://upgrade.yubico.com/getapikey/).
22+
23+
# Example
24+
```rust
25+
extern crate yubico;
26+
27+
use yubico::Yubico;
28+
29+
fn main() {
30+
let yubi = Yubico::new("CLIENT_ID".into(), "API_KEY".into());
31+
let result = yubi.verify("OTP".into());
32+
match result {
33+
Ok(answer) => println!("{}", answer),
34+
Err(e) => println!("Error: {}", e),
35+
}
36+
}
37+
```
38+
39+
## License
40+
41+
* MIT license (see [LICENSE](LICENSE) or <http://opensource.org/licenses/MIT>)

src/lib.rs

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#[macro_use] extern crate url;
2+
#[macro_use] extern crate hyper;
3+
extern crate base64;
4+
extern crate crypto;
5+
extern crate rand;
6+
extern crate threadpool;
7+
8+
pub mod yubicoerror;
9+
10+
use yubicoerror::YubicoError;
11+
use hyper::Client;
12+
use hyper::header::{Headers};
13+
use std::io::prelude::*;
14+
use base64::{encode};
15+
use crypto::mac::{Mac};
16+
use crypto::hmac::Hmac;
17+
use crypto::sha1::Sha1;
18+
use rand::{thread_rng, Rng};
19+
use std::collections::HashMap;
20+
use threadpool::ThreadPool;
21+
use std::sync::mpsc::{ channel, Sender };
22+
23+
use url::percent_encoding::{utf8_percent_encode, SIMPLE_ENCODE_SET};
24+
define_encode_set! {
25+
/// This encode set is used in the URL parser for query strings.
26+
pub QUERY_ENCODE_SET = [SIMPLE_ENCODE_SET] | {'+', '='}
27+
}
28+
29+
static API1_HOST : &'static str = "https://api.yubico.com/wsapi/2.0/verify";
30+
static API2_HOST : &'static str = "https://api2.yubico.com/wsapi/2.0/verify";
31+
static API3_HOST : &'static str = "https://api3.yubico.com/wsapi/2.0/verify";
32+
static API4_HOST : &'static str = "https://api4.yubico.com/wsapi/2.0/verify";
33+
static API5_HOST : &'static str = "https://api5.yubico.com/wsapi/2.0/verify";
34+
35+
header! { (UserAgent, "User-Agent") => [String] }
36+
37+
/// The `Result` type used in this crate.
38+
type Result<T> = ::std::result::Result<T, YubicoError>;
39+
40+
enum Response {
41+
Signal(Result<String>),
42+
}
43+
44+
#[derive(Clone)]
45+
pub struct Request {
46+
otp: String,
47+
nonce: String,
48+
signature: String,
49+
query: String,
50+
}
51+
52+
#[derive(Clone)]
53+
pub struct Yubico {
54+
client_id: String,
55+
key: String,
56+
}
57+
58+
impl Yubico {
59+
/// Creates a new Yubico instance.
60+
pub fn new(client_id: String, key: String) -> Self {
61+
Yubico {
62+
client_id: client_id,
63+
key: key,
64+
}
65+
}
66+
67+
// Verify a provided OTP
68+
pub fn verify(&self, otp: String) -> Result<String> {
69+
match self.printable_characters(otp.clone()) {
70+
false => Err(YubicoError::BadOTP),
71+
_ => {
72+
// TODO: use OsRng to generate a most secure nonce
73+
let nonce: String = thread_rng().gen_ascii_chars().take(40).collect();
74+
let mut query = format!("id={}&otp={}&nonce={}&sl=secure", self.client_id, otp, nonce);
75+
76+
let signature = self.build_signature(query.clone());
77+
query.push_str(signature.as_ref());
78+
79+
let request = Request {otp: otp, nonce: nonce, signature: signature, query: query};
80+
81+
let pool = ThreadPool::new(3);
82+
let (tx, rx) = channel();
83+
let api_hosts = vec![API1_HOST, API2_HOST, API3_HOST, API4_HOST, API5_HOST];
84+
for api_host in api_hosts {
85+
let tx = tx.clone();
86+
let request = request.clone();
87+
let self_clone = self.clone(); //threads can't reference values which are not owned by the thread.
88+
pool.execute(move|| { self_clone.process(tx, api_host, request) });
89+
}
90+
91+
let mut results: Vec<Result<String>> = Vec::new();
92+
for _ in 0..5 {
93+
match rx.recv() {
94+
Ok(Response::Signal(result)) => {
95+
match result {
96+
Ok(_) => {
97+
results.truncate(0);
98+
break
99+
},
100+
Err(_) => results.push(result),
101+
}
102+
},
103+
Err(e) => {
104+
results.push(Err(YubicoError::ChannelError(e)));
105+
break
106+
},
107+
}
108+
}
109+
110+
if results.len() == 0 {
111+
Ok("The OTP is valid.".into())
112+
} else {
113+
let result = results.pop().unwrap();
114+
result
115+
}
116+
},
117+
}
118+
}
119+
120+
// 1. Apply the HMAC-SHA-1 algorithm on the line as an octet string using the API key as key
121+
// 2. Base 64 encode the resulting value according to RFC 4648
122+
// 3. Append the value under key h to the message.
123+
fn build_signature(&self, query: String) -> String {
124+
let mut hmac = Hmac::new(Sha1::new(), self.key.as_bytes());
125+
hmac.input(query.as_bytes());
126+
let signature = encode(hmac.result().code());
127+
let signature_str = format!("&h={}", signature);
128+
utf8_percent_encode(signature_str.as_ref(), QUERY_ENCODE_SET).collect::<String>()
129+
}
130+
131+
// Recommendation is that clients only check that the input consists of 32-48 printable characters
132+
fn printable_characters(&self, otp: String) -> bool {
133+
if otp.len() < 32 || otp.len() > 48 { false } else { true }
134+
}
135+
136+
fn process(&self, sender: Sender<Response>, api_host: &str, request: Request) {
137+
let url = format!("{}?{}", api_host, request.query);
138+
match self.get(url) {
139+
Ok(result) => {
140+
let response_map: HashMap<String, String> = self.build_response_map(result);
141+
142+
// Check if "otp" in the response is the same as the "otp" supplied in the request.
143+
let otp_response : &str = &*response_map.get("otp").unwrap();
144+
if !request.otp.contains(otp_response) {
145+
sender.send(Response::Signal(Err(YubicoError::OTPMismatch))).unwrap();
146+
}
147+
148+
// Check if "nonce" in the response is the same as the "nonce" supplied in the request.
149+
let nonce_response : &str = &*response_map.get("nonce").unwrap();
150+
if !request.nonce.contains(nonce_response) {
151+
sender.send(Response::Signal(Err(YubicoError::NonceMismatch))).unwrap();
152+
}
153+
154+
// Check the status of the operation
155+
let status: &str = &*response_map.get("status").unwrap();
156+
match status {
157+
"OK" => sender.send(Response::Signal(Ok("The OTP is valid.".to_owned()))).unwrap(),
158+
"BAD_OTP" => sender.send(Response::Signal(Err(YubicoError::BadOTP))).unwrap(),
159+
"REPLAYED_OTP" => sender.send(Response::Signal(Err(YubicoError::ReplayedOTP))).unwrap(),
160+
"BAD_SIGNATURE" => sender.send(Response::Signal(Err(YubicoError::BadSignature))).unwrap(),
161+
"MISSING_PARAMETER" => sender.send(Response::Signal(Err(YubicoError::MissingParameter))).unwrap(),
162+
"NO_SUCH_CLIENT" => sender.send(Response::Signal(Err(YubicoError::NoSuchClient))).unwrap(),
163+
"OPERATION_NOT_ALLOWED" => sender.send(Response::Signal(Err(YubicoError::OperationNotAllowed))).unwrap(),
164+
"BACKEND_ERROR" => sender.send(Response::Signal(Err(YubicoError::BackendError))).unwrap(),
165+
"NOT_ENOUGH_ANSWERS" => sender.send(Response::Signal(Err(YubicoError::NotEnoughAnswers))).unwrap(),
166+
"REPLAYED_REQUEST" => sender.send(Response::Signal(Err(YubicoError::ReplayedRequest))).unwrap(),
167+
_ => sender.send(Response::Signal(Err(YubicoError::UnknownStatus))).unwrap()
168+
}
169+
},
170+
Err(e) => {
171+
sender.send( Response::Signal(Err(e)) ).unwrap();
172+
}
173+
}
174+
}
175+
176+
fn build_response_map(&self, result: String) -> HashMap<String, String> {
177+
let mut parameters = HashMap::new();
178+
for line in result.lines() {
179+
let param: Vec<&str> = line.splitn(2, '=').collect();
180+
if param.len() > 1 {
181+
parameters.insert(param[0].to_string(), param[1].to_string());
182+
}
183+
}
184+
parameters
185+
}
186+
187+
pub fn get(&self, url: String) -> Result<String> {
188+
let client = Client::new();
189+
let mut custom_headers = Headers::new();
190+
custom_headers.set(UserAgent("yubico-rs".to_owned()));
191+
192+
let mut response = String::new();
193+
let mut res = try!(client.get(&url).headers(custom_headers).send());
194+
try!(res.read_to_string(&mut response));
195+
196+
Ok(response)
197+
}
198+
}

0 commit comments

Comments
 (0)