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); } |