Welcome to little lamb

Code » ssp » commit fa435ff

Add command get (& fix a few idiocies)

author Olivier Brunel
2023-04-25 11:30:52 UTC
committer Olivier Brunel
2023-04-25 15:02:08 UTC
parent da4a68a325d28a62219c972640cda1b5b22a9263

Add command get (& fix a few idiocies)

src/include/hotp.h +12 -0
src/include/ssp.h +16 -5
src/ssp/add.c +27 -12
src/ssp/commands.c +2 -0
src/ssp/edit.c +25 -22
src/ssp/get.c +214 -0
src/ssp/hotp.c +22 -0
src/ssp/import.c +7 -3
src/ssp/show.c +6 -4
src/ssp/ssp.c +4 -0

diff --git a/src/include/hotp.h b/src/include/hotp.h
new file mode 100644
index 0000000..fe0ec3b
--- /dev/null
+++ b/src/include/hotp.h
@@ -0,0 +1,12 @@
+/* This file is part of ssp                             https://lila.oss/ssp
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef SSP_HOTP_H
+#define SSP_HOTP_H
+
+#include <limb/hasher.h>
+#include <limb/u64.h>
+
+int hotp(hasher *hr, const char *secret, size_t slen, u64 c, int digits);
+
+#endif /* SSP_HOTP_H */
diff --git a/src/include/ssp.h b/src/include/ssp.h
index 2c73bf0..c79f90c 100644
--- a/src/include/ssp.h
+++ b/src/include/ssp.h
@@ -7,6 +7,7 @@
 #include <skalibs/stralloc.h>
 #include <limb/cdb.h>
 #include <limb/cdbmake.h>
