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