diff --git a/CHANGELOG.md b/CHANGELOG.md index 83fc8d97..c1de6d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Unreleased - Added `--usb-path` option that restricts the USB path of the device to connect to - Bumped `structopt` dependency to `0.3.17` +- Added the `fill` command that fills the SD card of a Nitrokey Storage device + with random data + - Added the `termion` dependency in version `1.5.5` 0.3.4 diff --git a/Cargo.lock b/Cargo.lock index 75528cc3..94126a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,15 @@ version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.3.3" @@ -204,9 +213,11 @@ dependencies = [ "nitrokey", "nitrokey-test", "nitrokey-test-state", + "progressing", "regex", "serde", "structopt", + "termion", "toml", ] @@ -257,6 +268,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "proc-macro-error" version = "1.0.2" @@ -292,6 +309,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "progressing" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b7db19a74ba7c34de36558abed080568491d2b8999a34de914b1793b0b4b1b" +dependencies = [ + "log", +] + [[package]] name = "quote" version = "1.0.3" @@ -316,6 +342,15 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +dependencies = [ + "redox_syscall", +] + [[package]] name = "redox_users" version = "0.3.4" @@ -423,6 +458,18 @@ dependencies = [ "syn", ] +[[package]] +name = "termion" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 43ff6cbf..a24cc810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,9 @@ version = "0.1" [dependencies.nitrokey] version = "0.7.1" +[dependencies.progressing] +version = "3.0.2" + [dependencies.serde] version = "1.0" features = ["derive"] @@ -61,6 +64,9 @@ features = ["derive"] version = "0.3.17" default-features = false +[dependencies.termion] +version = "1.5.5" + [dependencies.toml] version = "0.5.6" diff --git a/doc/nitrocli.1 b/doc/nitrocli.1 index 6fb1fd9d..b9e4c857 100644 --- a/doc/nitrocli.1 +++ b/doc/nitrocli.1 @@ -131,6 +131,16 @@ open. .TP \fBnitrocli hidden close Close a hidden volume. +.TP +\fBnitrocli fill\fR +Fills the SD card with random data, overwriting all existing data. +This operation takes about one hour to finish for a 16 GB SD card. +It cannot be cancelled, even if the \fBnitrocli\fR process is terminated before +it finishes. + +This command requires the admin PIN. +To avoid accidental calls of this command, the user has to enter the PIN even +if it has been cached. .SS One-time passwords The Nitrokey Pro and the Nitrokey Storage support the generation of one-time diff --git a/doc/nitrocli.1.pdf b/doc/nitrocli.1.pdf index d98ab41f..557dec79 100644 Binary files a/doc/nitrocli.1.pdf and b/doc/nitrocli.1.pdf differ diff --git a/src/args.rs b/src/args.rs index 80abe17b..4947825c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -79,6 +79,8 @@ Command! { Config(ConfigArgs) => |ctx, args: ConfigArgs| args.subcmd.execute(ctx), /// Interacts with the device's encrypted volume Encrypted(EncryptedArgs) => |ctx, args: EncryptedArgs| args.subcmd.execute(ctx), + /// Fills the SD card with random data + Fill => crate::commands::fill, /// Interacts with the device's hidden volume Hidden(HiddenArgs) => |ctx, args: HiddenArgs| args.subcmd.execute(ctx), /// Lists the attached Nitrokey devices diff --git a/src/commands.rs b/src/commands.rs index ebb28ca1..34b4a82f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -22,6 +22,7 @@ use nitrokey::GetPasswordSafe; use crate::args; use crate::config; +use crate::output; use crate::pinentry; use crate::Context; @@ -461,6 +462,39 @@ pub fn list(ctx: &mut Context<'_>, no_connect: bool) -> anyhow::Result<()> { Ok(()) } +/// Fill the SD card with random data +pub fn fill(ctx: &mut Context<'_>) -> anyhow::Result<()> { + with_storage_device(ctx, |ctx, mut device| { + let pin_entry = pinentry::PinEntry::from(args::PinType::Admin, &device)?; + + // Similar to reset, we want the user to re-enter the admin PIN even if is cached to avoid + // accidental data loss. + pinentry::clear(&pin_entry).context("Failed to clear cached secret")?; + + try_with_pin(ctx, &pin_entry, |pin| { + device.fill_sd_card(&pin).context("Failed to fill SD card") + })?; + + let mut progress_bar = output::ProgressBar::new(); + progress_bar.draw(ctx)?; + while !progress_bar.is_finished() { + use nitrokey::OperationStatus; + + thread::sleep(time::Duration::from_secs(1)); + let status = device + .get_operation_status() + .context("Failed to query operation status")?; + match status { + OperationStatus::Ongoing(progress) => progress_bar.update(progress)?, + OperationStatus::Idle => progress_bar.finish(), + }; + progress_bar.draw(ctx)?; + } + + Ok(()) + }) +} + /// Perform a factory reset. pub fn reset(ctx: &mut Context<'_>) -> anyhow::Result<()> { with_device(ctx, |ctx, mut device| { diff --git a/src/main.rs b/src/main.rs index baad15c2..c0c7da55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,6 +56,7 @@ mod arg_util; mod args; mod commands; mod config; +mod output; mod pinentry; #[cfg(test)] mod tests; @@ -98,6 +99,8 @@ pub struct Context<'io> { pub stdout: &'io mut dyn io::Write, /// The `Write` object used as standard error throughout the program. pub stderr: &'io mut dyn io::Write, + /// Whether `stdout` is a TTY. + pub is_tty: bool, /// The admin PIN, if provided through an environment variable. pub admin_pin: Option, /// The user PIN, if provided through an environment variable. @@ -118,7 +121,12 @@ pub struct Context<'io> { } impl<'io> Context<'io> { - fn from_env(stdout: &'io mut O, stderr: &'io mut E, config: config::Config) -> Context<'io> + fn from_env( + stdout: &'io mut O, + stderr: &'io mut E, + is_tty: bool, + config: config::Config, + ) -> Context<'io> where O: io::Write, E: io::Write, @@ -126,6 +134,7 @@ impl<'io> Context<'io> { Context { stdout, stderr, + is_tty, admin_pin: env::var_os(NITROCLI_ADMIN_PIN), user_pin: env::var_os(NITROCLI_USER_PIN), new_admin_pin: env::var_os(NITROCLI_NEW_ADMIN_PIN), @@ -154,8 +163,9 @@ fn main() { let rc = match config::Config::load() { Ok(config) => { + let is_tty = termion::is_tty(&stdout); let args = env::args().collect::>(); - let ctx = &mut Context::from_env(&mut stdout, &mut stderr, config); + let ctx = &mut Context::from_env(&mut stdout, &mut stderr, is_tty, config); run(ctx, args) } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 00000000..590e0c81 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,106 @@ +// commands.rs + +// Copyright (C) 2020 The Nitrocli Developers +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::Context; + +/// A progress bar that can be printed to an interactive output. +pub struct ProgressBar { + /// Whether to redraw the entire progress bar in the next call to `draw`. + redraw: bool, + /// The current progress of the progress bar (0 <= progress <= 100). + progress: u8, + /// Toogled on every call to `draw` to print a pulsing indicator. + toggle: bool, + /// Whether this progress bar finished. + finished: bool, +} + +impl ProgressBar { + /// Creates a new empty progress bar. + pub fn new() -> ProgressBar { + ProgressBar { + redraw: true, + progress: 0, + toggle: false, + finished: false, + } + } + + /// Whether this progress bar is finished. + pub fn is_finished(&self) -> bool { + self.finished + } + + /// Updates the progress bar with the given progress (0 <= progress <= 100). + pub fn update(&mut self, progress: u8) -> anyhow::Result<()> { + anyhow::ensure!(!self.finished, "Tried to update finished progress bar"); + anyhow::ensure!( + progress <= 100, + "Progress bar value out of range: {}", + progress + ); + if progress != self.progress { + self.redraw = true; + self.progress = progress; + } + self.toggle = !self.toggle; + Ok(()) + } + + /// Finish this progress bar. + /// + /// A finished progress bar may no longer be updated. + pub fn finish(&mut self) { + self.finished = true; + self.redraw = true; + self.progress = 100; + } + + /// Print the progress bar to the stdout set in the given context. + /// + /// On every call of this method (as long as the progress bar is not finished), a pulsing + /// indicator is printed to show that the process is still running. If there was progress since + /// the last call to `draw`, or if this is the first call, this function will also print the + /// progress bar itself. + pub fn draw(&self, ctx: &mut Context<'_>) -> anyhow::Result<()> { + use anyhow::Context as _; + use termion::cursor::DetectCursorPos as _; + use termion::raw::IntoRawMode as _; + + if !ctx.is_tty { + return Ok(()); + } + + let pos = ctx + .stdout + .into_raw_mode() + .context("Failed to activate raw mode")? + .cursor_pos() + .context("Failed to query cursor position")?; + let progress_char = if self.toggle && !self.finished { + "." + } else { + " " + }; + if self.redraw { + use progressing::Baring as _;; + + let mut progress_bar = progressing::mapping::Bar::with_range(0, 100); + progress_bar.set(self.progress); + + print!(ctx, "{}", termion::clear::CurrentLine)?; + print!(ctx, "{}", termion::cursor::Goto(1, pos.1))?; + print!(ctx, " {} {}", progress_char, progress_bar)?; + if self.finished { + println!(ctx)?; + } + } else { + print!(ctx, "{}{}", termion::cursor::Goto(2, pos.1), progress_char)?; + } + + ctx.stdout.flush()?; + Ok(()) + } +} diff --git a/src/redefine.rs b/src/redefine.rs index dad45292..10fb631c 100644 --- a/src/redefine.rs +++ b/src/redefine.rs @@ -14,6 +14,12 @@ macro_rules! println { }; } +macro_rules! print { + ($ctx:expr, $($arg:tt)*) => { + write!($ctx.stdout, $($arg)*) + }; +} + macro_rules! eprintln { ($ctx:expr) => { writeln!($ctx.stderr, "") diff --git a/src/tests/fill.rs b/src/tests/fill.rs new file mode 100644 index 00000000..70ea0812 --- /dev/null +++ b/src/tests/fill.rs @@ -0,0 +1,15 @@ +// fill.rs + +// Copyright (C) 2020 The Nitrocli Developers +// SPDX-License-Identifier: GPL-3.0-or-later + +use super::*; + +// Ignore this test as it takes about one hour to execute +#[ignore] +#[test_device(storage)] +fn fill(model: nitrokey::Model) -> anyhow::Result<()> { + let res = Nitrocli::new().model(model).handle(&["fill"]); + assert!(res.is_ok()); + Ok(()) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 631ffb0d..1871f3c0 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,6 +9,7 @@ use nitrokey_test::test as test_device; mod config; mod encrypted; +mod fill; mod hidden; mod list; mod lock; @@ -98,6 +99,7 @@ impl Nitrocli { let ctx = &mut crate::Context { stdout: &mut stdout, stderr: &mut stderr, + is_tty: false, admin_pin: self.admin_pin.clone(), user_pin: self.user_pin.clone(), new_admin_pin: self.new_admin_pin.clone(),