Add basic cli infrastructure

This commit is contained in:
Bilal Elmoussaoui 2025-02-16 19:30:40 +01:00
parent 1423a48620
commit ac58bd5cf2
12 changed files with 337 additions and 21 deletions

146
Cargo.lock generated
View file

@ -17,6 +17,56 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "apdu-core"
version = "0.4.0"
@ -44,6 +94,61 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cli"
version = "0.0.0"
dependencies = [
"clap",
"pcsc",
"ykoath2",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@ -102,6 +207,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
@ -111,6 +222,12 @@ dependencies = [
"digest",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "iso7816-tlv"
version = "0.4.4"
@ -132,6 +249,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "once_cell"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "ouroboros"
version = "0.18.5"
@ -149,7 +272,7 @@ version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
@ -280,6 +403,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
@ -315,6 +444,12 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
@ -330,6 +465,15 @@ dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"

View file

@ -1,23 +1,10 @@
[dependencies]
apdu-core = "0.4.0"
getrandom = "0.3.1"
hmac = "0.12.1"
iso7816-tlv = "0.4.4"
ouroboros = "0.18.5"
pbkdf2 = {version = "0.12.2", features = ["sha1"]}
pcsc = "2.9.0"
regex = "1.11.1"
sha1 = "0.10.6"
sha2 = "0.10.8"
[workspace]
resolver = "2"
[[example]]
name = "example"
path = "./src/example.rs"
members = [
"lib",
"cli",
]
[package]
name = "ykoath2"
version = "0.1.0"
[workspace.package]
edition = "2021"
authors = ["Grimmauld <grimmauld@grimmauld.de>"]
description = "experiments with smartcards"
license-file = "LICENSE"

8
cli/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "cli"
edition = "2021"
[dependencies]
clap = { version = "4.5", features = [ "cargo", "derive" ] }
pcsc = "2.9.0"
ykoath2 = {path = "../lib"}

140
cli/src/main.rs Normal file
View file

@ -0,0 +1,140 @@
// SPDX-License-Identifier: BSD-3-Clause
use clap::{Args, Parser, Subcommand};
use ykoath2::{
constants::OathType, oath_credential::OathCredential, oath_credential_id::CredentialIDData,
OathSession,
};
// use clap::Parser;
#[derive(Subcommand)]
enum Commands {
#[command(name = "store", about = "Store a credential")]
Store {
#[arg(help = "Credential name")]
name: String,
#[arg(help = "Credential type: Time-based or counter-based")]
oath_type: String,
#[arg(help = "Credential issuer")]
issuer: Option<String>,
#[arg(help = "Credential refresh period if it is time-based")]
period: Option<u8>,
},
#[command(name = "tokens", about = "List all credentials for a device")]
Tokens,
#[command(name = "list", about = "List all connected devices")]
List,
}
#[derive(Parser)]
#[clap(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[command(flatten)]
args: Arguments,
}
#[derive(Args, Debug)]
struct Arguments {
#[arg(name = "device", short, long, global = true, help = "Yubikey device")]
device: Option<String>,
}
fn main() {
let cli = Cli::parse();
// Create a HID API context for detecting devices
let context = pcsc::Context::establish(pcsc::Scope::User).unwrap();
let mut readers_buf = [0; 2048];
let devices = context
.list_readers(&mut readers_buf)
.unwrap()
.into_iter()
.map(|r| r.to_str().unwrap())
.collect::<Vec<&str>>();
// Show message if no YubiKey(s)
if devices.is_empty() {
println!("No yubikeys detected");
std::process::exit(0);
}
match cli.command {
Commands::Tokens => {
let Some(selected_device) = cli.args.device else {
println!("A device is required to store a credential.");
std::process::exit(1);
};
if devices.iter().find(|d| **d == selected_device).is_none() {
println!("{selected_device} was not found.");
std::process::exit(1);
}
let session = OathSession::new(&selected_device).unwrap();
for code in session.list_oath_codes().unwrap() {
let oath_type = if code.oath_type() == OathType::Hotp {
"hotp"
} else {
"totp"
};
println!(
"Name: {}, Issuer: {}, Type: {}",
code.name(),
code.issuer().unwrap_or_default(),
oath_type
);
}
}
Commands::Store {
name,
oath_type,
issuer,
period,
} => {
let Some(selected_device) = cli.args.device else {
println!("A device is required to store a credential.");
std::process::exit(1);
};
if devices.iter().find(|d| **d == selected_device).is_none() {
println!("{selected_device} was not found.");
std::process::exit(1);
}
let session = OathSession::new(&selected_device).unwrap();
session
.put_credential(
OathCredential::new(
&selected_device,
CredentialIDData::new(
&name,
ykoath2::constants::OathType::Totp,
issuer.as_deref(),
None,
),
false,
),
b"some secret",
ykoath2::constants::HashAlgo::Sha256,
6,
None,
)
.unwrap();
}
Commands::List => {
// Print device info for all the YubiKeys we detected
for device in devices {
let session = OathSession::new(device).unwrap();
println!("Device: {device}.");
println!(
"Version: {:#?}.",
session
.version()
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".")
);
}
}
}
}

23
lib/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[dependencies]
apdu-core = "0.4.0"
getrandom = "0.3.1"
hmac = "0.12.1"
iso7816-tlv = "0.4.4"
ouroboros = "0.18.5"
pbkdf2 = {version = "0.12.2", features = ["sha1"]}
pcsc = "2.9.0"
regex = "1.11.1"
sha1 = "0.10.6"
sha2 = "0.10.8"
[[example]]
name = "example"
path = "./src/example.rs"
[package]
name = "ykoath2"
version = "0.1.0"
edition = "2021"
authors = ["Grimmauld <grimmauld@grimmauld.de>"]
description = "experiments with smartcards"
license-file = "LICENSE"

View file

@ -38,6 +38,20 @@ impl Display for CredentialIDData {
}
impl CredentialIDData {
pub fn new(
name: &str,
oath_type: OathType,
issuer: Option<&str>,
period: Option<Duration>,
) -> Self {
Self {
name: name.to_owned(),
oath_type,
issuer: issuer.map(ToOwned::to_owned),
period,
}
}
/// reads id data from tlv data
/// `id_bytes` refers to the byte buffer containing issuer, name and period
/// `oath_type_tag` refers to the tlv tag containing the oath type information