├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── src └── lib.rs └── tests └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oath" 3 | version = "0.10.1" 4 | authors = ["Andrei Vacariu ", "crypto-universe "] 5 | license = "MIT" 6 | description = """ 7 | An implementation of OATH algorithms in Rust. Includes TOTP, HOTP, and OCRA. 8 | """ 9 | homepage = "https://github.com/avacariu/rust-oath" 10 | repository = "https://github.com/avacariu/rust-oath" 11 | documentation = "https://docs.rs/oath/" 12 | keywords = ["oath", "totp", "hotp", "ocra"] 13 | categories = ["cryptography"] 14 | 15 | [dependencies] 16 | sha-1 = "0.3" 17 | sha2 = "0.5" 18 | digest = "0.5" 19 | hmac = "0.1" 20 | rustc-hex = "1.0" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andrei Vacariu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-oath 2 | [![Build Status](https://travis-ci.org/avacariu/rust-oath.svg?branch=master)](https://travis-ci.org/avacariu/rust-oath) 3 | [![oath on crates.io](https://img.shields.io/crates/v/oath.svg)](https://crates.io/crates/oath) 4 | [![Documentation](https://docs.rs/oath/badge.svg)](https://docs.rs/oath/) 5 | [![MIT license](https://img.shields.io/dub/l/vibe-d.svg)](https://opensource.org/licenses/MIT) 6 | 7 | 8 | This library aims to provide implementations of HOTP, TOTP, and OCRA as 9 | specified by the RFCs. 10 | 11 | Implemented: 12 | 13 | * HOTP ([RFC 4226](http://tools.ietf.org/html/rfc4226)) 14 | * TOTP ([RFC 6238](http://tools.ietf.org/html/rfc6238)) 15 | * OCRA ([RFC 6287](https://tools.ietf.org/html/rfc6287)) 16 | 17 | **WARNING** While [ieee754 is broken](https://github.com/rust-lang/rust/issues/41793), 18 | [RAMP](https://crates.io/crates/ramp) fails to compile. 19 | OCRA numeric question mode can't use long Int, it is forced to use u64 instead. 20 | This data type leads us to question length limitation: 19 symbols. Number must fit u64. 21 | For default challenge format (N08) it is more that enough. 22 | 23 | ## Examples 24 | 25 | ### HOTP 26 | 27 | extern crate oath; 28 | 29 | use oath::hotp; 30 | 31 | fn main () { 32 | assert_eq!(hotp("ff", 23, 6).unwrap(), 330795); 33 | } 34 | 35 | ### TOTP 36 | 37 | All the times below are in seconds. 38 | 39 | extern crate oath; 40 | 41 | use oath::{totp_raw_now, HashType}; 42 | 43 | fn main () { 44 | // Return value differs every 30 seconds. 45 | totp_raw_now(b"12345678901234567890", 6, 0, 30, &HashType::SHA1); 46 | } 47 | 48 | ### OCRA 49 | 50 | extern crate oath; 51 | 52 | use oath::ocra; 53 | 54 | let NULL: &[u8] = &[]; 55 | let STANDARD_KEY_20: &[u8] = &[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 56 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30]; 57 | let STANDARD_KEY_32 = "12345678901234567890123456789012".as_bytes(); 58 | let STANDARD_KEY_64 = "1234567890123456789012345678901234567890123456789012345678901234".as_bytes(); 59 | let PIN_1234_SHA1: &[u8] = &[0x71, 0x10, 0xed, 0xa4, 0xd0, 0x9e, 0x06, 0x2a, 0xa5, 0xe4, 60 | 0xa3, 0x90, 0xb0, 0xa5, 0x72, 0xac, 0x0d, 0x2c, 0x02, 0x20]; 61 | 62 | let suite = "OCRA-1:HOTP-SHA1-6:QN08"; 63 | let result = ocra(&suite, &STANDARD_KEY_20, 0, "00000000", NULL, NULL, 0) 64 | assert_eq!(result, Ok(237653)); 65 | 66 | // Attention! PIN must be already hashed! 67 | let suite_c = "OCRA-1:HOTP-SHA256-8:C-QN08-PSHA1"; 68 | let result_c = ocra(&suite_c, &STANDARD_KEY_32, 8, "12345678", PIN_1234_SHA1, NULL, 0); 69 | assert_eq!(result_c, Ok(75011558)); 70 | 71 | let suite_t = "OCRA-1:HOTP-SHA512-8:QN08-T1M"; 72 | let t = 1_206_446_760; // UTC time in seconds 73 | let result_t = ocra(&suite, &STANDARD_KEY_64, 18, "22222222", NULL, NULL, t); 74 | assert_eq!(result_t, Ok(22048402)); 75 | 76 | ### Google Authenticator 77 | 78 | Keys provided by Google are encoded using base32. You'll need to convert them 79 | before passing them to any of the functions in this crate. 80 | 81 | A simple way to do this is using the [base32](https://crates.io/crates/base32/) 82 | crate. 83 | 84 | // assuming AAAAAAAAAAAAAAAA is your key 85 | base32::decode(base32::Alphabet::RFC4648 {padding: false}, "AAAAAAAAAAAAAAAA").unwrap().as_ref() 86 | 87 | And pass the result of that as the key parameter to the HOTP and TOTP 88 | functions. 89 | 90 | ### Misc 91 | 92 | If you don't want to use other crates for hex conversion, this library provides a 93 | convenient function `from_hex()`. This helps with the functions that expect byte 94 | arrays. 95 | 96 | let seed = oath::from_hex("ff").unwrap(); 97 | totp_raw(seed.as_slice(), 6, 0, 30); 98 | 99 | ## Licensing 100 | 101 | This library is licensed under the MIT license. If you're a potential user, or 102 | a current user, and this license causes an issue for you, I'm willing to 103 | consider multi-licensing it. 104 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! rust-oath is a rust implementation of three one-time password generators: 2 | //! [HOTP](https://www.ietf.org/rfc/rfc4226.txt), 3 | //! [TOTP](https://www.ietf.org/rfc/rfc6238.txt), 4 | //! [OCRA](https://www.ietf.org/rfc/rfc6287.txt) 5 | 6 | #![warn(missing_docs)] 7 | 8 | extern crate sha_1 as sha1; 9 | extern crate sha2; 10 | extern crate digest; 11 | extern crate hmac; 12 | // Replace with `hex` crate? 13 | extern crate rustc_hex; 14 | 15 | use sha1::Sha1; 16 | use sha2::Sha256; 17 | use sha2::Sha512; 18 | use hmac::{Hmac, Mac}; 19 | use digest::Digest; 20 | use rustc_hex::{FromHex, ToHex}; 21 | use std::io::Write as Write_io; 22 | 23 | /// `HashType` enum represents possible hashing modes. 24 | pub enum HashType { 25 | /// Sha1 26 | SHA1, 27 | /// Sha2 256 bit mode 28 | SHA256, 29 | /// Sha2 512 bit mode 30 | SHA512 31 | } 32 | 33 | /// This library provides a wrapper around `rustc_hex::from_hex()`. 34 | /// This helps with the functions that expect byte arrays. 35 | /// 36 | /// # Examples 37 | /// 38 | /// ``` 39 | /// extern crate oath; 40 | /// 41 | /// use oath::{totp_raw_now, HashType}; 42 | /// 43 | /// fn main () { 44 | /// let seed = oath::from_hex("ff").unwrap(); 45 | /// totp_raw_now(seed.as_slice(), 6, 0, 30, &HashType::SHA1); 46 | /// } 47 | /// ``` 48 | pub fn from_hex(data: &str) -> Result, &str> { 49 | match data.from_hex() { 50 | Ok(d) => Ok(d), 51 | Err(_) => Err("Unable to decode hex") 52 | } 53 | } 54 | 55 | #[inline] 56 | #[allow(unknown_lints)] //cargo doesn't know identity_op lint 57 | #[allow(identity_op)] //clippy grumbles about (1 * 8) 58 | fn u64_from_be_bytes_4(bytes: &[u8], start: usize) -> u64 { 59 | let mut val = 0u64; 60 | 61 | val += (bytes[start] as u64) << ((3 * 8) as u64); 62 | val += (bytes[start+1] as u64) << ((2 * 8) as u64); 63 | val += (bytes[start+2] as u64) << ((1 * 8) as u64); 64 | val += (bytes[start+3] as u64) << ((0 * 8) as u64); 65 | 66 | val 67 | } 68 | 69 | #[inline] 70 | fn dynamic_truncation(hs: &[u8]) -> u64 { 71 | let offset_bits = (hs[hs.len()-1] & 0xf) as usize; 72 | let p = u64_from_be_bytes_4(hs, offset_bits); 73 | 74 | p & 0x7fffffff 75 | } 76 | 77 | fn hmac_and_truncate(key: &[u8], message: &[u8], 78 | digits: u32) -> u64 { 79 | let mut hmac = Hmac::::new(key); 80 | hmac.input(message); 81 | let result = hmac.result(); 82 | let hs = result.code(); 83 | 84 | dynamic_truncation(hs) % 10_u64.pow(digits) 85 | } 86 | 87 | /// Computes an one-time password using HOTP algorithm. 88 | /// Same as [`hotp`](fn.hotp.html), but expects key to be a `&[u8]`. 89 | /// 90 | /// `key` is a slice, that represents the shared secret; 91 | /// 92 | /// `counter` is a counter. Due to [RFC4226](https://www.ietf.org/rfc/rfc4226.txt) it MUST be synchronized between the 93 | /// HOTP generator (client) and the HOTP validator (server); 94 | /// 95 | /// `digits` - number of digits in output (usually 6 or 8); 96 | /// 97 | /// # Example 98 | /// 99 | /// ``` 100 | /// extern crate oath; 101 | /// 102 | /// use oath::hotp_raw; 103 | /// 104 | /// fn main () { 105 | /// assert_eq!(hotp_raw(b"\xff", 23, 6), 330795); 106 | /// } 107 | /// ``` 108 | pub fn hotp_raw(key: &[u8], counter: u64, digits: u32) -> u64 { 109 | let message = counter.to_be(); 110 | let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&message as *const u64 as *const u8, 8) }; 111 | hmac_and_truncate::(key, msg_ptr, digits) 112 | } 113 | 114 | /// Hi-level function, that computes an one-time password using HOTP algorithm. 115 | /// Same as [`hotp_raw`](fn.hotp_raw.html), but expects key to be a `&str` instead. 116 | /// 117 | /// `key` is a string slice, that represents the shared secret; 118 | /// 119 | /// `counter` is a counter. Due to [RFC4226](https://www.ietf.org/rfc/rfc4226.txt) it MUST be synchronized between the 120 | /// HOTP generator (client) and the HOTP validator (server); 121 | /// 122 | /// `digits` - number of digits in output (usually 6 or 8); 123 | /// 124 | /// # Example 125 | /// 126 | /// ``` 127 | /// extern crate oath; 128 | /// 129 | /// use oath::hotp; 130 | /// 131 | /// fn main () { 132 | /// assert_eq!(hotp("ff", 23, 6).unwrap(), 330795); 133 | /// } 134 | /// ``` 135 | pub fn hotp(key: &str, counter: u64, digits: u32) -> Result { 136 | match key.from_hex() { 137 | Ok(bytes) => Ok(hotp_raw(bytes.as_ref(), counter, digits)), 138 | Err(_) => Err("Unable to parse hex.") 139 | } 140 | } 141 | 142 | /// Low-level function, that computes an one-time password using TOTP algorithm. 143 | /// It's generic over hashing algorithm `D`. 144 | /// 145 | /// `key` is a slice, that represents the shared secret; 146 | /// 147 | /// `digits` - number of digits in output (usually 6 or 8); 148 | /// 149 | /// `epoch` - initial counter time T0 (default value is 0); 150 | /// 151 | /// `time_step` - time step in seconds (default value is 30); 152 | /// 153 | /// `current_time` - current Unix time (in seconds); 154 | /// 155 | /// # Example 156 | /// 157 | /// ``` 158 | /// extern crate sha_1 as sha1; 159 | /// extern crate oath; 160 | /// 161 | /// use sha1::Sha1; 162 | /// use oath::totp_custom; 163 | /// 164 | /// fn main () { 165 | /// assert_eq!(totp_custom::(b"\xff", 6, 0, 1, 23), 330795); 166 | /// } 167 | /// ``` 168 | pub fn totp_custom(key: &[u8], digits: u32, epoch: u64, 169 | time_step: u64, current_time: u64) -> u64 { 170 | let counter: u64 = (current_time - epoch) / time_step; 171 | let message = counter.to_be(); 172 | let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&message as *const u64 as *const u8, 8) }; 173 | hmac_and_truncate::(key, msg_ptr, digits) 174 | } 175 | 176 | /// Computes an one-time password using TOTP algorithm for arbitrary timestamp. 177 | /// Same as [`totp_custom_time`](fn.totp_custom_time.html), but expects key to be a `&[u8]`. 178 | /// 179 | /// `key` is a slice, that represents the shared secret; 180 | /// 181 | /// `digits` - number of digits in output (usually 6 or 8); 182 | /// 183 | /// `epoch` - initial counter time T0 (default value is 0); 184 | /// 185 | /// `time_step` - time step in seconds (default value is 30); 186 | /// 187 | /// `timestamp` - moment of time to be computed (in seconds); 188 | /// 189 | /// # Example 190 | /// 191 | /// ``` 192 | /// extern crate oath; 193 | /// 194 | /// use oath::{totp_raw_custom_time, HashType}; 195 | /// 196 | /// fn main () { 197 | /// totp_raw_custom_time(b"12345678901234567890", 6, 0, 30, 26*365*24*60*60, &HashType::SHA1); 198 | /// } 199 | /// ``` 200 | pub fn totp_raw_custom_time(key: &[u8], digits: u32, epoch: u64, time_step: u64, 201 | timestamp: u64, hash: &HashType) -> u64 { 202 | match *hash { 203 | HashType::SHA1 => totp_custom::(key, digits, epoch, time_step, timestamp), 204 | HashType::SHA256 => totp_custom::(key, digits, epoch, time_step, timestamp), 205 | HashType::SHA512 => totp_custom::(key, digits, epoch, time_step, timestamp), 206 | } 207 | } 208 | 209 | /// Computes an one-time password for this moment using TOTP algorithm. 210 | /// Same as [`totp_now`](fn.totp_now.html), but expects key to be a `&[u8]`. 211 | /// 212 | /// `key` is a slice, that represents the shared secret; 213 | /// 214 | /// `digits` - number of digits in output (usually 6 or 8); 215 | /// 216 | /// `epoch` - initial counter time T0 (default value is 0); 217 | /// 218 | /// `time_step` - time step in seconds (default value is 30); 219 | /// 220 | /// # Example 221 | /// 222 | /// ``` 223 | /// extern crate oath; 224 | /// 225 | /// use oath::{totp_raw_now, HashType}; 226 | /// 227 | /// fn main () { 228 | /// // Return value differs every 30 seconds. 229 | /// totp_raw_now(b"12345678901234567890", 6, 0, 30, &HashType::SHA1); 230 | /// } 231 | /// ``` 232 | pub fn totp_raw_now(key: &[u8], digits: u32, epoch: u64, time_step: u64, hash: &HashType) -> u64 { 233 | use std::time::{UNIX_EPOCH, SystemTime}; 234 | let current_time: u64 = SystemTime::now().duration_since(UNIX_EPOCH) 235 | .expect("Earlier than 1970-01-01 00:00:00 UTC").as_secs(); 236 | totp_raw_custom_time(key, digits, epoch, time_step, current_time, hash) 237 | } 238 | 239 | /// Computes an one-time password using TOTP algorithm for arbitrary timestamp. 240 | /// Same as [`totp_raw_custom_time`](fn.totp_raw_custom_time.html), but expects key to be a `&str` and returns Result. 241 | /// 242 | /// `key` is a string slice, that represents the shared secret; 243 | /// 244 | /// `digits` - number of digits in output; 245 | /// 246 | /// `epoch` - initial counter time T0 (default value is 0); 247 | /// 248 | /// `time_step` - time step in seconds (default value is 30); 249 | /// 250 | /// # Example 251 | /// 252 | /// ``` 253 | /// extern crate oath; 254 | /// 255 | /// use oath::{totp_custom_time, HashType}; 256 | /// 257 | /// fn main () { 258 | /// // Returns TOTP result for 436437456 second after 1 Jan 1970 259 | /// totp_custom_time("0F35", 6, 0, 30, 436437456, &HashType::SHA512); 260 | /// } 261 | /// ``` 262 | pub fn totp_custom_time<'a>(key: &str, digits: u32, epoch: u64, 263 | time_step: u64, timestamp: u64, hash: &HashType) -> Result { 264 | match key.from_hex() { 265 | Ok(bytes) => Ok(totp_raw_custom_time(bytes.as_ref(), digits, epoch, time_step, timestamp, hash)), 266 | Err(_) => Err("Unable to parse hex.") 267 | } 268 | } 269 | 270 | /// Computes an one-time password for current moment of time using TOTP algorithm. 271 | /// Same as [`totp_raw_now`](fn.totp_raw_now.html), but expects key to be a `&str` and returns Result. 272 | /// 273 | /// `key` is a string slice, that represents the shared secret; 274 | /// 275 | /// `digits` - number of digits in output; 276 | /// 277 | /// `epoch` - initial counter time T0 (default value is 0) 278 | /// 279 | /// `time_step` - time step in seconds (default value is 30) 280 | /// 281 | /// # Example 282 | /// 283 | /// ``` 284 | /// extern crate oath; 285 | /// 286 | /// use oath::{totp_now, HashType}; 287 | /// 288 | /// fn main () { 289 | /// // Return value differs every 30 seconds. 290 | /// totp_now("0F35", 6, 0, 30, &HashType::SHA512); 291 | /// } 292 | /// ``` 293 | pub fn totp_now<'a>(key: &str, digits: u32, epoch: u64, 294 | time_step: u64, hash: &HashType) -> Result { 295 | match key.from_hex() { 296 | Ok(bytes) => Ok(totp_raw_now(bytes.as_ref(), digits, epoch, time_step, hash)), 297 | Err(_) => Err("Unable to parse hex.") 298 | } 299 | } 300 | 301 | /// `ocra` is a wrapper over [`ocra_debug`](fn.ocra_debug.html) function. Use this function in production code! 302 | /// ocra function doesn't leak any info about internal errors, because 303 | /// such internal info could be a starting point for hackers. 304 | pub fn ocra(suite: &str, key: &[u8], counter: u64, question: &str, 305 | password: &[u8], session_info: &[u8], num_of_time_steps: u64) -> Result { 306 | ocra_debug(suite, key, counter, question, password, session_info, num_of_time_steps).or(Err(())) 307 | } 308 | 309 | /// `ocra_debug` is an [OCRA](https://tools.ietf.org/html/rfc6287) implementation with 310 | /// detailed errors descriptions. Use it for debugging purpose only! 311 | /// For production code use [`ocra`](fn.ocra.html) function. 312 | /// 313 | /// `suite` is a value representing the suite of operations to compute an OCRA response; 314 | /// 315 | /// `key` is a secret, previously shared between client and server; 316 | /// 317 | /// `counter` optional synchronized between the client and the server u64 counter; 318 | /// 319 | /// `question` mandatory challenge question(s) generated by the parties; 320 | /// 321 | /// `password` optional ALREADY HASHED value of PIN/password that is known to all parties 322 | /// during the execution of the algorithm; 323 | /// 324 | /// `session_info` optional information about the current session; 325 | /// 326 | /// `num_of_time_steps` optional number of time steps since midnight UTC of January 1, 1970; 327 | /// step size is predefined in the suite; 328 | /// 329 | /// # Example 330 | /// 331 | /// ``` 332 | /// extern crate oath; 333 | /// 334 | /// use oath::ocra; 335 | /// 336 | /// fn main () { 337 | /// let suite_c = "OCRA-1:HOTP-SHA256-8:C-QN08-PSHA1"; 338 | /// let STANDARD_KEY_32 = b"12345678901234567890123456789012"; 339 | /// let PIN_1234_SHA1 = &[0x71, 0x10, 0xed, 0xa4, 0xd0, 0x9e, 0x06, 0x2a, 0xa5, 0xe4, 340 | /// 0xa3, 0x90, 0xb0, 0xa5, 0x72, 0xac, 0x0d, 0x2c, 0x02, 0x20]; 341 | /// assert_eq!(ocra(&suite_c, STANDARD_KEY_32, 6, "12345678", PIN_1234_SHA1, &[], 0), Ok(70069104)); 342 | /// } 343 | /// ``` 344 | pub fn ocra_debug(suite: &str, key: &[u8], counter: u64, question: &str, 345 | password: &[u8], session_info: &[u8], num_of_time_steps: u64) -> Result { 346 | let parsed_suite: Vec<&str> = suite.split(':').collect(); 347 | if (parsed_suite.len() != 3) || (parsed_suite[0].to_uppercase() != "OCRA-1") { 348 | return Err("Malformed suite string.".to_string()); 349 | } 350 | 351 | let crypto_function: Vec<&str> = parsed_suite[1].split('-').collect(); 352 | if crypto_function[0].to_uppercase() != "HOTP" { 353 | return Err("Only HOTP crypto function is supported. You requested ".to_string() + crypto_function[0] + "."); 354 | } 355 | 356 | let hotp_sha_type: HashType = match crypto_function[1].to_uppercase().as_str() { 357 | "SHA1" => HashType::SHA1, 358 | "SHA256" => HashType::SHA256, 359 | "SHA512" => HashType::SHA512, 360 | _ => return Err("Unknown hash type. Supported: SHA1/SHA256/SHA512. Requested: ".to_string() + crypto_function[1] + "."), 361 | }; 362 | 363 | let num_of_digits = if crypto_function.len() == 3 { 364 | let temp_num = crypto_function[2].parse().unwrap_or(0); 365 | if temp_num > 10 || temp_num < 4 { 366 | return Err("Number of returned digits should satisfy: 4 <= num <= 10. You requested ".to_string() + crypto_function[2] + "."); 367 | } 368 | temp_num 369 | } else { 370 | 0 371 | }; 372 | 373 | let data_input: Vec<&str> = parsed_suite[2].split('-').collect(); 374 | // Counters 375 | let question_len: usize = 128; 376 | let mut counter_len: usize = 0; 377 | let mut hashed_pin_len: usize = 0; 378 | let mut session_info_len: usize = 0; 379 | let mut timestamp_len: usize = 0; 380 | 381 | let mut parsed_question_type: (QType, usize) = (QType::N, 0); 382 | let mut parsed_pin_sha_type: (HashType, usize); 383 | let mut timestamp_parsed: u64 = 0; 384 | 385 | for p in data_input { 386 | let setting: &[u8] = p.as_bytes(); 387 | match setting[0] { 388 | b'q' | b'Q' => { 389 | match ocra_parse_question(p) { 390 | Ok(expr) => { 391 | parsed_question_type = expr; 392 | // Mutual Challenge-Response tests fails on this verification. 393 | /*if question.len() != parsed_question_type.1 { 394 | return Err("Claimed and real question lengths are different."); 395 | }*/ 396 | }, 397 | Err(err_str) => return Err(err_str + " Can't parse question " + p + "."), 398 | }; 399 | }, 400 | b'c' | b'C' => counter_len = 8, 401 | b'p' | b'P' => { 402 | match parse_pin_sha_type(p) { 403 | Ok(expr) => { 404 | parsed_pin_sha_type = expr; 405 | // Here we don't care about hash type 406 | // because pin already must be hashed. 407 | hashed_pin_len = parsed_pin_sha_type.1; 408 | if password.len() != hashed_pin_len { 409 | return Err("Wrong hashed password length.".to_string()); 410 | } 411 | }, 412 | Err(err_str) => return Err(err_str + " Can't parse hash " + p + "."), 413 | }; 414 | }, 415 | b's' | b'S' => { 416 | match parse_session_info_len(p) { 417 | Ok(value) => session_info_len = value, 418 | Err(err_str) => return Err(err_str + " Wrong session info parameter " + p + "."), 419 | }; 420 | }, 421 | b't' | b'T' => { 422 | match parse_timestamp_format(p) { 423 | Ok(value) => { 424 | timestamp_parsed = num_of_time_steps / (value as u64); 425 | timestamp_len = 8; 426 | }, 427 | Err(err_str) => return Err(err_str + " Wrong timestamp parameter " + p + "."), 428 | }; 429 | }, 430 | _ => return Err("Unknown parameter ".to_string() + p + "."), 431 | } 432 | } 433 | 434 | let full_message_len = suite.len() + 1 + counter_len + question_len + hashed_pin_len + session_info_len + timestamp_len; 435 | let mut current_message_len = suite.len() + 1; 436 | 437 | let mut message: Vec = Vec::with_capacity(full_message_len); 438 | message.extend_from_slice(suite.as_bytes()); 439 | message.push(0u8); //Delimiter. Mandatory! 440 | if counter_len > 0 { 441 | let counter_be = counter.to_be(); 442 | let msg_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(&counter_be as *const u64 as *const u8, 8) }; 443 | message.extend_from_slice(msg_ptr); 444 | current_message_len += counter_len; 445 | } 446 | if parsed_question_type.1 != 0 { 447 | let push_result = push_correct_question(&mut message, parsed_question_type, question); 448 | match push_result { 449 | Ok(_) => { 450 | current_message_len += question_len; 451 | message.resize(current_message_len, 0) 452 | }, 453 | Err(err_str) => return Err(err_str), 454 | } 455 | } else { 456 | return Err("No question parameter specified or question length is 0.".to_string()); 457 | } 458 | if hashed_pin_len > 0 { 459 | message.extend_from_slice(password); 460 | current_message_len += hashed_pin_len; 461 | } 462 | if session_info_len > 0 { 463 | let real_len = session_info.len(); 464 | message.resize(current_message_len + session_info_len - real_len, 0); 465 | message.extend_from_slice(session_info); 466 | //current_message_len += session_info_len; 467 | } 468 | if timestamp_len > 0 { 469 | let timestamp_parsed_be = timestamp_parsed.to_be(); 470 | let timestamp_ptr: &[u8] = unsafe { ::std::slice::from_raw_parts(×tamp_parsed_be as *const u64 as *const u8, 8) }; 471 | message.extend_from_slice(timestamp_ptr); 472 | //current_message_len += timestamp_len; 473 | } 474 | 475 | let result: u64 = match hotp_sha_type { 476 | HashType::SHA1 => hmac_and_truncate::(key, message.as_slice(), num_of_digits), 477 | HashType::SHA256 => hmac_and_truncate::(key, message.as_slice(), num_of_digits), 478 | HashType::SHA512 => hmac_and_truncate::(key, message.as_slice(), num_of_digits), 479 | }; 480 | 481 | Ok(result) 482 | } 483 | 484 | fn parse_session_info_len(session_info: &str) -> Result { 485 | let (_, num) = session_info.split_at(1); 486 | match num { 487 | "064" => Ok(64), 488 | "128" => Ok(128), 489 | "256" => Ok(256), 490 | "512" => Ok(512), 491 | _ => Err("Wrong session info length. Possible values: 064, 128, 256, 512.".to_string()), 492 | } 493 | } 494 | 495 | // To get timestamp for OCRA, divide current UTC time by this coefficient 496 | fn parse_timestamp_format(timestamp: &str) -> Result { 497 | let (_, time_step) = timestamp.split_at(1); 498 | let (num_s, time_type) = time_step.split_at(time_step.len()-1); 499 | let num = num_s.parse::().unwrap_or(0); 500 | if num < 1 || num > 59 { 501 | return Err("Wrong timestamp value.".to_string()); 502 | } 503 | let coefficient: usize; 504 | match time_type { 505 | "S" => coefficient = num, 506 | "M" => coefficient = num * 60, 507 | "H" => { 508 | if num < 49 { 509 | coefficient = num * 60 * 60; 510 | } else { 511 | return Err("Time interval is too big. Use H <= 48.".to_string()); 512 | } 513 | }, 514 | _ => return Err("Can't parse timestamp. S/M/H time intervals are supported.".to_string()), 515 | } 516 | 517 | Ok(coefficient) 518 | } 519 | 520 | fn parse_pin_sha_type(psha: &str) -> Result<(HashType, usize), String> { 521 | let psha_local: String = psha.to_uppercase(); 522 | if psha_local.starts_with("PSHA") { 523 | let (_, num) = psha_local.split_at(4); 524 | match num { 525 | "1" => Ok((HashType::SHA1, 20)), 526 | "256" => Ok((HashType::SHA256, 32)), 527 | "512" => Ok((HashType::SHA512, 64)), 528 | _ => Err("Unknown SHA hash mode.".to_string()), 529 | } 530 | } else { 531 | Err("Unknown hashing algorithm.".to_string()) 532 | } 533 | } 534 | 535 | fn push_correct_question(message: &mut Vec, q_info: (QType, usize), question: &str) -> Result<(), String> { 536 | let (q_type, q_length) = q_info; 537 | match q_type { 538 | QType::A => { 539 | let hex_representation: String = question.as_bytes().to_hex(); 540 | let mut hex_encoded: Vec = hex_representation.from_hex().unwrap(); 541 | message.append(hex_encoded.by_ref()); 542 | }, 543 | QType::N => { 544 | // While RAMP is broken, let's assume, that question numbers will be short 545 | if question.len() > 19 { 546 | // That is the max number len for u64 547 | assert!(false, "Temporary limitation question.len() < 20 is exceeded.".to_string()); 548 | } 549 | let q_as_u64: u64 = question.parse::().unwrap(); 550 | let mut q_as_hex_str: String = format!("{:X}", q_as_u64); 551 | if q_as_hex_str.len() % 2 == 1 { 552 | q_as_hex_str.push('0'); 553 | } 554 | message.append(from_hex(q_as_hex_str.as_str()).unwrap().by_ref()); 555 | 556 | 557 | /* See https://github.com/rust-lang/rust/issues/41793 558 | let q_as_int: Int = Int::from_str_radix(question, 10).expect("Can't parse your numeric question."); 559 | let sign = q_as_int.sign(); 560 | if sign == -1 { 561 | return Err("Question number can't be negative.".to_string()); 562 | } 563 | //Do nothing if sign == 0; 564 | if sign == 1 { 565 | // Let's make some calculations to prevent extra heap allocations. 566 | // from_hex expects string to be even, but ramp's to_str_radix 567 | // can return string with odd length 568 | // bit_length() = floor(log2(abs(self)))+1 569 | let bit_len: u32 = q_as_int.bit_length(); 570 | let num_of_chars = if bit_len % 4 != 0 { 571 | bit_len / 4 + 1 572 | } else { 573 | bit_len / 4 574 | }; 575 | let mut q_as_hex: String = String::with_capacity(num_of_chars as usize); 576 | let write_result = write!(&mut q_as_hex, "{:X}", q_as_int); 577 | match write_result { 578 | Ok(_) => { 579 | // Now tricky part! If length is odd, number must be padded with 0 580 | // on the right. Numeric value will change! 581 | // Padding on the left side (to keep number correct) will produce 582 | // wrong result! 583 | if num_of_chars % 2 == 1 { 584 | q_as_hex.push('0'); 585 | } 586 | message.append(from_hex(q_as_hex.as_str()).unwrap().by_ref()); 587 | }, 588 | Err(_) => return Err("Unexpected error. Can't write to buffer.".to_string()), 589 | } 590 | }*/ 591 | }, 592 | QType::H => { 593 | if q_length % 2 == 0 { 594 | message.append(from_hex(question).unwrap().by_ref()); 595 | } else { 596 | let mut question_owned = String::with_capacity(q_length + 1); 597 | question_owned.push_str(question); 598 | question_owned.push('0'); 599 | message.append(from_hex(question_owned.as_str()).unwrap().by_ref()); 600 | } 601 | }, 602 | }; 603 | 604 | Ok(()) 605 | } 606 | 607 | enum QType {A, N, H} 608 | fn ocra_parse_question(question: &str) -> Result<(QType, usize), String> { 609 | assert_eq!(question.len(), 4); 610 | let (type_str, len_str) = question.split_at(2); 611 | 612 | let data: &[u8] = type_str.as_bytes(); 613 | assert!(data[0] == b'Q' || data[0] == b'q'); 614 | let q_type_result: Result = match data[1] { 615 | b'a' | b'A' => Ok(QType::A), 616 | b'n' | b'N' => Ok(QType::N), 617 | b'h' | b'H' => Ok(QType::H), 618 | _ => Err("This question type is not supported! Use A/N/H, please.".to_string()), 619 | }; 620 | 621 | if q_type_result.is_err() { 622 | return Err(q_type_result.err().unwrap().to_string()); 623 | } 624 | 625 | let q_len_result = len_str.parse::(); 626 | if q_len_result.is_err() { 627 | return Err("Can't parse question length.".to_string()); 628 | } 629 | 630 | let q_len = q_len_result.unwrap(); 631 | if q_len < 4 || q_len > 64 { 632 | return Err("Make sure you request question length such that 4 <= question_length <= 64.".to_string()); 633 | } 634 | 635 | Ok((q_type_result.unwrap(), q_len)) 636 | } -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unknown_lints)] 2 | #![allow(zero_prefixed_literal)] 3 | 4 | extern crate oath; 5 | extern crate sha_1 as sha1; 6 | extern crate sha2; 7 | extern crate digest; 8 | 9 | use oath::*; 10 | use sha1::Sha1; 11 | use sha2::Sha256; 12 | use sha2::Sha512; 13 | use digest::Digest; 14 | 15 | static NULL: &[u8] = &[]; 16 | static STANDARD_KEY_20: &[u8] = &[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 17 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30]; 18 | static STANDARD_KEY_32: &[u8] = &[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 19 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 20 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 21 | 0x31, 0x32]; 22 | static STANDARD_KEY_64: &[u8] = &[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 23 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 24 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 25 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 26 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 27 | 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 28 | 0x31, 0x32, 0x33, 0x34]; 29 | static PIN_1234_SHA1: &[u8] = &[0x71, 0x10, 0xed, 0xa4, 0xd0, 0x9e, 0x06, 0x2a, 0xa5, 0xe4, 30 | 0xa3, 0x90, 0xb0, 0xa5, 0x72, 0xac, 0x0d, 0x2c, 0x02, 0x20]; 31 | #[test] 32 | fn sha1_pin_correct() { 33 | let mut sha: Sha1 = Sha1::default(); 34 | sha.input(b"1234"); 35 | 36 | let result = sha.result(); 37 | 38 | assert_eq!(&PIN_1234_SHA1[..], result.as_slice()); 39 | } 40 | 41 | #[test] 42 | fn test_hotp() { 43 | assert_eq!(hotp_raw(b"\xff", 23, 6), 330795); 44 | assert_eq!(hotp("ff", 23, 6).unwrap(), 330795); 45 | 46 | // test values from RFC 4226 47 | assert_eq!(hotp_raw(b"12345678901234567890", 0, 6), 755224); 48 | assert_eq!(hotp_raw(b"12345678901234567890", 1, 6), 287082); 49 | assert_eq!(hotp_raw(b"12345678901234567890", 2, 6), 359152); 50 | assert_eq!(hotp_raw(b"12345678901234567890", 3, 6), 969429); 51 | assert_eq!(hotp_raw(b"12345678901234567890", 4, 6), 338314); 52 | assert_eq!(hotp_raw(b"12345678901234567890", 5, 6), 254676); 53 | assert_eq!(hotp_raw(b"12345678901234567890", 6, 6), 287922); 54 | assert_eq!(hotp_raw(b"12345678901234567890", 7, 6), 162583); 55 | assert_eq!(hotp_raw(b"12345678901234567890", 8, 6), 399871); 56 | assert_eq!(hotp_raw(b"12345678901234567890", 9, 6), 520489); 57 | } 58 | 59 | 60 | #[test] 61 | fn test_totp() { 62 | let seeds20 = b"12345678901234567890"; 63 | let seeds32 = b"12345678901234567890123456789012"; 64 | let seeds64 = b"1234567890123456789012345678901234567890123456789012345678901234"; 65 | 66 | assert_eq!(totp_custom::(b"\xff", 6, 0, 1, 23), 330795); 67 | 68 | // test values from RFC 6238 69 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 59), 94287082); 70 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 59), 46119246); 71 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 59), 90693936); 72 | 73 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 1111111109), 07081804); 74 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 1111111109), 68084774); 75 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 1111111109), 25091201); 76 | 77 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 1111111111), 14050471); 78 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 1111111111), 67062674); 79 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 1111111111), 99943326); 80 | 81 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 1234567890), 89005924); 82 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 1234567890), 91819424); 83 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 1234567890), 93441116); 84 | 85 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 2000000000), 69279037); 86 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 2000000000), 90698825); 87 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 2000000000), 38618901); 88 | 89 | assert_eq!(totp_custom::(seeds20, 8, 0, 30, 20000000000), 65353130); 90 | assert_eq!(totp_custom::(seeds32, 8, 0, 30, 20000000000), 77737706); 91 | assert_eq!(totp_custom::(seeds64, 8, 0, 30, 20000000000), 47863826); 92 | } 93 | 94 | #[test] 95 | fn test_ocra_20byte_sha1() { 96 | let suite = "OCRA-1:HOTP-SHA1-6:QN08"; 97 | 98 | // Test values from RFC 6287 99 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "00000000", NULL, NULL, 0), Ok(237653)); 100 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "11111111", NULL, NULL, 0), Ok(243178)); 101 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "22222222", NULL, NULL, 0), Ok(653583)); 102 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "33333333", NULL, NULL, 0), Ok(740991)); 103 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "44444444", NULL, NULL, 0), Ok(608993)); 104 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "55555555", NULL, NULL, 0), Ok(388898)); 105 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "66666666", NULL, NULL, 0), Ok(816933)); 106 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "77777777", NULL, NULL, 0), Ok(224598)); 107 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "88888888", NULL, NULL, 0), Ok(750600)); 108 | assert_eq!(ocra(suite, STANDARD_KEY_20, 0, "99999999", NULL, NULL, 0), Ok(294470)); 109 | 110 | // Values, generated from original Java source 111 | let suite2 = "OCRA-1:HOTP-SHA1-6:QH07"; 112 | assert_eq!(ocra(suite2, STANDARD_KEY_20, 0, "153158E", NULL, NULL, 0), Ok(347935)); 113 | assert_eq!(ocra(suite2, STANDARD_KEY_20, 0, "ABC1DEF", NULL, NULL, 0), Ok(857750)); 114 | 115 | let suite3 = "OCRA-1:HOTP-SHA1-6:QH08"; 116 | assert_eq!(ocra(suite3, STANDARD_KEY_20, 0, "F153158E", NULL, NULL, 0), Ok(004133)); 117 | assert_eq!(ocra(suite3, STANDARD_KEY_20, 0, "ABC10DEF", NULL, NULL, 0), Ok(277962)); 118 | 119 | // My values. Could be wrong. 120 | let suite4 = "OCRA-1:HOTP-SHA1-6:QA31"; 121 | assert_eq!(ocra(suite4, STANDARD_KEY_20, 0, "Thanks avacariu for a nice lib!", NULL, NULL, 0), Ok(044742)); 122 | assert_eq!(ocra(suite4, STANDARD_KEY_20, 0, "Hope to see it in crates.io :)", NULL, NULL, 0), Ok(516968)); 123 | } 124 | 125 | #[test] 126 | fn test_ocra_32byte_sha256() { 127 | let suite_c = "OCRA-1:HOTP-SHA256-8:C-QN08-PSHA1"; 128 | let suite = "OCRA-1:HOTP-SHA256-8:QN08-PSHA1"; 129 | 130 | // Test values from RFC 6287 131 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 0, "12345678", PIN_1234_SHA1, NULL, 0), Ok(65347737)); 132 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 1, "12345678", PIN_1234_SHA1, NULL, 0), Ok(86775851)); 133 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 2, "12345678", PIN_1234_SHA1, NULL, 0), Ok(78192410)); 134 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 3, "12345678", PIN_1234_SHA1, NULL, 0), Ok(71565254)); 135 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 4, "12345678", PIN_1234_SHA1, NULL, 0), Ok(10104329)); 136 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 5, "12345678", PIN_1234_SHA1, NULL, 0), Ok(65983500)); 137 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 6, "12345678", PIN_1234_SHA1, NULL, 0), Ok(70069104)); 138 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 7, "12345678", PIN_1234_SHA1, NULL, 0), Ok(91771096)); 139 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 8, "12345678", PIN_1234_SHA1, NULL, 0), Ok(75011558)); 140 | assert_eq!(ocra(suite_c, STANDARD_KEY_32, 9, "12345678", PIN_1234_SHA1, NULL, 0), Ok(08522129)); 141 | 142 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "00000000", PIN_1234_SHA1, NULL, 0), Ok(83238735)); 143 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "11111111", PIN_1234_SHA1, NULL, 0), Ok(01501458)); 144 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "22222222", PIN_1234_SHA1, NULL, 0), Ok(17957585)); 145 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "33333333", PIN_1234_SHA1, NULL, 0), Ok(86776967)); 146 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "44444444", PIN_1234_SHA1, NULL, 0), Ok(86807031)); 147 | } 148 | 149 | #[test] 150 | fn test_ocra_64byte_sha512() { 151 | let suite = "OCRA-1:HOTP-SHA512-8:C-QN08"; 152 | 153 | // Test values from RFC 6287 154 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "00000000", NULL, NULL, 0), Ok(07016083)); 155 | assert_eq!(ocra(suite, STANDARD_KEY_64, 1, "11111111", NULL, NULL, 0), Ok(63947962)); 156 | assert_eq!(ocra(suite, STANDARD_KEY_64, 2, "22222222", NULL, NULL, 0), Ok(70123924)); 157 | assert_eq!(ocra(suite, STANDARD_KEY_64, 3, "33333333", NULL, NULL, 0), Ok(25341727)); 158 | assert_eq!(ocra(suite, STANDARD_KEY_64, 4, "44444444", NULL, NULL, 0), Ok(33203315)); 159 | assert_eq!(ocra(suite, STANDARD_KEY_64, 5, "55555555", NULL, NULL, 0), Ok(34205738)); 160 | assert_eq!(ocra(suite, STANDARD_KEY_64, 6, "66666666", NULL, NULL, 0), Ok(44343969)); 161 | assert_eq!(ocra(suite, STANDARD_KEY_64, 7, "77777777", NULL, NULL, 0), Ok(51946085)); 162 | assert_eq!(ocra(suite, STANDARD_KEY_64, 8, "88888888", NULL, NULL, 0), Ok(20403879)); 163 | assert_eq!(ocra(suite, STANDARD_KEY_64, 9, "99999999", NULL, NULL, 0), Ok(31409299)); 164 | 165 | // Pin must be ignored due to suite settings 166 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "00000000", PIN_1234_SHA1, NULL, 0), Ok(07016083)); 167 | } 168 | 169 | #[test] 170 | fn test_ocra_64byte_sha512_t() { 171 | let suite = "OCRA-1:HOTP-SHA512-8:QN08-T1M"; 172 | // "132d0b6" from RFC with 1M step 173 | let t = 0x132d0b6 * 60; // 1_206_446_760 174 | 175 | // Test values from RFC 6287 176 | // Counter must be ignored. 177 | assert_eq!(ocra(suite, STANDARD_KEY_64, 55, "00000000", NULL, NULL, t), Ok(95209754)); 178 | assert_eq!(ocra(suite, STANDARD_KEY_64, 62, "11111111", NULL, NULL, t), Ok(55907591)); 179 | assert_eq!(ocra(suite, STANDARD_KEY_64, 18, "22222222", NULL, NULL, t), Ok(22048402)); 180 | assert_eq!(ocra(suite, STANDARD_KEY_64, 26, "33333333", NULL, NULL, t), Ok(24218844)); 181 | assert_eq!(ocra(suite, STANDARD_KEY_64, 99, "44444444", NULL, NULL, t), Ok(36209546)); 182 | } 183 | 184 | #[test] 185 | fn test_ocra_32byte_sha256_mutual() { 186 | let server_suite = "OCRA-1:HOTP-SHA256-8:QA08"; 187 | let client_suite = "OCRA-1:HOTP-SHA256-8:QA08"; 188 | 189 | assert_eq!(ocra(server_suite, STANDARD_KEY_32, 0, "CLI22220SRV11110", NULL, NULL, 0), Ok(28247970)); 190 | assert_eq!(ocra(server_suite, STANDARD_KEY_32, 0, "CLI22221SRV11111", NULL, NULL, 0), Ok(01984843)); 191 | assert_eq!(ocra(server_suite, STANDARD_KEY_32, 0, "CLI22222SRV11112", NULL, NULL, 0), Ok(65387857)); 192 | assert_eq!(ocra(server_suite, STANDARD_KEY_32, 0, "CLI22223SRV11113", NULL, NULL, 0), Ok(03351211)); 193 | assert_eq!(ocra(server_suite, STANDARD_KEY_32, 0, "CLI22224SRV11114", NULL, NULL, 0), Ok(83412541)); 194 | 195 | assert_eq!(ocra(client_suite, STANDARD_KEY_32, 0, "SRV11110CLI22220", NULL, NULL, 0), Ok(15510767)); 196 | assert_eq!(ocra(client_suite, STANDARD_KEY_32, 0, "SRV11111CLI22221", NULL, NULL, 0), Ok(90175646)); 197 | assert_eq!(ocra(client_suite, STANDARD_KEY_32, 0, "SRV11112CLI22222", NULL, NULL, 0), Ok(33777207)); 198 | assert_eq!(ocra(client_suite, STANDARD_KEY_32, 0, "SRV11113CLI22223", NULL, NULL, 0), Ok(95285278)); 199 | assert_eq!(ocra(client_suite, STANDARD_KEY_32, 0, "SRV11114CLI22224", NULL, NULL, 0), Ok(28934924)); 200 | } 201 | 202 | #[test] 203 | fn test_ocra_64byte_sha512_mutual() { 204 | let server_suite = "OCRA-1:HOTP-SHA512-8:QA08"; 205 | let client_suite = "OCRA-1:HOTP-SHA512-8:QA08-PSHA1"; 206 | 207 | assert_eq!(ocra(server_suite, STANDARD_KEY_64, 0, "CLI22220SRV11110", NULL, NULL, 0), Ok(79496648)); 208 | assert_eq!(ocra(server_suite, STANDARD_KEY_64, 0, "CLI22221SRV11111", NULL, NULL, 0), Ok(76831980)); 209 | assert_eq!(ocra(server_suite, STANDARD_KEY_64, 0, "CLI22222SRV11112", NULL, NULL, 0), Ok(12250499)); 210 | assert_eq!(ocra(server_suite, STANDARD_KEY_64, 0, "CLI22223SRV11113", NULL, NULL, 0), Ok(90856481)); 211 | assert_eq!(ocra(server_suite, STANDARD_KEY_64, 0, "CLI22224SRV11114", NULL, NULL, 0), Ok(12761449)); 212 | 213 | assert_eq!(ocra(client_suite, STANDARD_KEY_64, 0, "SRV11110CLI22220", PIN_1234_SHA1, NULL, 0), Ok(18806276)); 214 | assert_eq!(ocra(client_suite, STANDARD_KEY_64, 0, "SRV11111CLI22221", PIN_1234_SHA1, NULL, 0), Ok(70020315)); 215 | assert_eq!(ocra(client_suite, STANDARD_KEY_64, 0, "SRV11112CLI22222", PIN_1234_SHA1, NULL, 0), Ok(01600026)); 216 | assert_eq!(ocra(client_suite, STANDARD_KEY_64, 0, "SRV11113CLI22223", PIN_1234_SHA1, NULL, 0), Ok(18951020)); 217 | assert_eq!(ocra(client_suite, STANDARD_KEY_64, 0, "SRV11114CLI22224", PIN_1234_SHA1, NULL, 0), Ok(32528969)); 218 | } 219 | 220 | #[test] 221 | fn test_ocra_32byte_sha256_signature() { 222 | let suite = "OCRA-1:HOTP-SHA256-8:QA08"; 223 | 224 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "SIG10000", NULL, NULL, 0), Ok(53095496)); 225 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "SIG11000", NULL, NULL, 0), Ok(04110475)); 226 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "SIG12000", NULL, NULL, 0), Ok(31331128)); 227 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "SIG13000", NULL, NULL, 0), Ok(76028668)); 228 | assert_eq!(ocra(suite, STANDARD_KEY_32, 0, "SIG14000", NULL, NULL, 0), Ok(46554205)); 229 | } 230 | 231 | #[test] 232 | fn test_ocra_64byte_sha512_signature_t() { 233 | let suite = "OCRA-1:HOTP-SHA512-8:QA10-T1M"; 234 | let timestamp = 0x132d0b6 * 60; // Timestamp from RFC with 1 minute coefficient 235 | 236 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "SIG1000000", NULL, NULL, timestamp), Ok(77537423)); 237 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "SIG1100000", NULL, NULL, timestamp), Ok(31970405)); 238 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "SIG1200000", NULL, NULL, timestamp), Ok(10235557)); 239 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "SIG1300000", NULL, NULL, timestamp), Ok(95213541)); 240 | assert_eq!(ocra(suite, STANDARD_KEY_64, 0, "SIG1400000", NULL, NULL, timestamp), Ok(65360607)); 241 | } 242 | 243 | #[test] 244 | fn negative_tests_ocra() { 245 | assert_eq!(ocra_debug("OCRA-2:HOTP-SHA512-8:QA10-T1M", STANDARD_KEY_64, 0, "SIG1000000", NULL, NULL, 0).expect_err("Test failed!").as_str() 246 | , "Malformed suite string."); 247 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA512-8:QA10:MORE", STANDARD_KEY_64, 0, "SIG1000000", NULL, NULL, 0).expect_err("Test failed!").as_str() 248 | , "Malformed suite string."); 249 | assert_eq!(ocra_debug("OCRA-1:SOTP-SHA512-8:QA10-T1M", STANDARD_KEY_32, 0, "Question#1", NULL, NULL, 0).expect_err("Test failed!").as_str() 250 | , "Only HOTP crypto function is supported. You requested SOTP."); 251 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1024-8:QN08", STANDARD_KEY_32, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 252 | , "Unknown hash type. Supported: SHA1/SHA256/SHA512. Requested: SHA1024."); 253 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-3:QN08", STANDARD_KEY_32, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 254 | , "Number of returned digits should satisfy: 4 <= num <= 10. You requested 3."); 255 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-11:QN08", STANDARD_KEY_32, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 256 | , "Number of returned digits should satisfy: 4 <= num <= 10. You requested 11."); 257 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QR08", STANDARD_KEY_64, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 258 | , "This question type is not supported! Use A/N/H, please. Can't parse question QR08."); 259 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN0F", STANDARD_KEY_64, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 260 | , "Can't parse question length. Can't parse question QN0F."); 261 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN03", STANDARD_KEY_64, 0, "123", NULL, NULL, 0).expect_err("Test failed!").as_str() 262 | , "Make sure you request question length such that 4 <= question_length <= 64. Can't parse question QN03."); 263 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA256-6:QN65", STANDARD_KEY_64, 0, "1234567890", NULL, NULL, 0).expect_err("Test failed!").as_str() 264 | , "Make sure you request question length such that 4 <= question_length <= 64. Can't parse question QN65."); 265 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA256-8:QN08-PMD5", STANDARD_KEY_32, 0, "11111111", PIN_1234_SHA1, NULL, 0).expect_err("Test failed!").as_str() 266 | , "Unknown hashing algorithm. Can't parse hash PMD5."); 267 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA256-8:QN08-PSHA3", STANDARD_KEY_32, 0, "11111111", PIN_1234_SHA1, NULL, 0).expect_err("Test failed!").as_str() 268 | , "Unknown SHA hash mode. Can't parse hash PSHA3."); 269 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA256-8:QN08-PSHA256", STANDARD_KEY_32, 0, "11111111", PIN_1234_SHA1, NULL, 0).expect_err("Test failed!").as_str() 270 | , "Wrong hashed password length."); 271 | // Session info parameter can be "064", not just "64" 272 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN08-S64", STANDARD_KEY_32, 0, "11111111", NULL, NULL, 0).expect_err("Test failed!").as_str() 273 | , "Wrong session info length. Possible values: 064, 128, 256, 512. Wrong session info parameter S64."); 274 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN08-T1D", STANDARD_KEY_32, 0, "11111111", NULL, NULL, 0).expect_err("Test failed!").as_str() 275 | , "Can't parse timestamp. S/M/H time intervals are supported. Wrong timestamp parameter T1D."); 276 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN08-T0M", STANDARD_KEY_64, 0, "11111111", NULL, NULL, 0).expect_err("Test failed!").as_str() 277 | , "Wrong timestamp value. Wrong timestamp parameter T0M."); 278 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN08-T60M", STANDARD_KEY_32, 0, "22222222", NULL, NULL, 0).expect_err("Test failed!").as_str() 279 | , "Wrong timestamp value. Wrong timestamp parameter T60M."); 280 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA1-8:QN08-T49H", STANDARD_KEY_32, 0, "99999999", NULL, NULL, 0).expect_err("Test failed!").as_str() 281 | , "Time interval is too big. Use H <= 48. Wrong timestamp parameter T49H."); 282 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA512-8:QN08-WD40", STANDARD_KEY_32, 0, "12345678", NULL, NULL, 0).expect_err("Test failed!").as_str() 283 | , "Unknown parameter WD40."); 284 | assert_eq!(ocra_debug("OCRA-1:HOTP-SHA512-8:C-T2H", STANDARD_KEY_32, 0, "12345678", NULL, NULL, 456).expect_err("Test failed!").as_str() 285 | , "No question parameter specified or question length is 0."); 286 | } 287 | --------------------------------------------------------------------------------