commit 2e6a3df756fbc08e462cd6db64f2979795cc1f83
parent 6bb655df0bb1bb3e4f19a27de88a79bdac1ab5b7
Author: boyska <piuttosto@logorroici.org>
Date: Sat, 4 Aug 2012 18:34:10 +0200
Add KDF support #82
Include pbkdf2 tools inside tomb
It also supports parameters (itertime).
Diffstat:
14 files changed, 504 insertions(+), 38 deletions(-)
diff --git a/KEY_SPECIFICATIONS.txt b/KEY_SPECIFICATIONS.txt
@@ -0,0 +1,36 @@
+Overview
+=========
+
+
+What's a key?
+It basicly is a gpg simmetrically encrypted, ascii-armored file.
+It's encryption key is a function (see below, on KDF section) of your tomb
+passphrase.
+
+
+Layout
+======
+
+Before coming to the gpg part, there could be some "header" lines specifying
+metatada. They're done like this:
+_FIELD_params_params_and_more_params_
+
+where FIELD should be the description for the header.
+Pay much attention to the fact that there should ONLY be ASCII characters there,
+to avoid encoding issues and whatever. Needs something more? Use base64encode.
+(Of course, you're free to pack params into a single field, base64encoding
+whatever you want).
+And every header field should be in only one line.
+
+KDF
+===
+
+Key Derivation Functions, are functions which will make your key stronger
+spending some CPU time: the basic idea is that you have to compute that function
+just once in a while, but an attacker that wants to bruteforce has to compute it
+for every passphrase he's checking. This will make the bruteforce much more
+expensive.
+
+The header line format is _KDF_$method_$params_$params_... where $method is the
+method we are using (ie: scrypt) and params is something that it needs (ie:
+salt).
diff --git a/Makefile.am b/Makefile.am
@@ -1 +1 @@
-SUBDIRS = src share doc
+SUBDIRS = src src/kdf share doc
diff --git a/configure.ac b/configure.ac
@@ -89,6 +89,7 @@ dnl ---------------------------------------------------------------
PKG_CHECK_MODULES(GTK2, [gtk+-2.0 >= 2.16], :,
AC_MSG_ERROR([*** Gtk+2 >=2.16 development files not found!]))
+AM_PATH_LIBGCRYPT([1.5.0], :, AC_MSG_ERROR([gcrypt development files not found]))
AC_SUBST([GTK2_CFLAGS])
AC_SUBST([GTK2_LIBS])
@@ -130,6 +131,7 @@ dnl alphabetic order on dir/subdir, but Makefile sorts before everything
AC_CONFIG_FILES([
Makefile
src/Makefile
+src/kdf/Makefile
doc/Makefile
share/Makefile
])
diff --git a/doc/tomb.1 b/doc/tomb.1
@@ -111,7 +111,7 @@ the size of the new \fIfile\fR to be created, in megabytes.
.IP "-k \fI<keyfile>\fR"
When opening a tomb, this option can be used to specify the location
of the key to use. Keys are created with the same name of the tomb
-file adding a '.gpg' suffix, but can be later renamed and transported
+file adding a '.key' suffix, but can be later renamed and transported
on other media. When a key is not found, the program asks to insert a
USB storage device and it will look for the key file inside it.
If \fI<keyfile>\fR is "-" (dash), it will read stdin
@@ -124,6 +124,31 @@ tomb create -s 100 tombname -k /media/usb/tombname
to put the key on a usb pendrive
.B
+.IP "--kdf \fI<method>\fR"
+This will specify the KDF method to use for the tomb we're creating.
+Please note that no stable release of tomb supports KDF; if you use it,
+your tomb might be unusable with an older version of tomb.
+
+You can specify parameters with --kdf=method:param. That is, for example,
+\fI--kdf=pbkdf2:2.5\fR will use pbkdf2 with an itertime of 2.5 seconds
+
+Supported methods are: pbkdf2, null
+
+.B pbkdf2
+is probably the most used kdf in security applications, so it's a good choice.
+It accepts one parameter, that is the seconds it will take on this computer to
+derive the key. The default is 1.
+
+.B null
+is just the same as not using --kdf at all: it will stick to the "classic"
+behaviour
+
+.B
+.IP "--kdf \fI<method>\fR"
+This will specify the KDF method to use for the tomb we're creating.
+Please note that no stable release of tomb supports KDF; if you use it,
+your tomb might be unusable with an older version of tomb.
+.B
.IP "-n"
Skip processing of post-hooks and bind-hooks if found inside the tomb.
See the \fIHOOKS\fR section in this manual for more information.
diff --git a/src/kdf/.gitignore b/src/kdf/.gitignore
@@ -0,0 +1,4 @@
+tomb-kdf-pbkdf2
+tomb-kdf-pbkdf2-gensalt
+tomb-kdf-pbkdf2-getiter
+tomb-utils-hexencode
diff --git a/src/kdf/Makefile.am b/src/kdf/Makefile.am
@@ -0,0 +1,13 @@
+bin_PROGRAMS = tomb-kdf-pbkdf2 tomb-kdf-pbkdf2-gensalt tomb-kdf-pbkdf2-getiter hexencode
+tomb_kdf_pbkdf2_SOURCES = pbkdf2/pbkdf2.c
+tomb_kdf_pbkdf2_CFLAGS = $(LIBGCRYPT_CFLAGS)
+tomb_kdf_pbkdf2_LDADD = $(LIBGCRYPT_LIBS)
+
+tomb_kdf_pbkdf2_gensalt_SOURCES = pbkdf2/gen_salt.c
+
+tomb_kdf_pbkdf2_getiter_SOURCES = pbkdf2/benchmark.c
+tomb_kdf_pbkdf2_getiter_CFLAGS = $(LIBGCRYPT_CFLAGS)
+tomb_kdf_pbkdf2_getiter_LDADD = $(LIBGCRYPT_LIBS)
+
+hexencode_SOURCES = hexencode.c
+
diff --git a/src/kdf/README b/src/kdf/README
@@ -0,0 +1,20 @@
+PLANS
+------
+
+While this can be useful for general purpose, it specially fits tomb, and it's designed for easy integration and compilation.
+
+Binary name will then be:
+tomb-kdf-${algo}
+tomb-kdf-${algo}-gensalt
+tomb-kdf-${algo}-getiter
+
+hexencode (or similar utils, should they be developed), go with:
+tomb-utils-hexencode
+
+Base64 vs hexencode
+-------------------
+
+While base64 is easier to use (shell command, more compact), pbkdf2 use hex
+in its specifications.
+This could be solved with an option (-x for hex, defaults to base64)
+
diff --git a/src/kdf/hexencode.c b/src/kdf/hexencode.c
@@ -0,0 +1,49 @@
+/*
+ * A simple utility that reads from stdin and output the hexencoding (on a single line) of the input
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+
+static int decode_mode = 0;
+int main(int argc, char *argv[]) {
+ char c;
+ char buf[3];
+ int read_bytes;
+ int opt;
+ static struct option long_options[] =
+ {
+ {"decode", no_argument, &decode_mode, 1},
+ {"encode", no_argument, &decode_mode, 0},
+ {0,0,0,0}
+ };
+ int option_index = 0;
+
+ while(1) {
+ option_index = 0;
+ opt = getopt_long(argc, argv, "", long_options, &option_index);
+ if(opt == -1)
+ break;
+ switch(opt) {
+ case 0:
+ break;
+ case '?':
+ return 127;
+ default:
+ abort();
+ }
+ }
+ if(decode_mode == 0) {
+ while(( c = (char)getchar() ) != EOF)
+ printf("%02x", c);
+ return 0;
+ } else {
+ while( (read_bytes=fread(buf, sizeof(char), 2, stdin)) != 0) {
+ if(read_bytes == 1) buf[1]='\0';
+ sscanf(buf, "%x", &c);
+ printf("%c", c);
+ }
+ return 0;
+ }
+}
diff --git a/src/kdf/pbkdf2/benchmark.c b/src/kdf/pbkdf2/benchmark.c
@@ -0,0 +1,59 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/time.h>
+
+#include <gcrypt.h>
+
+static long bench(int ic) {
+ char *pass = "mypass";
+ unsigned char *salt = "abcdefghijklmno";
+ int salt_len = strlen(salt);
+ int result_len = 64;
+ unsigned char *result = calloc(result_len, sizeof(char));
+ struct timeval start, end;
+ long microtime;
+
+ gettimeofday(&start, NULL);
+ gcry_kdf_derive( pass, strlen(pass), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, salt, salt_len, ic, result_len, result);
+ gettimeofday(&end, NULL);
+ microtime = 1000000*end.tv_sec+end.tv_usec - (1000000*start.tv_sec+start.tv_usec);
+
+ return (long)microtime;
+}
+int main(int argc, char *argv[])
+{
+ long desired_time = 1000000;
+ long microtime;
+ int ic=100;
+ int tries=0;
+ if(argc >= 2)
+ sscanf(argv[1], "%ld", &desired_time);
+ if (!gcry_check_version ("1.5.0")) {
+ fputs ("libgcrypt version mismatch\n", stderr);
+ exit (2);
+ }
+ /* Allocate a pool of 16k secure memory. This make the secure memory
+ available and also drops privileges where needed. */
+ gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0);
+ /* It is now okay to let Libgcrypt complain when there was/is
+ a problem with the secure memory. */
+ gcry_control (GCRYCTL_RESUME_SECMEM_WARN);
+ /* Tell Libgcrypt that initialization has completed. */
+ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
+
+
+ microtime = bench(ic);
+ while( abs(desired_time-microtime) > (desired_time/10) /*little difference */
+ && tries++ <= 5) {
+ float ratio = (float)desired_time/microtime;
+ if(ratio > 1000) ratio=1000.0;
+ ic*=ratio;
+ if(ic<1) ic=1;
+ microtime = bench(ic);
+ }
+ printf("%d\n", ic);
+ return 0;
+
+}
diff --git a/src/kdf/pbkdf2/gen_salt.c b/src/kdf/pbkdf2/gen_salt.c
@@ -0,0 +1,39 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void print_hex(unsigned char *buf, int len)
+{
+ int i;
+
+ for(i=0;i<len;i++){
+ printf("%02x", buf[i]);
+ }
+}
+
+int main(int argc, char **argv) {
+ int len=32;
+ int res;
+ unsigned char *buf;
+ FILE *rand;
+ if(argc>=2) {
+ if(sscanf(argv[1], "%d", &len) != 1) {
+ fprintf(stderr, "Error: len must be an integer\n");
+ return 1;
+ }
+ }
+ buf = calloc(len, sizeof(char));
+ memset(buf, 9, len);
+ rand = fopen("/dev/random", "r");
+ res = fread(buf, sizeof(char), len, rand);
+ if( res != len) {
+ fprintf(stderr, "Error reading /dev/random: %d != %d, \n", res, len);
+ fclose(rand);
+ free(buf);
+ return 2;
+ }
+ fclose(rand);
+ print_hex(buf, len);
+ free(buf);
+ return 0;
+}
diff --git a/src/kdf/pbkdf2/pbkdf2.c b/src/kdf/pbkdf2/pbkdf2.c
@@ -0,0 +1,144 @@
+/*
+** SYNOPSIS
+** echo "passphrase" | pbkdf2 salt_hex count > 48_byte_hex_key_and_iv
+**
+** DESCRIPTION
+**
+** Make the "Password-Based Key Derivation Function v2" function found in
+** the openssl library available to the command line, as it is not available
+** for use from the "openssl" command. At the time of writing the "openssl"
+** command only encrypts using the older, 'fast' pbkdf1.5 method.
+**
+** The 'salt_hex' is the salt to be used, as a hexadecimal string. Typically
+** this is 8 bytes (64 bit), and is an assigned randomly during encryption.
+**
+** The 'count' is iteration count used to make the calculation of the key
+** from the passphrase longer so as to take 1/2 to 2 seconds to generate.
+** This complexity prevents slows down brute force attacks enormously.
+**
+** The output of the above is a 48 bytes in hexadeximal, which is typically
+** used for 32 byte encryption key KEY and a 16 byte IV as needed by
+** Crypt-AES-256 (or some other encryption method).
+**
+** NOTE: While the "openssl" command can accept a hex encoded 'key' and 'iv'
+** it only does so on the command line, which is insecure. As such I
+** recommend that the output only be used with API access to the "OpenSSL"
+** cryptography libraries.
+**
+*************
+**
+** Anthony Thyssen 4 November 2009 A.Thyssen@griffith.edu.au
+**
+** Based on a test program "pkcs5.c" found on
+** http://www.mail-archive.com/openssl-users@openssl.org
+** which uses openssl to perform PBKDF2 (RFC2898) iteritive (slow) password
+** hashing.
+**
+** Build
+** gcc -o pbkdf2 pbkdf2.c -lcrypto
+**
+*/
+#include <stdio.h>
+#include <string.h>
+
+#include <gcrypt.h>
+
+/* TODO: move print_hex and hex_to_binary to utils.h, with separate compiling */
+void print_hex(unsigned char *buf, int len)
+{
+ int i;
+
+ for(i=0;i<len;i++)
+ printf("%02x", buf[i]);
+ printf("\n");
+}
+
+int hex_to_binary(unsigned char *buf, char *hex)
+{
+ int ret;
+ int count=0;
+ while(*hex) {
+ if( hex[1] ) {
+ ret = sscanf( hex, "%2x", (unsigned int*) buf++ );
+ hex += 2;
+ }
+ else {
+ ret = sscanf( hex++, "%1x", (unsigned int*)buf++ );
+ }
+ count++;
+ if( ret != 1)
+ return -1;
+ }
+ *buf = 0; // null terminate -- precaution
+ return count;
+}
+
+int main(int argc, char *argv[])
+{
+ char *pass = NULL;
+ unsigned char *salt;
+ int salt_len; // salt length in bytes
+ int ic=0; // iterative count
+ int result_len;
+ unsigned char *result; // result (binary - 32+16 chars)
+ int i;
+
+ if ( argc != 4 ) {
+ fprintf(stderr, "usage: %s salt count len <passwd >binary_key_iv\n", argv[0]);
+ exit(10);
+ }
+
+ //TODO: move to base64decode
+ salt=calloc(strlen(argv[1])/2+3, sizeof(char));
+ salt_len=hex_to_binary(salt, argv[1]);
+ if( salt_len <= 0 ) {
+ fprintf(stderr, "Error: %s is not a valid salt (it must be a hexadecimal string)\n", argv[1]);
+ exit(1);
+ }
+
+ if( sscanf(argv[2], "%d", &ic) == 0 || ic<=0) {
+ fprintf(stderr, "Error: count must be a positive integer\n");
+ exit(1);
+ }
+ if( sscanf(argv[3], "%d", &result_len) == 0 || result_len<=0) {
+ fprintf(stderr, "Error: result_len must be a positive integer\n");
+ exit(1);
+ }
+
+ fscanf(stdin, "%ms", &pass);
+ if ( pass[strlen(pass)-1] == '\n' )
+ pass[strlen(pass)-1] = '\0';
+
+ // PBKDF 2
+ result = calloc(result_len, sizeof(unsigned char*));
+ if (!gcry_check_version ("1.5.0")) {
+ fputs ("libgcrypt version mismatch\n", stderr);
+ exit (2);
+ }
+ /* Allocate a pool of 16k secure memory. This make the secure memory
+ available and also drops privileges where needed. */
+ gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0);
+ /* It is now okay to let Libgcrypt complain when there was/is
+ a problem with the secure memory. */
+ gcry_control (GCRYCTL_RESUME_SECMEM_WARN);
+ /* Tell Libgcrypt that initialization has completed. */
+ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
+
+ gcry_kdf_derive( pass, strlen(pass), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, salt, salt_len, ic, result_len, result);
+ print_hex(result, result_len); // Key + IV (as hex string)
+
+ //clear and free everything
+ for(i=0; i<result_len;i++)
+ result[i]=0;
+ free(result);
+ for(i=0; i<strlen(pass); i++) //blank
+ pass[i]=0;
+ free(pass);
+ for(i=0; i<strlen(argv[1])/2+3; i++) //blank
+ salt[i]=0;
+ free(salt);
+
+ return(0);
+}
+
+/* vim: set noexpandtab ts=4 sw=4: */
diff --git a/src/kdf/test.sh b/src/kdf/test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env zsh
+
+error=0
+while read line; do
+ pass=`cut -f1 <<<$line`
+ salt=`cut -f2 <<<$line`
+ iter=`cut -f3 <<<$line`
+ keylen=`cut -f4 <<<$line`
+ expected=`cut -f5 <<<$line`
+ hexsalt=`cut -f6 <<<$line`
+ #TODO: check!
+ derived=`./pbkdf2 $hexsalt $iter $keylen <<<$pass`
+ if [[ $derived != $expected ]]; then
+ echo ./pbkdf2 $hexsalt $iter $keylen "<<<$pass"
+ echo "Expected $expected, got $derived" >&2
+ error=$((error + 1))
+ fi
+done < test.txt
+
+if [[ $error == 1 ]]; then
+ exit $error
+fi
diff --git a/src/kdf/test.txt b/src/kdf/test.txt
Binary files differ.
diff --git a/src/tomb b/src/tomb
@@ -166,6 +166,17 @@ check_bin() {
# resize suite check bin!
which e2fsck > /dev/null || die "Cannot find e2fsck. Please install it." 1
which resize2fs > /dev/null || die "Cannot find resize2fs. Please install it." 1
+
+ if which tomb-kdf-pbkdf2 &> /dev/null; then
+ KDF_PBKDF2="tomb-kdf-pbkdf2"
+ else
+ local our_pbkdf2
+ our_pbkdf2="$(dirname $(readlink -f $TOMBEXEC))/kdf/tomb-kdf-pbkdf2"
+ if which $our_pbkdf2 &> /dev/null; then
+ KDF_PBKDF2=$our_pbkdf2
+ fi
+ fi
+
}
# }}}
@@ -732,15 +743,15 @@ create_tomb() {
# here user is prompted for key password
for c in 1 2 3; do
- # 3 tries to write two times a matching password
- tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname}"`
- tombpasstmp=$tombpass
- tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname} (again)"`
- if [ "$tombpasstmp" = "$tombpass" ]; then
- break;
- fi
- unset tombpasstmp
- unset tombpass
+ # 3 tries to write two times a matching password
+ tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname}"`
+ tombpasstmp=$tombpass
+ tombpass=`exec_as_user ${TOMBEXEC} askpass "Secure key for ${tombname} (again)"`
+ if [ "$tombpasstmp" = "$tombpass" ]; then
+ break;
+ fi
+ unset tombpasstmp
+ unset tombpass
done
if [ -z $tombpass ]; then
@@ -751,9 +762,36 @@ create_tomb() {
fi
- gpg \
- --openpgp --batch --no-options --no-tty --passphrase-fd 0 2>/dev/null \
- -o "${tombkey}" -c -a ${keytmp}/tomb.tmp <<< ${tombpass}
+ _verbose "KDF method chosen is: '`option_value --kdf`'"
+ kdf_method=$(cut -d: -f1 <<<`option_value --kdf` )
+ case $kdf_method in
+ pbkdf2)
+#one parameter: iter time in seconds
+ seconds=$(cut -d: -f2 -s <<<`option_value --kdf`)
+ if [[ -z $seconds ]]; then
+ seconds=1
+ fi
+ local -i microseconds
+ microseconds=$((seconds*1000000))
+ _verbose "Microseconds: $microseconds"
+ pbkdf2_salt=`${KDF_PBKDF2}-gensalt`
+ pbkdf2_iter=`${KDF_PBKDF2}-getiter $microseconds`
+ tombpass=`${KDF_PBKDF2} $pbkdf2_salt $pbkdf2_iter 64 <<<${tombpass}` #64bytes=512bits is the key length (huge!)
+ header="_KDF_pbkdf2sha1_${pbkdf2_salt}_${pbkdf2_iter}_64\n"
+ ;;
+ ""|null)
+
+ header=""
+ ;;
+ *)
+ _warning "KDF method non recognized"
+ return 1
+ header=""
+ ;;
+ esac
+ ( echo -n $header; gpg \
+ --openpgp --batch --no-options --no-tty --passphrase-fd 0 2>/dev/null \
+ -o - -c -a ${keytmp}/tomb.tmp <<< ${tombpass} ) > $tombkey
unset tombpass
chown ${_uid}:${_gid} ${tombkey}
@@ -924,31 +962,46 @@ mount_tomb() {
_warning "Password is required for key ${keyname}"
for c in 1 2 3; do
- if [ $c = 1 ]; then
- tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb ${keyname}"`
- else
- tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb $keyname (retry $c)"`
- fi
- (gpg --batch --passphrase-fd 0 --no-tty --no-options \
- -d "${tombkey}" 2> /dev/null <<< ${tombpass} ) \
- | cryptsetup --key-file - luksOpen ${nstloop} ${mapper}
- unset tombpass
+ if [ $c = 1 ]; then
+ tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb ${keyname}"`
+ else
+ tombpass=`exec_as_user ${TOMBEXEC} askpass "Open tomb $keyname (retry $c)"`
+ fi
+#TODO: read the first line: if it looks like a KDF, do KDF
+ firstline=`head -n1 < $tombkey`
+ if [[ $firstline =~ '^_KDF_' ]]; then
+ _verbose "KDF: `cut -d_ -f 3 <<<$firstline`"
+ case `cut -d_ -f 3 <<<$firstline` in
+ pbkdf2sha1)
+ pbkdf2_param=`cut -d_ -f 4- <<<$firstline | tr '_' ' '`
+ tombpass=$(${KDF_PBKDF2} ${=pbkdf2_param} 2> /dev/null <<<$tombpass)
+ ;;
+ *)
+ _failure "No suitable program for KDF `cut -f 3 <<<$firstline`"
+ return 1
+ ;;
+ esac
+ fi
+ (gpg --batch --passphrase-fd 0 --no-tty --no-options \
+ -d "${tombkey}" 2> /dev/null <<< ${tombpass} ) \
+ | cryptsetup --key-file - luksOpen ${nstloop} ${mapper}
+ unset tombpass
- # if key was from stdin delete temp file and dir
- if [ $tombkeydir ]; then
- ${=WIPE} ${tombkey}
- rmdir $tombkeydir
- fi
+ # if key was from stdin delete temp file and dir
+ if [ $tombkeydir ]; then
+ ${=WIPE} ${tombkey}
+ rmdir $tombkeydir
+ fi
- # if key was from stdin delete temp file and dir
- if [ $tombkeydir ]; then
- ${WIPE[@]} ${tombkey}
- rmdir $tombkeydir
- fi
+ # if key was from stdin delete temp file and dir
+ if [ $tombkeydir ]; then
+ ${WIPE[@]} ${tombkey}
+ rmdir $tombkeydir
+ fi
- if [ -r /dev/mapper/${mapper} ]; then
- break; # password was correct
- fi
+ if [ -r /dev/mapper/${mapper} ]; then
+ break; # password was correct
+ fi
done
if ! [ -r /dev/mapper/${mapper} ]; then
@@ -1679,7 +1732,7 @@ main() {
subcommands_opts[__default]=""
subcommands_opts[open]="f n -nohook=n k: -key=k U: -uid=U G: -gid=G o: -mount-options=o -ignore-swap"
subcommands_opts[mount]=${subcommands_opts[open]}
- subcommands_opts[create]="f s: -size=s -force k: -key=k U: -uid=U G: -gid=G -ignore-swap"
+ subcommands_opts[create]="f s: -size=s -force k: -key=k U: -uid=U G: -gid=G -ignore-swap -kdf:"
subcommands_opts[passwd]="f -ignore-swap"
subcommands_opts[close]=""
subcommands_opts[help]=""
@@ -1756,7 +1809,7 @@ main() {
continue #it shouldnt be appended to PARAM
elif [[ $arg[1] == '-' ]]; then
if [[ $ok == 0 ]]; then
- die "unrecognized option $arg" 127
+ die "unrecognized option $arg for subcommand $subcommand" 127
fi
fi
PARAM+=$arg