aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2019-01-07 22:05:03 +0000
committerRobin Krahl <me@robin-krahl.de>2019-01-11 04:36:49 +0100
commit516ee5886f7ddcd60670aa5bdb2edc98cbec257d (patch)
tree5ecee26eec9744db387a9f6212ef2d309353d470
parentdd55b04186c295319602d95d2486225570ed3591 (diff)
downloadnitrocli-otp-qr-516ee5886f7ddcd60670aa5bdb2edc98cbec257d.tar.gz
nitrocli-otp-qr-516ee5886f7ddcd60670aa5bdb2edc98cbec257d.tar.bz2
Implement otpauth URI parsing
-rw-r--r--Cargo.lock51
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs132
3 files changed, 179 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 277ad6e..008d2ff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -23,11 +23,26 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "idna"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "libc"
version = "0.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "matches"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "mktemp"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -41,9 +56,16 @@ version = "0.1.0"
dependencies = [
"argparse 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mktemp 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
+name = "percent-encoding"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "rand"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -69,6 +91,29 @@ version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
+name = "unicode-bidi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "url"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "uuid"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -101,11 +146,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
"checksum libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "023a4cd09b2ff695f9734c1934145a315594b7986398496841c7031a5a1bbdbd"
+"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
"checksum mktemp 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "77001ceb9eed65439f3dc2a2543f9ba1417d912686bf224a7738d0966e6dcd69"
+"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
"checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
+"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25"
+"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
"checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f"
"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
diff --git a/Cargo.toml b/Cargo.toml
index 8315b8f..86135cc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,3 +12,5 @@ license = "MIT"
[dependencies]
argparse = "0.2"
mktemp = "0.3"
+percent-encoding = "1.0"
+url = "1.7"
diff --git a/src/main.rs b/src/main.rs
index 9c2a8c1..9fa1a72 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,18 +2,20 @@
//! Reads OTP configuration from a QR code and writes it to an OTP slot on a Nitrokey device.
+use std::borrow;
use std::fmt;
use std::fs;
use std::io;
use std::path;
use std::process;
-use std::string;
+use std::str;
#[derive(Debug)]
enum Error {
IoError(io::Error),
Error(String),
- Utf8Error(string::FromUtf8Error),
+ UrlParseError(url::ParseError),
+ Utf8Error(str::Utf8Error),
}
impl fmt::Display for Error {
@@ -21,6 +23,7 @@ impl fmt::Display for Error {
match *self {
Error::IoError(ref err) => write!(f, "IO error: {}", err),
Error::Error(ref string) => write!(f, "Error: {}", string),
+ Error::UrlParseError(ref err) => write!(f, "URL parse error: {}", err),
Error::Utf8Error(ref err) => write!(f, "UTF-8 error: {}", err),
}
}
@@ -49,12 +52,29 @@ impl From<(&str, process::ExitStatus)> for Error {
}
}
-impl From<string::FromUtf8Error> for Error {
- fn from(error: string::FromUtf8Error) -> Error {
+impl From<url::ParseError> for Error {
+ fn from(error: url::ParseError) -> Error {
+ Error::UrlParseError(error)
+ }
+}
+
+impl From<str::Utf8Error> for Error {
+ fn from(error: str::Utf8Error) -> Error {
Error::Utf8Error(error)
}
}
+#[derive(Debug, PartialEq)]
+struct UrlData {
+ otp_type: String,
+ label: String,
+ secret: String,
+ issuer: Option<String>,
+ digits: Option<String>,
+ counter: Option<String>,
+ period: Option<String>,
+}
+
#[derive(Debug)]
struct Options {
slot: u8,
@@ -113,7 +133,7 @@ fn decode_qr_code(path: &path::Path) -> Result<String, Error> {
.arg(path)
.output()?;
if output.status.success() {
- let output = String::from_utf8(output.stdout)?;
+ let output = String::from_utf8(output.stdout).map_err(|err| err.utf8_error())?;
let urls = output
.split("\n")
.filter(|url| url.starts_with("otpauth://"))
@@ -131,12 +151,74 @@ fn decode_qr_code(path: &path::Path) -> Result<String, Error> {
}
}
+fn strip_issuer_prefix(label: String, issuer: &str) -> String {
+ let prefix = format!("{}:", issuer);
+ if label.starts_with(&prefix) {
+ let label = label.trim_left_matches(&prefix);
+ let label = label.trim_left_matches(" ");
+ label.to_string()
+ } else {
+ label
+ }
+}
+
+fn parse_url(url: &str) -> Result<UrlData, Error> {
+ let url = url::Url::parse(url)?;
+ let scheme = url.scheme();
+ if scheme != "otpauth" {
+ return Err(Error::Error(format!("Unexpected URL scheme: {}", scheme)));
+ }
+ let otp_type = match url.host_str() {
+ Some(host) => host.to_string(),
+ None => return Err(Error::from("otpauth URL does not contain type")),
+ };
+ let label = url.path();
+ let label = percent_encoding::percent_decode(label.as_bytes()).decode_utf8()?;
+ let label = label.trim_start_matches("/").to_string();
+ let mut secret: Option<String> = None;
+ let mut issuer: Option<String> = None;
+ let mut digits: Option<String> = None;
+ let mut counter: Option<String> = None;
+ let mut period: Option<String> = None;
+ for (key, value) in url.query_pairs() {
+ let field = match key {
+ borrow::Cow::Borrowed("secret") => Some(&mut secret),
+ borrow::Cow::Borrowed("issuer") => Some(&mut issuer),
+ borrow::Cow::Borrowed("digits") => Some(&mut digits),
+ borrow::Cow::Borrowed("counter") => Some(&mut counter),
+ borrow::Cow::Borrowed("period") => Some(&mut period),
+ _ => None,
+ };
+ if let Some(field) = field {
+ *field = Some(value.into_owned());
+ }
+ }
+ let label = match issuer {
+ Some(ref issuer) => strip_issuer_prefix(label, issuer),
+ None => label,
+ };
+ match secret {
+ Some(secret) => Ok(UrlData {
+ otp_type,
+ label,
+ secret,
+ issuer,
+ digits,
+ counter,
+ period,
+ }),
+ None => Err(Error::from("otpauth URL did not contain a secret")),
+ }
+}
+
fn run(options: Options) -> Result<(), Error> {
let path = match options.file {
Some(ref file) => path::PathBuf::from(file),
None => import_qr_code()?,
};
- println!("{}", decode_qr_code(&path)?);
+ let url = decode_qr_code(&path)?;
+ let url_data = parse_url(&url)?;
+ println!("{:?}", url_data);
if options.file.is_none() {
fs::remove_file(&path)?;
}
@@ -156,3 +238,41 @@ fn main() {
};
process::exit(status);
}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn parse_url() -> Result<(), super::Error> {
+ let result = super::parse_url(
+ "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
+ )?;
+ let expected = super::UrlData {
+ otp_type: "totp".to_string(),
+ label: "alice@google.com".to_string(),
+ secret: "JBSWY3DPEHPK3PXP".to_string(),
+ issuer: Some("Example".to_string()),
+ digits: None,
+ counter: None,
+ period: None,
+ };
+ assert_eq!(result, expected);
+
+ let result = super::parse_url("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30")?;
+ let expected = super::UrlData {
+ otp_type: "totp".to_string(),
+ label: "john.doe@email.com".to_string(),
+ secret: "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ".to_string(),
+ issuer: Some("ACME Co".to_string()),
+ digits: Some("6".to_string()),
+ counter: None,
+ period: Some("30".to_string()),
+ };
+ assert_eq!(result, expected);
+
+ assert!(super::parse_url("otpauth://totp/test?secret=blubb").is_ok());
+ assert!(super::parse_url("otauth://totp/test?secret=blubb").is_err());
+ assert!(super::parse_url("otpauth://totp/test").is_err());
+
+ Ok(())
+ }
+}