reading oath

This commit is contained in:
Grimmauld 2025-02-06 12:02:12 +01:00
commit ec1d1bba0e
No known key found for this signature in database
11 changed files with 1044 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
result
target
aliases.d

313
Cargo.lock generated Normal file
View file

@ -0,0 +1,313 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[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 = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "clap"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap-stdin"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1101d998d15574d862ee282bcb93e0cf2d192c2fb12338dec35daa91425769a9"
dependencies = [
"thiserror",
]
[[package]]
name = "clap_builder"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
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",
"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 = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "oath-rs-experiments"
version = "0.1.0"
dependencies = [
"byteorder",
"clap",
"clap-stdin",
"pcsc",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "pcsc"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd833ecf8967e65934c49d3521a175929839bf6d0e497f3bd0d3a2ca08943da"
dependencies = [
"bitflags",
"pcsc-sys",
]
[[package]]
name = "pcsc-sys"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ef017e15d2e5592a9e39a346c1dbaea5120bab7ed7106b210ef58ebd97003"
dependencies = [
"pkg-config",
]
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[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"
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"

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[dependencies]
byteorder = "1.5.0"
clap = {version = "4.5.23", features = ["derive"]}
clap-stdin = "0.6.0"
# serde_json = "1.0.134"
# serde = { version = "1.0", features = ["derive"] }
pcsc = "2.9.0"
[package]
name = "oath-rs-experiments"
version = "0.1.0"
edition = "2021"
authors = ["Grimmauld <grimmauld@grimmauld.de>"]
description = "experiments with smartcards"
license-file = "LICENSE"

11
LICENSE Normal file
View file

@ -0,0 +1,11 @@
Copyright 2024 Grimmauld
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

89
flake.lock generated Normal file
View file

@ -0,0 +1,89 @@
{
"nodes": {
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1738765515,
"narHash": "sha256-/fN8eSCHWbjOPOe+rbJWfWrtOdFMElJW+L1y2Cq32bY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ccfae3057498f5a740be4c5a13aa800813a13084",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixpkgs-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"nix-github-actions": "nix-github-actions",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1738808867,
"narHash": "sha256-m5rbY/ck0NAlfSBxo++vl7EZn8fkZ02H3kGGc7q883c=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ae46f37fb727030ddc2ef65a675b751484c90032",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1738680491,
"narHash": "sha256-8X7tR3kFGkE7WEF5EXVkt4apgaN85oHZdoTGutCFs6I=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "64dbb922d51a42c0ced6a7668ca008dded61c483",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

71
flake.nix Normal file
View file

@ -0,0 +1,71 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
nix-github-actions = {
url = "github:nix-community/nix-github-actions";
inputs.nixpkgs.follows = "nixpkgs";
};
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
nixpkgs,
self,
nix-github-actions,
rust-overlay,
treefmt-nix,
...
}:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
genPkgs =
system:
import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
forAllPkgs = f: forAllSystems (system: f (genPkgs system));
treefmtEval = forAllPkgs (pkgs: treefmt-nix.lib.evalModule pkgs ./treefmt.nix);
in
{
devShells = forAllPkgs (pkgs: {
default = pkgs.mkShell {
nativeBuildInputs = [
];
buildInputs = with pkgs; [
pkg-config
pcsclite.dev
rustup
(pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
];
};
});
packages = forAllPkgs (pkgs: {
# default = ;
});
formatter = forAllSystems (system: treefmtEval.${system}.config.build.wrapper);
checks = forAllSystems (system: {
formatting = treefmtEval.${system}.config.build.check self;
});
githubActions = nix-github-actions.lib.mkGithubMatrix {
checks = nixpkgs.lib.getAttrs [ "x86_64-linux" ] self.checks;
}; # todo: figure out testing on aarch64-linux
};
}

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

15
src/args.rs Normal file
View file

@ -0,0 +1,15 @@
// SPDX-License-Identifier: BSD-3-Clause
use clap::Parser;
use clap_stdin::MaybeStdin;
#[derive(Debug, Parser)]
#[clap(name="ssg-sudo-shim", version=env!("CARGO_PKG_VERSION"),about=env!("CARGO_PKG_DESCRIPTION"), author=env!("CARGO_PKG_AUTHORS"))]
pub struct Cli {
/// The desktop file to search for
pub cmd: MaybeStdin<String>,
/// save the local environment and reimport it in the ssh session
#[clap(long, default_value_t = false)]
pub keep_env: bool,
}

446
src/lib_ykoath.rs Normal file
View file

@ -0,0 +1,446 @@
/// Utilities for interacting with YubiKey OATH/TOTP functionality
extern crate pcsc;
extern crate byteorder;
use std::ffi::{CString};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read, Write};
use std::time::{SystemTime};
pub type DetectResult<'a> = Result<Vec<YubiKey<'a>>, pcsc::Error>;
pub const INS_SELECT: u8 = 0xa4;
pub const OATH_AID: [u8; 7] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01];
pub enum ErrorResponse {
NoSpace = 0x6a84,
CommandAborted = 0x6f00,
InvalidInstruction = 0x6d00,
AuthRequired = 0x6982,
WrongSyntax = 0x6a80,
GenericError = 0x6581,
NoSuchObject = 0x6984,
}
pub enum SuccessResponse {
MoreData = 0x61,
Okay = 0x9000,
}
pub fn format_code(code: u32, digits: OathDigits) -> String {
let mut code_string = code.to_string();
match digits {
OathDigits::Six => {
if code_string.len() <= 6 {
format!("{:0>6}", code_string)
} else {
code_string.split_off(code_string.len() - 6)
}
},
OathDigits::Eight => {
if code_string.len() <= 8 {
format!("{:0>8}", code_string)
} else {
code_string.split_off(code_string.len() - 8)
}
},
}
}
fn to_error_response(sw1: u8, sw2: u8) -> Option<String> {
let code: usize = (sw1 as usize | sw2 as usize) << 8;
match code {
code if code == ErrorResponse::GenericError as usize => {
Some(String::from("Generic error"))
},
code if code == ErrorResponse::NoSpace as usize => {
Some(String::from("No space on device"))
},
code if code == ErrorResponse::CommandAborted as usize => {
Some(String::from("Command was aborted"))
},
code if code == ErrorResponse::AuthRequired as usize => {
Some(String::from("Authentication required"))
},
code if code == ErrorResponse::WrongSyntax as usize => {
Some(String::from("Wrong syntax"))
},
code if code == ErrorResponse::InvalidInstruction as usize => {
Some(String::from("Invalid instruction"))
},
code if code == SuccessResponse::Okay as usize => {
None
},
sw1 if sw1 == SuccessResponse::MoreData as usize => {
None
},
_ => {
Some(String::from("Unknown error"))
},
}
}
fn to_tlv(tag: Tag, value: &[u8]) -> Vec<u8> {
let mut buf = vec![tag as u8];
let len = value.len();
if len < 0x80 {
buf.push(len as u8);
} else if len < 0xff {
buf.push(0x81);
buf.push(len as u8);
} else {
buf.push(0x82);
buf.write_u16::<BigEndian>(len as u16).unwrap();
}
buf.write(value).unwrap();
buf
}
fn time_challenge(timestamp: Option<SystemTime>) -> Vec<u8> {
let mut buf = Vec::new();
let ts = match timestamp {
Some(datetime) => {
datetime
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30
}
None => {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30
}
};
buf.write_u64::<BigEndian>(ts).unwrap();
buf
}
pub enum Instruction {
Put = 0x01,
Delete = 0x02,
SetCode = 0x03,
Reset = 0x04,
List = 0xa1,
Calculate = 0xa2,
Validate = 0xa3,
CalculateAll = 0xa4,
SendRemaining = 0xa5,
}
#[repr(u8)]
pub enum Mask {
Algo = 0x0f,
Type = 0xf0,
}
#[repr(u8)]
pub enum Tag {
Name = 0x71,
NameList = 0x72,
Key = 0x73,
Challenge = 0x74,
Response = 0x75,
TruncatedResponse = 0x76,
Hotp = 0x77,
Property = 0x78,
Version = 0x79,
Imf = 0x7a,
Algorithm = 0x7b,
Touch = 0x7c,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum OathAlgo {
Sha1 = 0x01,
Sha256 = 0x02,
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
pub enum OathType {
Totp = 0x10,
Hotp = 0x20,
}
#[derive(Debug, PartialEq)]
pub struct OathCredential {
pub name: String,
pub code: OathCode,
// TODO: Support this stuff
// pub oath_type: OathType,
// pub touch: bool,
// pub algo: OathAlgo,
// pub hidden: bool,
// pub steam: bool,
}
impl OathCredential {
pub fn new(name: &str, code: OathCode) -> OathCredential {
OathCredential {
name: name.to_string(),
code: code,
// oath_type: oath_type,
// touch: touch,
// algo: algo,
// hidden: name.starts_with("_hidden:"),
// steam: name.starts_with("Steam:"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OathDigits {
Six = 6,
Eight = 8,
}
#[derive(Debug, PartialEq)]
pub struct OathCode {
pub digits: OathDigits,
pub value: u32,
// pub expiration: u32,
// pub steam: bool,
}
pub struct ApduResponse {
pub buf: Vec<u8>,
pub sw1: u8,
pub sw2: u8,
}
pub struct YubiKey<'a> {
pub name: &'a str,
}
impl<'a> YubiKey<'a> {
/// Read the OATH codes from the device
pub fn get_oath_codes(&self) -> Result<Vec<OathCredential>, String>{
// Establish a PC/SC context
let ctx = match pcsc::Context::establish(pcsc::Scope::User) {
Ok(ctx) => ctx,
Err(err) => return Err(format!("{}", err)),
};
// Connect to the card
let mut card = match ctx.connect(
&CString::new(self.name).unwrap(),
pcsc::ShareMode::Shared,
pcsc::Protocols::ANY
) {
Ok(card) => card,
Err(err) => return Err(format!("{}", err)),
};
// Create a transaction context
let tx = match card.transaction() {
Ok(tx) => tx,
Err(err) => return Err(format!("{}", err)),
};
// Switch to the OATH applet
if let Err(e) = self.apdu(&tx, 0, INS_SELECT, 0x04, 0, Some(&OATH_AID)) {
return Err(format!("{}", e));
}
// Store the response buffer
let mut response_buf = Vec::new();
// Request OATH codes from device
let response = self.apdu(&tx, 0, Instruction::CalculateAll as u8, 0,
0x01, Some(&to_tlv(Tag::Challenge,
&time_challenge(Some(SystemTime::now())))));
// Handle errors from command
match response {
Ok(resp) => {
let mut sw1 = resp.sw1;
let mut sw2 = resp.sw2;
response_buf.extend(resp.buf);
while sw1 == (SuccessResponse::MoreData as u8) {
let ins = Instruction::SendRemaining as u8;
match self.apdu(&tx, 0, ins, 0, 0, None) {
Ok(more_resp) => {
sw1 = more_resp.sw1;
sw2 = more_resp.sw2;
response_buf.extend(more_resp.buf);
},
Err(e) => {
return Err(format!("{}", e));
},
}
}
if let Some(msg) = to_error_response(sw1, sw2) {
return Err(format!("{}", msg));
}
return Ok(self.parse_list(&response_buf).unwrap());
},
Err(e) => {
return Err(format!("{}", e));
}
}
}
/// Accepts a raw byte buffer payload and parses it
pub fn parse_list(&self, b: &[u8]) -> Result<Vec<OathCredential>, String> {
let mut rdr = Cursor::new(b);
let mut results = Vec::new();
loop {
if let Err(_) = rdr.read_u8() {
break;
};
let mut len: u16 = match rdr.read_u8() {
Ok(len) => len as u16,
Err(_) => break,
};
if len > 0x80 {
let n_bytes = len - 0x80;
if n_bytes == 1 {
len = match rdr.read_u8() {
Ok(len) => len as u16,
Err(_) => break,
};
} else if n_bytes == 2 {
len = match rdr.read_u16::<BigEndian>() {
Ok(len) => len,
Err(_) => break,
};
}
}
let mut name = Vec::with_capacity(len as usize);
unsafe {
name.set_len(len as usize);
}
if let Err(_) = rdr.read_exact(&mut name) {
break;
};
rdr.read_u8().unwrap(); // TODO: Don't discard the response tag
rdr.read_u8().unwrap(); // TODO: Don't discard the response lenght + 1
let digits = match rdr.read_u8() {
Ok(6) => OathDigits::Six,
Ok(8) => OathDigits::Eight,
Ok(_) => break,
Err(_) => break,
};
let value = match rdr.read_u32::<BigEndian>() {
Ok(val) => val,
Err(_) => break,
};
results.push(OathCredential::new(
&String::from_utf8(name).unwrap(),
OathCode { digits, value }
));
}
Ok(results)
}
/// Sends the APDU package to the device
pub fn apdu(
&self,
tx: &pcsc::Transaction,
class: u8,
instruction: u8,
parameter1: u8,
parameter2: u8,
data: Option<&[u8]>
) -> Result<ApduResponse, pcsc::Error> {
// Create a container for the transaction payload
let mut tx_buf = Vec::new();
// Construct an empty buffer to hold the response
let mut rx_buf = [0; pcsc::MAX_BUFFER_SIZE];
// Number of bytes of data
let nc = match data {
Some(ref data) => data.len(),
None => 0,
};
// Construct and attach the header
tx_buf.push(class);
tx_buf.push(instruction);
tx_buf.push(parameter1);
tx_buf.push(parameter2);
// Construct and attach the data's byte count
if nc > 255 {
tx_buf.push(0);
tx_buf.write_u16::<BigEndian>(nc as u16).unwrap();
} else {
tx_buf.push(nc as u8);
}
// Attach the data itself if included
if let Some(data) = data {
tx_buf.write(data).unwrap();
}
// DEBUG
{
let mut s = String::new();
for byte in &tx_buf {
s += &format!("{:02X} ", byte);
}
println!("DEBUG (SEND) >> {}", s);
}
// Write the payload to the device and error if there is a problem
let rx_buf = match tx.transmit(&tx_buf, &mut rx_buf) {
Ok(slice) => slice,
Err(err) => return Err(err),
};
// DEBUG
{
let mut s = String::new();
for byte in &rx_buf.to_vec() {
s += &format!("{:02X} ", byte);
}
println!("DEBUG (RECV) << {}", s);
}
let sw1 = match rx_buf.get((rx_buf.len() - 2) as usize) {
Some(sw1) => sw1,
None => return Err(pcsc::Error::UnknownError),
};
let sw2 = match rx_buf.get((rx_buf.len() - 1) as usize) {
Some(sw2) => sw2,
None => return Err(pcsc::Error::UnknownError),
};
let mut buf = rx_buf.to_vec();
buf.truncate(rx_buf.len() - 2);
Ok(ApduResponse {
buf,
sw1: *sw1,
sw2: *sw2,
})
}
}

72
src/main.rs Normal file
View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: BSD-3-Clause
mod args;
mod lib_ykoath;
use pcsc;
use std::process;
// use crate::args::Cli;
// use clap::Parser;
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 readers = context.list_readers(&mut readers_buf).unwrap();
// Initialize a vector to track all our detected devices
let mut yubikeys: Vec<lib_ykoath::YubiKey> = Vec::new();
// Iterate over the connected USB devices
for reader in readers {
yubikeys.push(lib_ykoath::YubiKey{
name: reader.to_str().unwrap(),
});
}
// Show message if no YubiKey(s)
if yubikeys.len() == 0 {
println!("No yubikeys detected");
process::exit(0);
}
// Print device info for all the YubiKeys we detected
for yubikey in yubikeys {
let device_label: String = yubikey.name.to_owned();
println!("Found device with label {}", device_label);
let codes = match yubikey.get_oath_codes() {
Ok(codes) => codes,
Err(e) => {
println!("ERROR {}", e);
continue;
},
};
// Show message is node codes found
if codes.len() == 0 {
println!("No credentials on device {}", device_label);
}
// Enumerate the OATH codes
for oath in codes {
let code = lib_ykoath::format_code(oath.code.value, oath.code.digits);
let name_clone = oath.name.clone();
let mut label_vec: Vec<&str> = name_clone.split(":").collect();
let mut code_entry_label: String = String::from(
label_vec.remove(0)
);
if label_vec.len() > 0 {
code_entry_label.push_str(" (");
code_entry_label.push_str(&label_vec.join(""));
code_entry_label.push_str(") ");
}
code_entry_label.push_str(&code.clone().to_owned());
println!("Found OATH label: {}", code_entry_label);
}
}
}

7
treefmt.nix Normal file
View file

@ -0,0 +1,7 @@
{ ... }:
{
projectRootFile = "flake.nix";
programs.nixfmt-rfc-style.enable = true;
programs.rustfmt.enable = true;
programs.toml-sort.enable = true;
}