feat: add passphrase support
Some checks are pending
CI / Check (push) Waiting to run
CI / Test Suite (push) Waiting to run
CI / Rustfmt (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / cargo-deny (push) Waiting to run

This commit is contained in:
Grimmauld 2025-02-15 01:31:40 +01:00
parent ef953aed58
commit 1ffd5b1c87
No known key found for this signature in database
5 changed files with 192 additions and 18 deletions

95
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 {

View file

@ -32,10 +32,6 @@ fn _hmac_sha1(key: &[u8], message: &[u8]) -> Vec<u8> {
mac.finalize().into_bytes().to_vec()
}
fn _derive_key(salt: &[u8], passphrase: &str) -> Vec<u8> {
pbkdf2::pbkdf2_hmac_array::<sha1::Sha1, 16>(passphrase.as_bytes(), salt, 1000).to_vec()
}
fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec<u8> {
if key.len() > algo.digest_size() {
algo.get_hash_fun()(key)
@ -44,14 +40,6 @@ fn _hmac_shorten_key(key: &[u8], algo: HashAlgo) -> Vec<u8> {
}
}
pub struct OathSession {
version: Vec<u8>,
salt: Vec<u8>,
challenge: Vec<u8>,
transaction_context: TransactionContext,
pub name: String,
}
pub struct RefreshableOathCredential<'a> {
pub cred: OathCredential,
pub code: Option<OathCodeDisplay>,
@ -130,6 +118,15 @@ impl<'a> RefreshableOathCredential<'a> {
}
}
pub struct OathSession {
version: Vec<u8>,
salt: Vec<u8>,
challenge: Option<Vec<u8>>,
transaction_context: TransactionContext,
pub locked: bool,
pub name: String,
}
impl OathSession {
pub fn new(name: &str) -> Result<Self, Error> {
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<u8> {
pbkdf2::pbkdf2_hmac_array::<sha1::Sha1, 16>(passphrase.as_bytes(), &self.salt, 1000)
.to_vec()
}
pub fn calculate_code(
&self,
cred: OathCredential,

View file

@ -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"),
}
}
}