author | Olivier Brunel
<jjk@jjacky.com> 2023-04-25 11:30:52 UTC |
committer | Olivier Brunel
<jjk@jjacky.com> 2023-04-25 15:02:08 UTC |
parent | da4a68a325d28a62219c972640cda1b5b22a9263 |
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);