+#include <limb/int.h>
 
 enum {
     OPT_HELP = 1 << 0,
@@ -35,7 +36,10 @@ struct ssp {
 enum {
     TYPE_COUNTER = 0,
     TYPE_TIME,
-    TYPE_UNKNOWN = (u8) -1
+    TYPE_UNKNOWN = (u8) -1,
+    /* note a "real" type, used by get to know when the counter value has been
+     * manually passed and therefore no db update should be done */
+    TYPE_COUNTER_MANUAL = TYPE_UNKNOWN - 1
 };
 
 enum {
@@ -54,15 +58,21 @@ extern const char *algos[];
 struct otp
 {
     size_t slen; /* length of secret within data; or offset to comments */
-    u8 type;
-    u8 algo;
     union {
-        u8 precision;
-        u8 digits;
+        u64 c;
+        struct {
+            u64 precision   : 6;
+            u64 unused      : 58;
+        } t;
     };
+    u8 algo;
+    u8 type;
+    u8 digits;
     char data[];
 };
 
+#define OTP_DEFAULT     { .slen = 0, .c = 1, .algo = ALGO_SHA1, .type = TYPE_COUNTER, .digits = 6 }
+
 /* ssp.c */
 int exitcode_from_errno(int e);
 int run_command(const char *name, int argc, const char *argv[], const char *env[], void *ctx);
@@ -87,6 +97,7 @@ int open_db(struct ssp *ctx);
 /* add.c */
 int validate_algo(int *first, const char *data, size_t len);
 int validate_digits(const char *data, size_t dlen);
+int validate_counter(u64 *c, const char *data, size_t dlen);
 int validate_precision(const char *data, size_t dlen);
 
 /* show.c */
diff --git a/src/ssp/add.c b/src/ssp/add.c
index 7cc694c..5e3ade8 100644
--- a/src/ssp/add.c
+++ b/src/ssp/add.c
@@ -39,13 +39,23 @@ validate_digits(const char *data, size_t dlen)
     return digits;
 }
 
+int
+validate_counter(u64 *c, const char *data, size_t dlen)
+{
+    if (u64_scan(c, data) != dlen) {
+        warn("invalid counter value: ", LEN(data, dlen));
+        return -1;
+    }
+    return 1;
+}
+
 int
 validate_precision(const char *data, size_t dlen)
 {
     dbg("validating precision ", LEN(data, dlen), "...");
     u16 u;
-    if (u16_scan(&u, data) != dlen || u < 10 || u > 120) {
-        warn("invalid precision argument, must be between 10 and 120: ", LEN(data, dlen));
+    if (u16_scan(&u, data) != dlen || u < 10 || u > 59) {
+        warn("invalid precision argument, must be between 10 and 59: ", LEN(data, dlen));
         return -1;
     }
     return u;
@@ -53,10 +63,11 @@ validate_precision(const char *data, size_t dlen)
 
 COMMAND(add, "Add a new entry",
         "<entry> [OPTION..] <secret>",
-" -a, --algo ALGO                       Set ALGO as hashing algorithm [sha256]\n"
+" -a, --algo ALGO                       Set ALGO as hashing algorithm [sha1]\n"
 "                                       Use 'list' to list available algorithms\n"
-" -c, --comments COMMENTS               Set COMMENTS as entry's comments\n"
-" -d, --digits NUM                      Set to counter-based (HOTP) using NUM digits [6]\n"
+" -C, --comments COMMENTS               Set COMMENTS as entry's comments\n"
+" -c, --counter NUM                     Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
 " -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
 );
 
@@ -66,7 +77,8 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct add *add)
 {
     const struct option options[] = {
         OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('c', "comments",                0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('C', "comments",                0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('c', "counter",                 0, OPTID_SHORTOPT),
         OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
         OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
         OPTION_DONE
@@ -96,24 +108,28 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct add *add)
                 add->otp.algo = r;
             }
             break;
-        case 'c':
+        case 'C':
             add->comments = PO_ARG(&po);
             break;
+        case 'c':
+            if (validate_counter(&add->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
+                diecmdusage(EX_USAGE, usage, &command_add);
+            add->otp.type = TYPE_COUNTER;
+            break;
         case 'd':
             r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
             if (r < 0)
                 diecmdusage(EX_USAGE, usage, &command_add);
-            add->otp.type = TYPE_COUNTER;
             add->otp.digits = r;
             break;
         case 't':
             if (!PO_ARG(&po)) {
-                add->otp.precision = 30;
+                add->otp.t.precision = 30;
             } else {
                 r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
                 if (r < 0)
                     diecmdusage(EX_USAGE, usage, &command_add);
-                add->otp.precision = r;
+                add->otp.t.precision = r;
             }
             add->otp.type = TYPE_TIME;
             break;
@@ -130,8 +146,7 @@ int
 add_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
 {
     struct ssp *ctx = ctx_;
-    struct add add = { .otp.type = TYPE_COUNTER, .otp.algo = ALGO_SHA256,
-        .otp.digits = 6, .comments = "" };
+    struct add add = { .otp = OTP_DEFAULT, .comments = "" };
     const char *entry;
     int i;
 
diff --git a/src/ssp/commands.c b/src/ssp/commands.c
index 6dc18d4..05f0395 100644
--- a/src/ssp/commands.c
+++ b/src/ssp/commands.c
@@ -6,6 +6,7 @@
 extern struct command command_add;
 extern struct command command_edit;
 extern struct command command_export;
+extern struct command command_get;
 extern struct command command_import;
 extern struct command command_list;
 extern struct command command_remove;
@@ -16,6 +17,7 @@ struct command *commands[] = {
     &command_add,
     &command_edit,
     &command_export,
+    &command_get,
     &command_import,
     &command_list,
     &command_remove,
diff --git a/src/ssp/edit.c b/src/ssp/edit.c
index c9614a7..dca99ee 100644
--- a/src/ssp/edit.c
+++ b/src/ssp/edit.c
@@ -20,10 +20,11 @@ struct edit {
 
 COMMAND(edit, "Edit an entry",
         "<entry> OPTION[..]",
-" -a, --algo ALGO                       Set ALGO as hashing algorithm [sha256]\n"
+" -a, --algo ALGO                       Set ALGO as hashing algorithm\n"
 "                                       Use 'list' to list available algorithms\n"
-" -c, --comments COMMENTS               Set COMMENTS as entry's comments\n"
-" -d, --digits[=NUM]                    Set to counter-based (HOTP) using NUM digits [6]\n"
+" -C, --comments COMMENTS               Set COMMENTS as entry's comments\n"
+" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
 " -s, --secret SECRET                   Set SECRET as entry's secret\n"
 " -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
 );
@@ -34,8 +35,9 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct edit *edi
 {
     const struct option options[] = {
         OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('c', "comments",                0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('d', "digits",                  0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('C', "comments",                0, OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('c', "count",                   0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
         OPTION_ARG_REQ ('s', "secret",                  0, OPTID_SHORTOPT),
         OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
         OPTION_DONE
@@ -65,31 +67,35 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct edit *edi
                 edit->otp.algo = r;
             }
             break;
-        case 'c':
+        case 'C':
             edit->comments = PO_ARG(&po);
             break;
-        case 'd':
+        case 'c':
             if (!PO_ARG(&po)) {
-                edit->otp.digits = 6;
+                edit->otp.c = 1;
             } else {
-                r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
-                if (r < 0)
+                if (validate_counter(&edit->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
                     diecmdusage(EX_USAGE, usage, &command_edit);
-                edit->otp.digits = r;
             }
             edit->otp.type = TYPE_COUNTER;
             break;
+        case 'd':
+            r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
+            if (r < 0)
+                diecmdusage(EX_USAGE, usage, &command_edit);
+            edit->otp.digits = r;
+            break;
         case 's':
             edit->secret = PO_ARG(&po);
             break;
         case 't':
             if (!PO_ARG(&po)) {
-                edit->otp.precision = 30;
+                edit->otp.t.precision = 30;
             } else {
                 r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
                 if (r < 0)
                     diecmdusage(EX_USAGE, usage, &command_edit);
-                edit->otp.precision = r;
+                edit->otp.t.precision = r;
             }
             edit->otp.type = TYPE_TIME;
             break;
@@ -167,15 +173,12 @@ edit_main(int argc, const char *argv[], const char *env[], const char usage[], v
     char buf[sizeof(edit.otp) + slen + clen + 1];
     struct otp *p = (struct otp *) buf;
     p->algo = (edit.otp.algo != ALGO_UNKNOWN) ? edit.otp.algo : cur->algo;
-    /* we only use digits below because both digits & precision are actualy the
-     * same variable (union) */
-    if (edit.otp.type != TYPE_UNKNOWN) {
-        p->type = edit.otp.type;
-        p->digits = edit.otp.digits;
-    } else {
-        p->type = cur->type;
-        p->digits = cur->digits;
-    }
+    p->type = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.type : cur->type;
+    p->digits = (edit.otp.digits) ? edit.otp.digits : cur->digits;
+    if (p->type == TYPE_COUNTER)
+        p->c = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.c : cur->c;
+    else
+        p->t.precision = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.t.precision : cur->t.precision;
 
     p->slen = slen;
     if (edit.secret)
diff --git a/src/ssp/get.c b/src/ssp/get.c
new file mode 100644
index 0000000..35ec2aa
--- /dev/null
+++ b/src/ssp/get.c
@@ -0,0 +1,214 @@
+/* This file is part of ssp                             https://lila.oss/ssp
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <errno.h>
+#include <time.h>
+#include <skalibs/stralloc.h>
+#include <limb/base32.h>
+#include <limb/command.h>
+#include <limb/exitcode.h>
+#include <limb/hasher.h>
+#include <limb/hasher_sha1.h>
+#include <limb/hasher_sha256.h>
+#include <limb/hasher_sha512.h>
+#include <limb/hasher_sha3_224.h>
+#include <limb/hasher_sha3_256.h>
+#include <limb/hasher_sha3_512.h>
+#include <limb/hasher_blake3.h>
+#include <limb/output.h>
+#include <limb/parseopt.h>
+#include <limb/u32.h>
+#include "ssp.h"
+#include "hotp.h"
+
+struct get {
+    struct otp otp;
+    const char *secret;
+    u64 ts;
+};
+
+COMMAND(get, "Get a One-Time Password", "<entry> [OPTION..]",
+" -a, --algo ALGO                       Set ALGO as hashing algorithm [sha1]\n"
+" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
+" -s, --secret SECRET                   Set SECRET as entry's secret\n"
+" -T, --use-time TS                     Use TS as unix timestamp instead of current time\n"
+" -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
+);
+
+static int
+parse_cmdline(int argc, const char *argv[], const char usage[], struct get *get)
+{
+    const struct option options[] = {
+        OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('c', "counter",                 0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('s', "secret",                  0, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('T', "use-time",                0, OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
+        OPTION_DONE
+    };
+    struct parseopt po = { 0 };
+
+    int c, r;
+    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+        case 'a':
+            {
+                int first = -1;
+                r = validate_algo(&first, PO_ARG(&po), strlen(PO_ARG(&po)));
+                if (r < 0) {
+                    if (first > -1)
+                        list_matches(obuffer_1, OLVL_NORMAL, "did you mean ",
+                                     NULL, " or ", " ?", PO_ARG(&po), strlen(PO_ARG(&po)),
+                                     first, algos);
+                    diecmdusage(EX_USAGE, usage, &command_get);
+                }
+                get->otp.algo = r;
+            }
+            break;
+        case 'c':
+            if (!PO_ARG(&po)) {
+                get->otp.c = 1;
+            } else {
+                if (validate_counter(&get->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
+                    diecmdusage(EX_USAGE, usage, &command_get);
+            }
+            get->otp.type = TYPE_COUNTER_MANUAL;
+            break;
+        case 'd':
+            r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
+            if (r < 0)
+                diecmdusage(EX_USAGE, usage, &command_get);
+            get->otp.digits = r;
+            break;
+        case 's':
+            get->secret = PO_ARG(&po);
+            get->otp.slen = 0;
+            break;
+        case 'T':
+            if (!u640_scan(&get->ts, PO_ARG(&po))) {
+                warn("invalid time stamp: ", PO_ARG(&po));
+                diecmdusage(EX_USAGE, usage, &command_get);
+            }
+            break;
+        case 't':
+            if (!PO_ARG(&po)) {
+                get->otp.t.precision = 30;
+            } else {
+                r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
+                if (r < 0)
+                    diecmdusage(EX_USAGE, usage, &command_get);
+                get->otp.t.precision = r;
+            }
+            get->otp.type = TYPE_TIME;
+            break;
+        case -1:
+            diecmdusage(EX_USAGE, usage, &command_get);
+        default:
+            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+    };
+
+    return PO_CUR(&po);
+}
+
+int
+get_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+{
+    struct ssp *ctx = ctx_;
+    struct get get = { .otp = OTP_DEFAULT, .otp.type = TYPE_COUNTER_MANUAL,
+        .secret = NULL, .ts = (u64) -1 };
+
+    if (argc == 1) {
+        warn("argument \"entry\" missing");
+        diecmdusage(EX_USAGE, usage, &command_get);
+    }
+
+    struct otp *otp;
+    if (strcmp(argv[1], "-")) {
+        const char *entry = get_entry_name(argv[1], ctx);
+        if (!entry)
+            diefusys(exitcode_from_errno(errno), "parse entry's name");
+
+        int r = open_db(ctx);
+        if (r < 0)
+            diefu(exitcode_from_errno(errno), "open database");
+        if (!r)
+            dief(EX_NOINPUT, "no database");
+
+        r = db_find(entry, ctx);
+        if (r < 0)
+            diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
+        if (!r)
+            dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
+
+        get.otp = *db_otp(ctx);
+        get.secret = db_otp(ctx)->data;
+    }
+
+    --argc;
+    ++argv;
+    int i = parse_cmdline(argc, argv, usage, &get);
+
+    if (argc > i) {
+        warn("too many arguments");
+        diecmdusage(EX_USAGE, usage, &command_get);
+    }
+
+    if (!get.secret)
+        dief(EX_DATA_ERR, "secret missing");
+
+    if (!get.otp.slen) {
+        size_t l = strlen(get.secret);
+        ssize_t r = base32_scan(NULL, get.secret, l, 0);
+        if (r < 0)
+            dief(EX_DATA_ERR, "invalid secret: Not in base32");
+        if (!stralloc_readyplus(&ctx->sa, r))
+            diefusys(EX_TEMPFAIL, "process secret");
+        base32_scan(ctx->sa.s + ctx->sa.len, get.secret, l , 0);
+        get.secret = ctx->sa.s + ctx->sa.len;
+        /* we don't update ctx->sa.len on purpose, so in case we need to rewrite
+         * the db the calculation is easier */
+        get.otp.slen = r;
+    }
+
+    hasher *hr = sha1;
+    switch (get.otp.algo) {
+        case ALGO_SHA256:   hr = sha256;   break;
+        case ALGO_SHA512:   hr = sha512;   break;
+        case ALGO_SHA3_224: hr = sha3_224; break;
+        case ALGO_SHA3_256: hr = sha3_256; break;
+        case ALGO_SHA3_512: hr = sha3_512; break;
+        case ALGO_BLAKE3:   hr = blake3;   break;
+    }
+
+    u64 c;
+
+    if (get.otp.type == TYPE_TIME) {
+        if (get.ts == (u64) -1)
+            get.ts = time(NULL);
+        c = get.ts / get.otp.t.precision;
+    } else {
+        c = get.otp.c;
+    }
+
+    dbg("calculating HOTP c=", PUTMSG_UINT(c), " digits=", PUTMSG_UINT(get.otp.digits));
+    int r = hotp(hr, get.secret, get.otp.slen, c, get.otp.digits);
+    add((get.otp.type == TYPE_COUNTER) ? "H" : "T", "OTP: ");
+    char buf[get.otp.digits + 1];
+    buf[u320_fmt(buf, r, sizeof(buf) - 1)] = 0;
+    quiet(buf);
+
+    /* TYPE_COUNTER implies to write the db with an updated/incremented
+     * counter (for next time) */
+    if (get.otp.type == TYPE_COUNTER) {
+        struct otp *e = (struct otp *) db_otp(ctx);
+        ++e->c;
+
+        size_t off = (ctx->db) ? 0 : strlen(ctx->sa.s) + 1;
+        off += sizeof(u32);
+        if (!write_db(ctx->sa.s + off, ctx->sa.len - off, ctx))
+            diefusys((errno == EINVAL) ? EX_DATA_ERR : EX_CANTCREAT, "save database");
+    }
+
+    return 0;
+}
diff --git a/src/ssp/hotp.c b/src/ssp/hotp.c
new file mode 100644
index 0000000..a40c8dd
--- /dev/null
+++ b/src/ssp/hotp.c
@@ -0,0 +1,22 @@
+/* This file is part of ssp                             https://lila.oss/ssp
+ * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
+/* SPDX-License-Identifier: GPL-2.0-only */
+#include <limb/hmac.h>
+#include "hotp.h"
+
+int
+hotp(hasher *hr, const char *secret, size_t slen, u64 c, int digits)
+{
+    char buf[hr->hlen];
+    u64p_be(&c);
+
+    hmac(buf, hr, secret, slen, &c, sizeof(c));
+
+    u64 tenpow[] = { 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000 };
+    int offset = buf[hr->hlen - 1] & 0xf;
+    int r = (buf[offset + 0] & 0x7f) << 24
+        |   (buf[offset + 1] & 0xff) << 16
+        |   (buf[offset + 2] & 0xff) <<  8
+        |   (buf[offset + 3] & 0xff);
+    return r % tenpow[digits - 5];
+}
diff --git a/src/ssp/import.c b/src/ssp/import.c
index ff78e5c..0d59128 100644
--- a/src/ssp/import.c
+++ b/src/ssp/import.c
@@ -125,14 +125,18 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                 r = validate_algo(NULL, copa_value(&copa), copa_vlen(&copa));
                 if (r < 0) goto err;
                 otp.algo = r;
-            } else if (otp.type == TYPE_COUNTER && copa_nlen(&copa) == 6
-                    && !strncmp(copa_name(&copa), "digits", 6)) {
+            } else if (copa_nlen(&copa) == 6 && !strncmp(copa_name(&copa), "digits", 6)) {
                 r = validate_digits(copa_value(&copa), copa_vlen(&copa));
                 if (r < 0) goto err;
                 otp.digits = r;
+            } else if (copa_nlen(&copa) == 7 && !strncmp(copa_name(&copa), "counter", 7)) {
+                if (validate_counter(&otp.c, copa_value(&copa), copa_vlen(&copa)) < 0)
+                    goto err;
             } else if (copa_nlen(&copa) == 4 && !strncmp(copa_name(&copa), "time", 4)) {
                 if (copa_vlen(&copa) == 1 && copa_value(&copa)[0] == '1')
                     otp.type = TYPE_TIME;
+                else if (copa_vlen(&copa) == 1 && copa_value(&copa)[0] == '0')
+                    otp.type = TYPE_COUNTER;
                 else
                     dief(EX_DATA_ERR, "invalid value for \"time\" in section/entry ",
                          ESC, LEN(name, nlen), ESC, ": ",
@@ -141,7 +145,7 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                     && !strncmp(copa_name(&copa), "precision", 9)) {
                 r = validate_precision(copa_value(&copa), copa_vlen(&copa));
                 if (r < 0) goto err;
-                otp.precision = r;
+                otp.t.precision = r;
             } else if (copa_nlen(&copa) == 6 && !strncmp(copa_name(&copa), "secret", 6)) {
                 slen = base32_scan(NULL, copa_value(&copa), copa_vlen(&copa), 0);
                 if (slen < 0)
diff --git a/src/ssp/show.c b/src/ssp/show.c
index 54b9a24..79aa504 100644
--- a/src/ssp/show.c
+++ b/src/ssp/show.c
@@ -67,9 +67,10 @@ show_entry(unsigned options, struct ssp *ctx)
     out("   Algo: ", algos[otp->algo]);
     add("   ", types[otp->type], "-based; ");
     if (otp->type == TYPE_COUNTER)
-        out("Digits: ", PUTMSG_UINT(otp->digits));
+        out("Counter: ", PUTMSG_UINT(otp->c));
     else /* TYPE_TIME */
-        out("Precision: ", PUTMSG_UINT(otp->precision), "s");
+        out("Precision: ", PUTMSG_UINT(otp->t.precision), "s");
+    out("   Digits: ", PUTMSG_UINT(otp->digits));
     if (options & OPT_SECRET) {
         size_t l = base32_fmt(NULL, otp->data, otp->slen, 0);
         char buf[l+1];
@@ -91,11 +92,12 @@ show_entry_ini(unsigned options, struct ssp *ctx)
 
     out("[", PUTMSG_TOGGLE_ESC, db_entry(ctx), PUTMSG_TOGGLE_ESC, "]");
     out("algo=", algos[otp->algo]);
+    out("digits=", PUTMSG_UINT(otp->digits));
     if (otp->type == TYPE_TIME) {
         out("time=1");
-        out("precision=", PUTMSG_UINT(otp->precision));
+        out("precision=", PUTMSG_UINT(otp->t.precision));
     } else {
-        out("digits=", PUTMSG_UINT(otp->digits));
+        out("counter=", PUTMSG_UINT(otp->c));
     }
     size_t l = base32_fmt(NULL, otp->data, otp->slen, 1);
     char buf[l + 1];
diff --git a/src/ssp/ssp.c b/src/ssp/ssp.c
index 6845a7e..31742f4 100644
--- a/src/ssp/ssp.c
+++ b/src/ssp/ssp.c
@@ -90,6 +90,7 @@ parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
         OPTION_ARG_OPT ( 0 , "debug",                   0, OPTID_DEBUG),
         OPTION_ARG_REQ ('D', "database",                0, OPTID_SHORTOPT),
         OPTION_ARG_NONE('h', "help",                    0, OPTID_SHORTOPT),
+        OPTION_ARG_NONE('q', "quiet",                   0, OPTID_SHORTOPT),
         OPTION_ARG_NONE( 0 , "version",                 0, OPTID_VERSION),
         OPTION_DONE
     };
@@ -103,6 +104,9 @@ parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
         case 'h':
             ctx->options |= OPT_HELP;
             break;
+        case 'q':
+            autoopt_quiet(&options[PO_IDX(&po)], PO_ARG(&po));
+            break;
         case OPTID_DEBUG:
             if (!autoopt_debug(&options[PO_IDX(&po)], PO_ARG(&po)))
                 dieusage(EX_USAGE, usage);