|
| 1 | +// main.rs |
| 2 | + |
| 3 | +// Copyright (C) 2020-2021 The Nitrocli Developers |
| 4 | +// SPDX-License-Identifier: GPL-3.0-or-later |
| 5 | + |
| 6 | +use std::fs; |
| 7 | +use std::io::Write as _; |
| 8 | +use std::path; |
| 9 | + |
| 10 | +use anyhow::Context as _; |
| 11 | + |
| 12 | +use structopt::StructOpt as _; |
| 13 | + |
| 14 | +// TODO: query from user |
| 15 | +const USER_PIN: &str = "123456"; |
| 16 | + |
| 17 | +#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] |
| 18 | +struct Cache { |
| 19 | + slots: Vec<Slot>, |
| 20 | +} |
| 21 | + |
| 22 | +impl Cache { |
| 23 | + pub fn find_slot(&self, name: &str) -> anyhow::Result<u8> { |
| 24 | + let slots = self |
| 25 | + .slots |
| 26 | + .iter() |
| 27 | + .filter(|s| s.name == name) |
| 28 | + .collect::<Vec<_>>(); |
| 29 | + if slots.len() > 1 { |
| 30 | + Err(anyhow::anyhow!( |
| 31 | + "Found multiple PWS slots with the given name" |
| 32 | + )) |
| 33 | + } else if let Some(slot) = slots.first() { |
| 34 | + Ok(slot.id) |
| 35 | + } else { |
| 36 | + Err(anyhow::anyhow!("Found no PWS slot with the given name")) |
| 37 | + } |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +#[derive(Debug, serde::Deserialize, serde::Serialize)] |
| 42 | +struct Slot { |
| 43 | + name: String, |
| 44 | + id: u8, |
| 45 | +} |
| 46 | + |
| 47 | +/// Access Nitrokey PWS slots by name |
| 48 | +/// |
| 49 | +/// This command caches the names of the PWS slots on a Nitrokey device |
| 50 | +/// and makes it possible to fetch a login or a password from a slot |
| 51 | +/// with a given name without knowing its index. It only queries the |
| 52 | +/// names of the PWS slots if there is no cached data or if the |
| 53 | +/// `--force-update` option is set. The cache includes the Nitrokey's |
| 54 | +/// serial number so that it is possible to use it with multiple |
| 55 | +/// devices. |
| 56 | +#[derive(Debug, structopt::StructOpt)] |
| 57 | +#[structopt(bin_name = "nitrocli pws-cache")] |
| 58 | +struct Args { |
| 59 | + /// Always query the slot data even if it is already cached |
| 60 | + #[structopt(short, long)] |
| 61 | + force_update: bool, |
| 62 | + #[structopt(subcommand)] |
| 63 | + cmd: Command, |
| 64 | +} |
| 65 | + |
| 66 | +#[derive(Debug, structopt::StructOpt)] |
| 67 | +enum Command { |
| 68 | + /// Fetches the login and the password from a PWS slot |
| 69 | + Get(GetArgs), |
| 70 | + /// Fetches the login from a PWS slot |
| 71 | + GetLogin(GetArgs), |
| 72 | + /// Fetches the password from a PWS slot |
| 73 | + GetPassword(GetArgs), |
| 74 | + /// Lists the cached slots and their names |
| 75 | + List, |
| 76 | +} |
| 77 | + |
| 78 | +#[derive(Debug, structopt::StructOpt)] |
| 79 | +struct GetArgs { |
| 80 | + /// The name of the PWS slot to fetch |
| 81 | + name: String, |
| 82 | +} |
| 83 | + |
| 84 | +fn main() -> anyhow::Result<()> { |
| 85 | + let args = Args::from_args(); |
| 86 | + let ctx = nitrocli_ext::Context::from_env()?; |
| 87 | + |
| 88 | + let cache = get_cache(&ctx, args.force_update)?; |
| 89 | + match &args.cmd { |
| 90 | + Command::Get(args) => cmd_get(&ctx, &cache, &args.name)?, |
| 91 | + Command::GetLogin(args) => cmd_get_login(&ctx, &cache, &args.name)?, |
| 92 | + Command::GetPassword(args) => cmd_get_password(&ctx, &cache, &args.name)?, |
| 93 | + Command::List => cmd_list(&cache), |
| 94 | + } |
| 95 | + Ok(()) |
| 96 | +} |
| 97 | + |
| 98 | +fn cmd_get(ctx: &nitrocli_ext::Context, cache: &Cache, slot_name: &str) -> anyhow::Result<()> { |
| 99 | + let slot = cache.find_slot(slot_name)?; |
| 100 | + prepare_pws_get(ctx, slot) |
| 101 | + .arg("--login") |
| 102 | + .arg("--password") |
| 103 | + .spawn() |
| 104 | +} |
| 105 | + |
| 106 | +fn cmd_get_login( |
| 107 | + ctx: &nitrocli_ext::Context, |
| 108 | + cache: &Cache, |
| 109 | + slot_name: &str, |
| 110 | +) -> anyhow::Result<()> { |
| 111 | + let slot = cache.find_slot(slot_name)?; |
| 112 | + prepare_pws_get(ctx, slot) |
| 113 | + .arg("--login") |
| 114 | + .arg("--quiet") |
| 115 | + .spawn() |
| 116 | +} |
| 117 | + |
| 118 | +fn cmd_get_password( |
| 119 | + ctx: &nitrocli_ext::Context, |
| 120 | + cache: &Cache, |
| 121 | + slot_name: &str, |
| 122 | +) -> anyhow::Result<()> { |
| 123 | + let slot = cache.find_slot(slot_name)?; |
| 124 | + prepare_pws_get(ctx, slot) |
| 125 | + .arg("--password") |
| 126 | + .arg("--quiet") |
| 127 | + .spawn() |
| 128 | +} |
| 129 | + |
| 130 | +fn cmd_list(cache: &Cache) { |
| 131 | + println!("slot\tname"); |
| 132 | + for slot in &cache.slots { |
| 133 | + println!("{}\t{}", slot.id, slot.name); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +fn get_cache(ctx: &nitrocli_ext::Context, force_update: bool) -> anyhow::Result<Cache> { |
| 138 | + let mut mgr = nitrokey::take().context("Failed to obtain Nitrokey manager instance")?; |
| 139 | + let mut device = ctx.connect(&mut mgr)?; |
| 140 | + let serial_number = get_serial_number(&device)?; |
| 141 | + let cache_file = ctx.cache_dir().join(&format!("{}.toml", serial_number)); |
| 142 | + |
| 143 | + if cache_file.is_file() && !force_update { |
| 144 | + load_cache(&cache_file) |
| 145 | + } else { |
| 146 | + let cache = get_pws_slots(&mut device)?; |
| 147 | + save_cache(&cache, &cache_file)?; |
| 148 | + Ok(cache) |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +fn load_cache(path: &path::Path) -> anyhow::Result<Cache> { |
| 153 | + let s = fs::read_to_string(path).context("Failed to read cache file")?; |
| 154 | + toml::from_str(&s).context("Failed to parse cache file") |
| 155 | +} |
| 156 | + |
| 157 | +fn save_cache(cache: &Cache, path: &path::Path) -> anyhow::Result<()> { |
| 158 | + if let Some(parent) = path.parent() { |
| 159 | + fs::create_dir_all(parent).context("Failed to create cache parent directory")?; |
| 160 | + } |
| 161 | + let mut f = fs::File::create(path).context("Failed to create cache file")?; |
| 162 | + let data = toml::to_vec(cache).context("Failed to serialize cache")?; |
| 163 | + f.write_all(&data).context("Failed to write cache file")?; |
| 164 | + Ok(()) |
| 165 | +} |
| 166 | + |
| 167 | +fn get_serial_number<'a>(device: &impl nitrokey::Device<'a>) -> anyhow::Result<String> { |
| 168 | + // TODO: Consider using hidapi serial number (if available) |
| 169 | + Ok(device.get_serial_number()?.to_string().to_lowercase()) |
| 170 | +} |
| 171 | + |
| 172 | +fn get_pws_slots<'a>(device: &mut impl nitrokey::GetPasswordSafe<'a>) -> anyhow::Result<Cache> { |
| 173 | + let pws = device |
| 174 | + .get_password_safe(USER_PIN) |
| 175 | + .context("Failed to open password safe")?; |
| 176 | + let slots = pws |
| 177 | + .get_slots() |
| 178 | + .context("Failed to query password safe slots")?; |
| 179 | + let mut cache = Cache::default(); |
| 180 | + for slot in slots { |
| 181 | + if let Some(slot) = slot { |
| 182 | + let id = slot.index(); |
| 183 | + let name = slot |
| 184 | + .get_name() |
| 185 | + .with_context(|| format!("Failed to query name for password slot {}", id))?; |
| 186 | + cache.slots.push(Slot { name, id }); |
| 187 | + } |
| 188 | + } |
| 189 | + Ok(cache) |
| 190 | +} |
| 191 | + |
| 192 | +fn prepare_pws_get(ctx: &nitrocli_ext::Context, slot: u8) -> nitrocli_ext::Nitrocli { |
| 193 | + let mut ncli = ctx.nitrocli(); |
| 194 | + let _ = ncli.args(&["pws", "get"]); |
| 195 | + let _ = ncli.arg(slot.to_string()); |
| 196 | + ncli |
| 197 | +} |
0 commit comments