diff --git a/Cargo.lock b/Cargo.lock index 8776d4e..10900a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi", + "windows-targets", +] + [[package]] name = "heck" version = "0.4.1" @@ -120,6 +132,7 @@ version = "0.1.0" dependencies = [ "apdu-core", "base64", + "getrandom", "hmac", "iso7816-tlv", "ouroboros", @@ -331,6 +344,88 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index a4ecbce..0e45df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [dependencies] apdu-core = "0.4.0" base64 = "0.22.1" +getrandom = "0.3.1" hmac = "0.12.1" iso7816-tlv = "0.4.4" ouroboros = "0.18.5" diff --git a/src/example.rs b/src/example.rs index dd4e68a..3988975 100644 --- a/src/example.rs +++ b/src/example.rs @@ -30,7 +30,12 @@ fn main() { for yubikey in yubikeys { let device_label: &str = yubikey; println!("Found device with label {}", device_label); - let session = OathSession::new(yubikey).unwrap(); + let mut session = OathSession::new(yubikey).unwrap(); + + // session.set_key(&session.derive_key("1234")).unwrap(); + // session.unlock_session(&session.derive_key("1234")).unwrap(); + // session.unset_key().unwrap(); + println!("YubiKey version is {:?}", session.get_version()); for c in session.list_oath_codes().unwrap() { println!("{}", c); @@ -49,7 +54,7 @@ fn main() { println!("No credentials on device {}", device_label); } - std::thread::sleep(std::time::Duration::from_secs(5)); // show refresh is working + std::thread::sleep(std::time::Duration::from_secs(0)); // show refresh is working // Enumerate the OATH codes for oath in codes { diff --git a/src/lib.rs b/src/lib.rs index 314a6ba..e0066a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,10 +32,6 @@ fn _hmac_sha1(key: &[u8], message: &[u8]) -> Vec { mac.finalize().into_bytes().to_vec() } -fn _derive_key(salt: &[u8], passphrase: &str) -> Vec { - pbkdf2::pbkdf2_hmac_array::(passphrase.as_bytes(), salt, 1000).to_vec() -} - fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { if key.len() > algo.digest_size() { algo.get_hash_fun()(key) @@ -44,14 +40,6 @@ fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec { } } -pub struct OathSession { - version: Vec, - salt: Vec, - challenge: Vec, - transaction_context: TransactionContext, - pub name: String, -} - pub struct RefreshableOathCredential<'a> { pub cred: OathCredential, pub code: Option, @@ -130,6 +118,15 @@ impl<'a> RefreshableOathCredential<'a> { } } +pub struct OathSession { + version: Vec, + salt: Vec, + challenge: Option>, + transaction_context: TransactionContext, + pub locked: bool, + pub name: String, +} + impl OathSession { pub fn new(name: &str) -> Result { let transaction_context = TransactionContext::from_name(name)?; @@ -142,7 +139,9 @@ impl OathSession { println!("{:?}: {:?}", tag, data); } + let challenge = info_map.get(&(Tag::Challenge as u8)).map(Vec::to_owned); Ok(Self { + locked: challenge.is_some(), version: info_map .get(&(Tag::Version as u8)) .unwrap_or(&vec![0u8; 0]) @@ -151,10 +150,7 @@ impl OathSession { .get(&(Tag::Name as u8)) .unwrap_or(&vec![0u8; 0]) .to_owned(), - challenge: info_map - .get(&(Tag::Challenge as u8)) - .unwrap_or(&vec![0u8; 0]) - .to_owned(), + challenge, name: name.to_string(), transaction_context, }) @@ -164,6 +160,76 @@ impl OathSession { &self.version } + pub fn unlock_session(&mut self, key: &[u8]) -> Result<(), Error> { + let chal = match self.challenge.to_owned() { + Some(chal) => chal, + None => return Ok(()), + }; + + if !self.locked { + return Ok(()); + } + + let hmac = _hmac_sha1(key, &chal); + let random_chal = getrandom::u64().unwrap().to_be_bytes(); // FIXME: unwrap + let data = &[ + to_tlv(Tag::Response, &hmac), + to_tlv(Tag::Challenge, &random_chal), + ] + .concat(); + let resp = + self.transaction_context + .apdu(0, Instruction::Validate as u8, 0, 0, Some(data))?; + let verification = _hmac_sha1(key, &random_chal); + if tlv_to_map(resp.buf) + .get(&(Tag::Response as u8)) + .map(|v| *v == verification) + .unwrap_or(false) + { + self.locked = false; + Ok(()) + } else { + Err(Error::FailedAuthentication) + } + } + + pub fn set_key(&mut self, key: &[u8]) -> Result<(), Error> { + let random_chal = getrandom::u64().unwrap().to_be_bytes(); // FIXME: unwrap + let hmac = _hmac_sha1(key, &random_chal); + let data = &[ + to_tlv( + Tag::Key, + &[&[(OathType::Totp as u8) | (HashAlgo::Sha1 as u8); 1], key].concat(), + ), + to_tlv(Tag::Challenge, &random_chal), + to_tlv(Tag::Response, &hmac), + ] + .concat(); + self.transaction_context + .apdu(0, Instruction::SetCode as u8, 0, 0, Some(data))?; + let info_buffer = + self.transaction_context + .apdu_read_all(0, INS_SELECT, 0x04, 0, Some(&OATH_AID))?; + let info_map = tlv_to_map(info_buffer); + self.challenge = info_map.get(&(Tag::Challenge as u8)).map(Vec::to_owned); + self.locked = self.challenge.is_some(); + + self.unlock_session(key) + } + + pub fn unset_key(&mut self) -> Result<(), Error> { + self.transaction_context.apdu( + 0, + Instruction::SetCode as u8, + 0, + 0, + Some(&to_tlv(Tag::Key, &[0u8; 0])), + )?; + self.locked = false; + self.challenge = None; + Ok(()) + } + pub fn rename_credential( &self, old: CredentialIDData, @@ -190,6 +256,11 @@ impl OathSession { ) } + pub fn derive_key(&self, passphrase: &str) -> Vec { + pbkdf2::pbkdf2_hmac_array::(passphrase.as_bytes(), &self.salt, 1000) + .to_vec() + } + pub fn calculate_code( &self, cred: OathCredential, diff --git a/src/transaction.rs b/src/transaction.rs index 1e894a8..3952966 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -14,6 +14,7 @@ pub enum Error { Pcsc(pcsc::Error), Parsing(String), DeviceMismatch, + FailedAuthentication, } impl Error { @@ -46,6 +47,7 @@ impl Display for Error { Self::Pcsc(error) => f.write_fmt(format_args!("{}", error)), Self::Parsing(msg) => f.write_str(msg), Self::DeviceMismatch => f.write_str("Devices do not match"), + Error::FailedAuthentication => f.write_str("Authentication failure"), } } }