From ec1d1bba0ee3805da3b2492fac30acaa0d7968bc Mon Sep 17 00:00:00 2001 From: Grimmauld Date: Thu, 6 Feb 2025 12:02:12 +0100 Subject: [PATCH] reading oath --- .gitignore | 3 + Cargo.lock | 313 +++++++++++++++++++++++++++++++ Cargo.toml | 15 ++ LICENSE | 11 ++ flake.lock | 89 +++++++++ flake.nix | 71 +++++++ rust-toolchain.toml | 2 + src/args.rs | 15 ++ src/lib_ykoath.rs | 446 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 72 +++++++ treefmt.nix | 7 + 11 files changed, 1044 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 rust-toolchain.toml create mode 100644 src/args.rs create mode 100644 src/lib_ykoath.rs create mode 100644 src/main.rs create mode 100644 treefmt.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a49cdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +result +target +aliases.d diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4c1985f --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f1140c6 --- /dev/null +++ b/Cargo.toml @@ -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 "] +description = "experiments with smartcards" +license-file = "LICENSE" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ede32b --- /dev/null +++ b/LICENSE @@ -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. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c16a022 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..060dacc --- /dev/null +++ b/flake.nix @@ -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 + }; +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..2db731a --- /dev/null +++ b/src/args.rs @@ -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, + + /// save the local environment and reimport it in the ssh session + #[clap(long, default_value_t = false)] + pub keep_env: bool, +} diff --git a/src/lib_ykoath.rs b/src/lib_ykoath.rs new file mode 100644 index 0000000..1dc967e --- /dev/null +++ b/src/lib_ykoath.rs @@ -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>, 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 { + 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 { + 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::(len as u16).unwrap(); + } + + buf.write(value).unwrap(); + buf +} + +fn time_challenge(timestamp: Option) -> Vec { + 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::(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, + 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, 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, 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::() { + 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::() { + 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 { + // 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::(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, + }) + } +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5eab039 --- /dev/null +++ b/src/main.rs @@ -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 = 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); + } + } +} diff --git a/treefmt.nix b/treefmt.nix new file mode 100644 index 0000000..1a29d93 --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,7 @@ +{ ... }: +{ + projectRootFile = "flake.nix"; + programs.nixfmt-rfc-style.enable = true; + programs.rustfmt.enable = true; + programs.toml-sort.enable = true; +}