From 8b3ffd9c6d7b1951158c5fc4922706ba139f14bc Mon Sep 17 00:00:00 2001 From: Nicholas Sushkin Date: Wed, 17 Aug 2016 13:42:06 -0400 Subject: [PATCH 1/4] Command line interface to YubikeyClient. Initial Revision. --- .../java/com/yubico/client/v2/ykclient.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 v2client/src/main/java/com/yubico/client/v2/ykclient.java diff --git a/v2client/src/main/java/com/yubico/client/v2/ykclient.java b/v2client/src/main/java/com/yubico/client/v2/ykclient.java new file mode 100644 index 0000000..a9940e4 --- /dev/null +++ b/v2client/src/main/java/com/yubico/client/v2/ykclient.java @@ -0,0 +1,185 @@ +/* Copyright (c) 2016, Nicholas Sushkin. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + Written by Nicholas Sushkin , August 2016. +*/ + +package com.yubico.client.v2; + +import com.yubico.client.v2.exceptions.*; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.ParseException; + +public class ykclient { + + static Options options = new Options() + .addOption("h", "help", false, "Display this help screen") + .addOption("V", "version", false, "Display version information") + .addOption("d", "debug", false, "Print debugging information") + .addOption(Option.builder("a").longOpt("apikey").hasArg().desc("API key for HMAC validation of request/response").argName("key").build()) + .addOption(Option.builder("u").longOpt("url").hasArg().desc("Yubikey validation service URL").argName("ykvalurl").build()); + + static void printUsage() { + (new HelpFormatter()).printHelp("java com.yubico.client.v2.ykclient (--help|--version|--apikey ) [--debug] [--url ]* CLIENTID YUBIKEYOTP", + "Validate the YUBIKEYOTP one-time-password against the YubiCloud using CLIENTID as the client identifier\n\n", + options, + "\nExit status is 0 on success, 1 if there is a hard failure, 2 if the OTP was replayed, 3 for other soft OTP-related failures."); + } + + static String getVersion() { + return ykclient.class.getName() + " " + Version.version; + } + + static void log(String description, String value) { + if (value != null) { + System.err.printf("%14s: %s\n", description, value); + } + } + + public static void main (String args[]) throws Exception { + CommandLineParser parser = new DefaultParser(); + + try { + CommandLine cli = parser.parse(options, args, true); + + String[] leftoverArgs = cli.getArgs(); + + if ( cli.hasOption("h") ) { + printUsage(); + return; + } + + if ( cli.hasOption("V") ) { + System.out.println(getVersion()); + return; + }; + + if ( ! cli.hasOption("a") ) { + System.err.println("Specify API key using --apikey key option"); + System.exit(1); + } + + if ( leftoverArgs.length != 2 ) { + printUsage(); + return; + } + + int clientId = -1; + try { + clientId = Integer.parseInt(leftoverArgs[0]); + } catch (NumberFormatException e) { + System.err.println("error: client identity must be a positive integer."); + System.exit(1); + } + + if (clientId <= 0) { + System.err.println("error: client identity must be a positive integer."); + System.exit(1); + } + + YubicoClient yc = YubicoClient.getClient(Integer.parseInt(leftoverArgs[0]), + cli.getOptionValue("a")); + + yc.setUserAgent(getVersion()); + + if ( cli.hasOption("u") ) { + yc.setWsapiUrls( cli.getOptionValues("u") ); + } + + String otp = leftoverArgs[1]; + if (otp.length() < 32) { + System.err.println("error: modhex encoded token must be at least 32 characters"); + System.exit(1); + } + + if ( cli.hasOption("d") ) { + System.err.printf("%s:\n", "Input"); + for ( String url: yc.getWsapiUrls() ) + { + System.err.printf("%14s: %s\n", "validation URL", url); + } + log("client id", Integer.toString(yc.getClientId())); + log("token", otp); + log("api key", yc.getKey()); + } + + try { + VerificationResponse response = yc.verify(otp); + ResponseStatus status = response.getStatus(); + + if ( cli.hasOption("d") ) { + System.err.printf("\n%s: %s\n", "Response", response); + + if (status != null) { + System.err.printf("%14s: (%d) %s\n", "status", status.ordinal(), status); + } + + log("otp", response.getOtp()); + log("nonce", response.getNonce()); + log("t", response.getT()); + log("timestamp", response.getTimestamp()); + log("sessioncounter", response.getSessioncounter()); + log("sessionuse", response.getSessionuse()); + log("sl", response.getSl()); + } + + if (response.isOk()) { + System.exit(0); + } else if (status == ResponseStatus.REPLAYED_OTP) { + System.exit(2); + } else { + System.exit(3); + } + } + catch (YubicoVerificationException e) { + System.err.println("Validation Error: " + e); + System.exit(3); + } + catch (YubicoValidationFailure f) { + System.err.println("Validation Failure: " + f); + System.exit(3); + } + catch (IllegalArgumentException a) { + System.err.println("Incorrectly formatted OTP string: " + a); + System.exit(3); + } + + return; + } + catch (ParseException pe) { + System.err.println("Error parsing command line: " + pe.getMessage()); + System.exit(1); + } + } +} From c123c54a5d34c9b6d98d77ced5a2563d715ea147 Mon Sep 17 00:00:00 2001 From: Nicholas Sushkin Date: Wed, 17 Aug 2016 13:49:55 -0400 Subject: [PATCH 2/4] Added dependency on commons-cli required by to parse command line in ykclient.java --- v2client/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/v2client/pom.xml b/v2client/pom.xml index 1434242..2ea18ba 100644 --- a/v2client/pom.xml +++ b/v2client/pom.xml @@ -46,6 +46,11 @@ commons-codec 1.4 + + commons-cli + commons-cli + 1.3.1 + From 7ed9bd2519d9a2e0024ac942a1ab8253cb3332d0 Mon Sep 17 00:00:00 2001 From: Nicholas Sushkin Date: Mon, 22 Aug 2016 17:38:14 -0400 Subject: [PATCH 3/4] Per Klas Lindfors, instead of creating a new class, add new options to the existing class Cmd. The new version of Cmd supports both ykclient style options (--key apikey and two unnamed options CLIENTID and OTP) and the original options (three unnamed arguments CLIENTID key OTP). Both styles support --debug and multiple --url options. --- .../main/java/com/yubico/client/v2/Cmd.java | 179 ++++++++++++++--- .../java/com/yubico/client/v2/ykclient.java | 185 ------------------ 2 files changed, 154 insertions(+), 210 deletions(-) delete mode 100644 v2client/src/main/java/com/yubico/client/v2/ykclient.java diff --git a/v2client/src/main/java/com/yubico/client/v2/Cmd.java b/v2client/src/main/java/com/yubico/client/v2/Cmd.java index fc27ba8..bf8d9ea 100644 --- a/v2client/src/main/java/com/yubico/client/v2/Cmd.java +++ b/v2client/src/main/java/com/yubico/client/v2/Cmd.java @@ -1,16 +1,16 @@ -/* Copyright (c) 2011, Linus Widströmer. All rights reserved. +/* Copyright (c) 2016, Nicholas Sushkin. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. + notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. + notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, @@ -26,37 +26,166 @@ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - Written by Linus Widströmer , January 2011. + Written by Nicholas Sushkin , August 2016. */ package com.yubico.client.v2; +import com.yubico.client.v2.exceptions.*; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.ParseException; + public class Cmd { - public static void main (String args []) throws Exception - { - if (args.length != 3) { - System.err.println("\n*** Test your Yubikey against Yubico OTP validation server ***"); - System.err.println("\nUsage: java -jar client.jar Client_ID Client_key OTP"); - System.err.println("\nEg. java -jar client.jar 28 vvfucnlcrrnejlbuthlktguhclhvegbungldcrefbnku"); - System.err.println("\nTouch Yubikey to generate the OTP. Visit Yubico.com for more details."); - return; - } + static Options options = new Options() + .addOption("h", "help", false, "Display this help screen") + .addOption("V", "version", false, "Display version information") + .addOption("d", "debug", false, "Print debugging information") + .addOption(Option.builder("a").longOpt("apikey").hasArg().desc("API key for HMAC validation of request/response").argName("key").build()) + .addOption(Option.builder("u").longOpt("url").hasArg().desc("Yubikey validation service URL").argName("ykvalurl").build()); - String otp = args[2]; + static void printUsage() { + (new HelpFormatter()).printHelp("java -jar client.jar (--help|--version|--apikey ) [--debug] [--url ]* CLIENTID YUBIKEYOTP\n" + + "or: java -jar client.jar [--debug] [--url ]* CLIENTID key YUBIKEYOTP", + "Validate the YUBIKEYOTP one-time-password against the YubiCloud using CLIENTID as the client identifier\n\n", + options, + "\nExit status is 0 on success, 1 if there is a hard failure, 2 if the OTP was replayed, 3 for other soft OTP-related failures."); + } - YubicoClient yc = YubicoClient.getClient(Integer.parseInt(args[0]), args[1]); - VerificationResponse response = yc.verify(otp); + static String getVersion() { + return Cmd.class.getName() + " " + Version.version; + } - if(response!=null && response.getStatus() == ResponseStatus.OK) { - System.out.println("\n* OTP verified OK"); - } else { - System.out.println("\n* Failed to verify OTP"); + static void log(String description, String value) { + if (value != null) { + System.err.printf("%14s: %s\n", description, value); } + } + + public static void main (String args[]) throws Exception { + CommandLineParser parser = new DefaultParser(); + + try { + CommandLine cli = parser.parse(options, args, true); + + String[] leftoverArgs = cli.getArgs(); + + if ( cli.hasOption("h") ) { + printUsage(); + return; + } + + if ( cli.hasOption("V") ) { + System.out.println(getVersion()); + return; + } + + if ( ( leftoverArgs.length < 2 ) + || ( leftoverArgs.length > 3 ) + ) { + printUsage(); + return; + } + + boolean legacyUsage = (leftoverArgs.length == 3); + + if ( ! legacyUsage && ! cli.hasOption("a") ) { + System.err.println("Specify API key using --apikey key option"); + System.exit(1); + } - System.out.println("\n* Last response: " + response); - System.exit(0); + int clientId = -1; + try { + clientId = Integer.parseInt(leftoverArgs[0]); + } catch (NumberFormatException e) { + System.err.println("error: client identity must be a positive integer."); + System.exit(1); + } - } // End of main + if (clientId <= 0) { + System.err.println("error: client identity must be a positive integer."); + System.exit(1); + } + String apiKey = legacyUsage ? leftoverArgs[1] : cli.getOptionValue("a"); + String otp = legacyUsage ? leftoverArgs[2] : leftoverArgs[1]; + + YubicoClient yc = YubicoClient.getClient(clientId, apiKey); + + yc.setUserAgent(getVersion()); + + if ( cli.hasOption("u") ) { + yc.setWsapiUrls( cli.getOptionValues("u") ); + } + + if (otp.length() < 32) { + System.err.println("error: modhex encoded token must be at least 32 characters"); + System.exit(1); + } + + if ( cli.hasOption("d") ) { + System.err.printf("%s:\n", "Input"); + for ( String url: yc.getWsapiUrls() ) + { + System.err.printf("%14s: %s\n", "validation URL", url); + } + log("client id", Integer.toString(yc.getClientId())); + log("token", otp); + log("api key", yc.getKey()); + } + + try { + VerificationResponse response = yc.verify(otp); + ResponseStatus status = response.getStatus(); + + if ( cli.hasOption("d") ) { + System.err.printf("\n%s: %s\n", "Response", response); + + if (status != null) { + System.err.printf("%14s: (%d) %s\n", "status", status.ordinal(), status); + } + + log("otp", response.getOtp()); + log("nonce", response.getNonce()); + log("t", response.getT()); + log("timestamp", response.getTimestamp()); + log("sessioncounter", response.getSessioncounter()); + log("sessionuse", response.getSessionuse()); + log("sl", response.getSl()); + } + + if (response.isOk()) { + System.exit(0); + } else if (status == ResponseStatus.REPLAYED_OTP) { + System.exit(2); + } else { + System.exit(3); + } + } + catch (YubicoVerificationException e) { + System.err.println("Validation Error: " + e); + System.exit(3); + } + catch (YubicoValidationFailure f) { + System.err.println("Validation Failure: " + f); + System.exit(3); + } + catch (IllegalArgumentException a) { + System.err.println("Incorrectly formatted OTP string: " + a); + System.exit(3); + } + + return; + } + catch (ParseException pe) { + System.err.println("Error parsing command line: " + pe.getMessage()); + System.exit(1); + } + } } diff --git a/v2client/src/main/java/com/yubico/client/v2/ykclient.java b/v2client/src/main/java/com/yubico/client/v2/ykclient.java deleted file mode 100644 index a9940e4..0000000 --- a/v2client/src/main/java/com/yubico/client/v2/ykclient.java +++ /dev/null @@ -1,185 +0,0 @@ -/* Copyright (c) 2016, Nicholas Sushkin. All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS - BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - SUCH DAMAGE. - - Written by Nicholas Sushkin , August 2016. -*/ - -package com.yubico.client.v2; - -import com.yubico.client.v2.exceptions.*; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.ParseException; - -public class ykclient { - - static Options options = new Options() - .addOption("h", "help", false, "Display this help screen") - .addOption("V", "version", false, "Display version information") - .addOption("d", "debug", false, "Print debugging information") - .addOption(Option.builder("a").longOpt("apikey").hasArg().desc("API key for HMAC validation of request/response").argName("key").build()) - .addOption(Option.builder("u").longOpt("url").hasArg().desc("Yubikey validation service URL").argName("ykvalurl").build()); - - static void printUsage() { - (new HelpFormatter()).printHelp("java com.yubico.client.v2.ykclient (--help|--version|--apikey ) [--debug] [--url ]* CLIENTID YUBIKEYOTP", - "Validate the YUBIKEYOTP one-time-password against the YubiCloud using CLIENTID as the client identifier\n\n", - options, - "\nExit status is 0 on success, 1 if there is a hard failure, 2 if the OTP was replayed, 3 for other soft OTP-related failures."); - } - - static String getVersion() { - return ykclient.class.getName() + " " + Version.version; - } - - static void log(String description, String value) { - if (value != null) { - System.err.printf("%14s: %s\n", description, value); - } - } - - public static void main (String args[]) throws Exception { - CommandLineParser parser = new DefaultParser(); - - try { - CommandLine cli = parser.parse(options, args, true); - - String[] leftoverArgs = cli.getArgs(); - - if ( cli.hasOption("h") ) { - printUsage(); - return; - } - - if ( cli.hasOption("V") ) { - System.out.println(getVersion()); - return; - }; - - if ( ! cli.hasOption("a") ) { - System.err.println("Specify API key using --apikey key option"); - System.exit(1); - } - - if ( leftoverArgs.length != 2 ) { - printUsage(); - return; - } - - int clientId = -1; - try { - clientId = Integer.parseInt(leftoverArgs[0]); - } catch (NumberFormatException e) { - System.err.println("error: client identity must be a positive integer."); - System.exit(1); - } - - if (clientId <= 0) { - System.err.println("error: client identity must be a positive integer."); - System.exit(1); - } - - YubicoClient yc = YubicoClient.getClient(Integer.parseInt(leftoverArgs[0]), - cli.getOptionValue("a")); - - yc.setUserAgent(getVersion()); - - if ( cli.hasOption("u") ) { - yc.setWsapiUrls( cli.getOptionValues("u") ); - } - - String otp = leftoverArgs[1]; - if (otp.length() < 32) { - System.err.println("error: modhex encoded token must be at least 32 characters"); - System.exit(1); - } - - if ( cli.hasOption("d") ) { - System.err.printf("%s:\n", "Input"); - for ( String url: yc.getWsapiUrls() ) - { - System.err.printf("%14s: %s\n", "validation URL", url); - } - log("client id", Integer.toString(yc.getClientId())); - log("token", otp); - log("api key", yc.getKey()); - } - - try { - VerificationResponse response = yc.verify(otp); - ResponseStatus status = response.getStatus(); - - if ( cli.hasOption("d") ) { - System.err.printf("\n%s: %s\n", "Response", response); - - if (status != null) { - System.err.printf("%14s: (%d) %s\n", "status", status.ordinal(), status); - } - - log("otp", response.getOtp()); - log("nonce", response.getNonce()); - log("t", response.getT()); - log("timestamp", response.getTimestamp()); - log("sessioncounter", response.getSessioncounter()); - log("sessionuse", response.getSessionuse()); - log("sl", response.getSl()); - } - - if (response.isOk()) { - System.exit(0); - } else if (status == ResponseStatus.REPLAYED_OTP) { - System.exit(2); - } else { - System.exit(3); - } - } - catch (YubicoVerificationException e) { - System.err.println("Validation Error: " + e); - System.exit(3); - } - catch (YubicoValidationFailure f) { - System.err.println("Validation Failure: " + f); - System.exit(3); - } - catch (IllegalArgumentException a) { - System.err.println("Incorrectly formatted OTP string: " + a); - System.exit(3); - } - - return; - } - catch (ParseException pe) { - System.err.println("Error parsing command line: " + pe.getMessage()); - System.exit(1); - } - } -} From 43524923ebb67e7118338c620bf2f0d54cd31f23 Mon Sep 17 00:00:00 2001 From: Nicholas Sushkin Date: Mon, 22 Aug 2016 17:57:42 -0400 Subject: [PATCH 4/4] When invoked the original way, print the original response messages. --- v2client/src/main/java/com/yubico/client/v2/Cmd.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/v2client/src/main/java/com/yubico/client/v2/Cmd.java b/v2client/src/main/java/com/yubico/client/v2/Cmd.java index bf8d9ea..732fd1e 100644 --- a/v2client/src/main/java/com/yubico/client/v2/Cmd.java +++ b/v2client/src/main/java/com/yubico/client/v2/Cmd.java @@ -160,6 +160,14 @@ public static void main (String args[]) throws Exception { log("sl", response.getSl()); } + if (legacyUsage) { + if (response.isOk()) { + System.out.println("\n* OTP verified OK"); + } else { + System.out.println("\n* Failed to verify OTP"); + } + } + if (response.isOk()) { System.exit(0); } else if (status == ResponseStatus.REPLAYED_OTP) {