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