diff options
| -rw-r--r-- | nitrocli/README.md | 61 | ||||
| -rw-r--r-- | nitrocli/src/crc32.rs | 88 | ||||
| -rw-r--r-- | nitrocli/src/main.rs | 13 | ||||
| -rw-r--r-- | nitrocli/src/nitrokey.rs | 149 | ||||
| -rw-r--r-- | nitrocli/src/pinentry.rs | 18 | 
5 files changed, 316 insertions, 13 deletions
| diff --git a/nitrocli/README.md b/nitrocli/README.md new file mode 100644 index 0000000..5ec814e --- /dev/null +++ b/nitrocli/README.md @@ -0,0 +1,61 @@ +nitrocli +======== + +**nitrocli** is a program that provides a command line interface for +certain commands on the [Nitrokey Storage][nitrokey] device. + +The following commands are currently supported: +- open: Open the encrypted volume. The user PIN needs to be entered. +- close: Close the encrypted volume. + + +Usage +----- + +Usage is as simple as providing the name of the respective command as a +parameter, e.g.: +```bash +# Open the nitrokey's encrypted volume. +$ nitrocli open +# Close it again. +$ nitrocli close +``` + + +Installation +------------ + +The following dependencies are required: +- **hidapi**: In order to provide USB access this library is used. +- **GnuPG**: The `gpg-connect-agent` program allows the user to enter +             PINs. + +#### From Source +In order to compile the program the `hid` crate needs to be available +which allows to access the nitrokey as a USB HID device. This crate and +its dependencies are contained in the form of subrepos in compatible and +tested versions. Cargo is required to build the program. + +The build is as simple as running: +```bash +$ cargo build --release +``` + +It is recommended that the resulting executable be installed in a +directory accessible via the `PATH` environment variable. + +#### From Crates.io +**nitrocli** is [published][nitrocli-cratesio] on crates.io. If an +installation from the checked-out source code is not desired, a +quick-and-dirty local installation can happen via: +```bash +$ cargo install nitrocli --root=$PWD/nitrocli +``` + +#### Via Packages +If you are using [Gentoo Linux](https://www.gentoo.org/), there is an +[ebuild](https://github.com/d-e-s-o/nitrocli-ebuild) available that can +be used directly. + +[nitrokey]: https://www.nitrokey.com/news/2016/nitrokey-storage-available +[nitrocli-cratesio]: https://crates.io/crates/nitrocli diff --git a/nitrocli/src/crc32.rs b/nitrocli/src/crc32.rs new file mode 100644 index 0000000..8431db5 --- /dev/null +++ b/nitrocli/src/crc32.rs @@ -0,0 +1,88 @@ +// crc32.rs + +// ************************************************************************* +// * Copyright (C) 2017 Daniel Mueller (deso@posteo.net)                   * +// *                                                                       * +// * This program is free software: you can redistribute it and/or modify  * +// * it under the terms of the GNU General Public License as published by  * +// * the Free Software Foundation, either version 3 of the License, or     * +// * (at your option) any later version.                                   * +// *                                                                       * +// * This program is distributed in the hope that it will be useful,       * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of        * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         * +// * GNU General Public License for more details.                          * +// *                                                                       * +// * You should have received a copy of the GNU General Public License     * +// * along with this program.  If not, see <http://www.gnu.org/licenses/>. * +// ************************************************************************* + +/// Polynomial used in STM32. +const CRC32_POLYNOMIAL: u32 = 0x04c11db7; + + +fn crc32(mut crc: u32, data: u32) -> u32 { +  crc = crc ^ data; + +  for _ in 0..32 { +    if crc & 0x80000000 != 0 { +      crc = (crc << 1) ^ CRC32_POLYNOMIAL; +    } else { +      crc = crc << 1; +    } +  } +  return crc; +} + + +/// Retrieve a u32 slice of the 'data' part. +/// +/// Note that the size of the supplied data has to be a multiple of 4 +/// bytes. +fn as_slice_u32(data: &[u8]) -> &[u32] { +  assert!(data.len() % ::std::mem::size_of::<u32>() == 0); + +  unsafe { +    let ptr = data.as_ptr() as *const u32; +    let len = data.len() / ::std::mem::size_of::<u32>(); +    return ::std::slice::from_raw_parts(ptr, len); +  } +} + + +/// Calculate the CRC of a byte slice. +pub fn crc(data: &[u8]) -> u32 { +  let mut crc = 0xffffffff; +  let data = as_slice_u32(data); + +  for i in 0..data.len() { +    crc = crc32(crc, data[i]); +  } +  return crc; +} + + +#[cfg(test)] +mod tests { +  use super::*; + +  #[test] +  fn test_crc32() { +    let mut crc = 0; + +    // The expected values were computed with the original function. +    crc = crc32(crc, 0xdeadbeef); +    assert_eq!(crc, 0x46dec763); + +    crc = crc32(crc, 42); +    assert_eq!(crc, 0x7e579b45); +  } + +  #[test] +  fn test_crc() { +    let data = &"thisisatextthatistobecrced..".to_string().into_bytes(); +    let crc = crc(data); + +    assert_eq!(crc, 0x469db4ee); +  } +} diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs index bdbfe37..700204d 100644 --- a/nitrocli/src/main.rs +++ b/nitrocli/src/main.rs @@ -26,8 +26,10 @@  extern crate hid as libhid; +mod crc32;  mod error;  mod nitrokey; +mod pinentry;  use error::Error;  use std::process; @@ -54,7 +56,11 @@ fn nitrokey_do(function: &NitroFunc) -> Result<()> {  /// Open the encrypted volume on the nitrokey.  fn open() -> Result<()> {    return nitrokey_do(&|handle| { -    println!("Found nitrokey. Opening encrypted volume..."); +    let passphrase = pinentry::inquire_passphrase()?; +    let payload = nitrokey::EnableEncryptedVolumeCommand::new(&passphrase); +    let report = nitrokey::Report::from(payload); + +    handle.feature().send_to(0, report.as_ref())?;      return Ok(());    });  } @@ -63,7 +69,10 @@ fn open() -> Result<()> {  /// Close the previously opened encrypted volume.  fn close() -> Result<()> {    return nitrokey_do(&|handle| { -    println!("Found nitrokey. Closing encrypted volume..."); +    let payload = nitrokey::DisableEncryptedVolumeCommand::new(); +    let report = nitrokey::Report::from(payload); + +    handle.feature().send_to(0, report.as_ref())?;      return Ok(());    });  } diff --git a/nitrocli/src/nitrokey.rs b/nitrocli/src/nitrokey.rs index 763bc4c..0b055fe 100644 --- a/nitrocli/src/nitrokey.rs +++ b/nitrocli/src/nitrokey.rs @@ -17,8 +17,157 @@  // * along with this program.  If not, see <http://www.gnu.org/licenses/>. *  // ************************************************************************* +use crc32::crc; +use std::cmp; +use std::mem; +  // The Nitrokey Storage vendor ID.  pub const VID: u16 = 0x20A0;  // The Nitrokey Storage product ID.  pub const PID: u16 = 0x4109; + + +#[derive(Debug)] +#[derive(PartialEq)] +#[repr(u8)] +pub enum Command { +  // The command to enable the encrypted volume. +  EnableEncryptedVolume = 0x20, +  // The command to disable the encrypted volume. +  DisableEncryptedVolume = 0x21, +} + + +/// A report is the entity we send to the Nitrokey Storage HID. +/// +/// A report is always 64 bytes in size. The last four bytes comprise a +/// CRC of the actual payload. Note that when sending or receiving a +/// report it usually is preceded by a one byte report ID. This report +/// ID is zero here and not represented in the actual report object in +/// our design. +#[repr(packed)] +pub struct Report<Payload> +  where Payload: AsRef<[u8]>, +{ +  // The actual payload data. A report may encapsulate a command to send +  // to the stick or a response to receive from it. +  pub data: Payload, +  pub crc: u32, +} + + +impl<P> AsRef<[u8]> for Report<P> +  where P: AsRef<[u8]>, +{ +  fn as_ref(&self) -> &[u8] { +    unsafe { return mem::transmute::<&Report<P>, &[u8; 64]>(self) }; +  } +} + + +impl<P> From<P> for Report<P> +  where P: AsRef<[u8]>, +{ +  fn from(payload: P) -> Report<P> { +    let crc = crc(payload.as_ref()); +    return Report { +      data: payload, +      crc: crc, +    }; +  } +} + + +#[allow(dead_code)] +#[repr(packed)] +pub struct EnableEncryptedVolumeCommand { +  command: Command, +  // The kind of password. Unconditionally 'P' because the User PIN is +  // used to enable the encrypted volume. +  kind: u8, +  // The password has a maximum length of twenty characters. +  password: [u8; 20], +  padding: [u8; 38], +} + + +impl EnableEncryptedVolumeCommand { +  pub fn new(password: &Vec<u8>) -> EnableEncryptedVolumeCommand { +    let mut report = EnableEncryptedVolumeCommand { +      command: Command::EnableEncryptedVolume, +      kind: 'P' as u8, +      password: [0; 20], +      padding: [0; 38], +    }; + +    debug_assert!(password.len() <= report.password.len()); + +    let len = cmp::min(report.password.len(), password.len()); +    report.password[..len].copy_from_slice(&password[..len]); +    return report; +  } +} + +impl AsRef<[u8]> for EnableEncryptedVolumeCommand { +  fn as_ref(&self) -> &[u8] { +    unsafe { return mem::transmute::<&EnableEncryptedVolumeCommand, &[u8; 60]>(self) }; +  } +} + + +#[allow(dead_code)] +#[repr(packed)] +pub struct DisableEncryptedVolumeCommand { +  command: Command, +  padding: [u8; 59], +} + +impl DisableEncryptedVolumeCommand { +  pub fn new() -> DisableEncryptedVolumeCommand { +    return DisableEncryptedVolumeCommand { +      command: Command::DisableEncryptedVolume, +      padding: [0; 59], +    }; +  } +} + +impl AsRef<[u8]> for DisableEncryptedVolumeCommand { +  fn as_ref(&self) -> &[u8] { +    unsafe { return mem::transmute::<&DisableEncryptedVolumeCommand, &[u8; 60]>(self) }; +  } +} + + +#[cfg(test)] +mod tests { +  use super::*; + +  #[test] +  fn encrypted_volume_report() { +    let password = "test42".to_string().into_bytes(); +    let report = EnableEncryptedVolumeCommand::new(&password); +    let expected = ['t' as u8, 'e' as u8, 's' as u8, 't' as u8, '4' as u8, '2' as u8, 0u8, 0u8, +                    0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8]; +    assert_eq!(report.password, expected); +  } + +  #[test] +  #[cfg(debug)] +  #[should_panic(expected = "assertion failed")] +  fn overly_long_password() { +    let password = "012345678912345678901".to_string().into_bytes(); +    EnableEncryptedVolumeCommand::new(&password); +  } + +  #[test] +  fn report_crc() { +    let password = "passphrase".to_string().into_bytes(); +    let payload = EnableEncryptedVolumeCommand::new(&password); +    let report = Report::from(payload); + +    // The expected checksum was computed using the original +    // functionality. +    assert_eq!(report.crc, 0xeeb583c); +  } +} diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs index eabf598..3fb533a 100644 --- a/nitrocli/src/pinentry.rs +++ b/nitrocli/src/pinentry.rs @@ -17,7 +17,7 @@  // * along with this program.  If not, see <http://www.gnu.org/licenses/>. *  // ************************************************************************* -use error::Error as Error; +use error::Error;  use std::process; @@ -50,14 +50,11 @@ fn parse_pinentry_passphrase(response: Vec<u8>) -> Result<Vec<u8>, Error> {  pub fn inquire_passphrase() -> Result<Vec<u8>, Error> { -  const PINENTRY_DESCR:  &'static str = "+"; -  const PINENTRY_TITLE:  &'static str = "Please+enter+user+PIN"; +  const PINENTRY_DESCR: &'static str = "+"; +  const PINENTRY_TITLE: &'static str = "Please+enter+user+PIN";    const PINENTRY_PASSWD: &'static str = "PIN"; -  let args = vec![CACHE_ID, -                  PINENTRY_DESCR, -                  PINENTRY_PASSWD, -                  PINENTRY_TITLE].join(" "); +  let args = vec![CACHE_ID, PINENTRY_DESCR, PINENTRY_PASSWD, PINENTRY_TITLE].join(" ");    let command = "GET_PASSPHRASE --data ".to_string() + &args;    // We could also use the --data parameter here to have a more direct    // representation of the passphrase but the resulting response was @@ -65,10 +62,9 @@ pub fn inquire_passphrase() -> Result<Vec<u8>, Error> {    // reported for the GET_PASSPHRASE command does not actually cause    // gpg-connect-agent to exit with a non-zero error code, we have to    // evaluate the output to determine success/failure. -  let output = process::Command::new("gpg-connect-agent") -                                .arg(command) -                                .arg("/bye") -                                .output()?; +  let output = process::Command::new("gpg-connect-agent").arg(command) +    .arg("/bye") +    .output()?;    return parse_pinentry_passphrase(output.stdout);  } | 
