aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--nitrocli/README.md61
-rw-r--r--nitrocli/src/crc32.rs88
-rw-r--r--nitrocli/src/main.rs13
-rw-r--r--nitrocli/src/nitrokey.rs149
-rw-r--r--nitrocli/src/pinentry.rs18
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);
}