author | Olivier Brunel
<jjk@jjacky.com> 2023-04-18 16:53:00 UTC |
committer | Olivier Brunel
<jjk@jjacky.com> 2023-04-19 07:43:49 UTC |
parent | 038bde7568e5beb6ec7abfe03cdaf104128d9cb5 |
src/include/ssp.h | +3 | -1 |
src/ssp/commands.c | +2 | -0 |
src/ssp/database.c | +6 | -0 |
src/ssp/edit.c | +207 | -0 |
diff --git a/src/include/ssp.h b/src/include/ssp.h index fc63971..ba8f30f 100644 --- a/src/include/ssp.h +++ b/src/include/ssp.h @@ -33,7 +33,8 @@ struct ssp { enum { TYPE_COUNTER = 0, - TYPE_TIME + TYPE_TIME, + TYPE_UNKNOWN = (1 << 2) - 1 /* max for 2 bits */ }; enum { @@ -44,6 +45,7 @@ enum { ALGO_SHA3_256, ALGO_SHA3_512, ALGO_BLAKE3, + ALGO_UNKNOWN = (1 << 4) - 1 /* max for 4 bits */ }; extern const char *algos[]; diff --git a/src/ssp/commands.c b/src/ssp/commands.c index 8d181b2..ab68f14 100644 --- a/src/ssp/commands.c +++ b/src/ssp/commands.c @@ -4,6 +4,7 @@ #include <limb/command.h> extern struct command command_add; +extern struct command command_edit; extern struct command command_list; extern struct command command_remove; extern struct command command_rename; @@ -11,6 +12,7 @@ extern struct command command_show; struct command *commands[] = { &command_add, + &command_edit, &command_list, &command_remove, &command_rename, diff --git a/src/ssp/database.c b/src/ssp/database.c index d78e657..d923fb2 100644 --- a/src/ssp/database.c +++ b/src/ssp/database.c @@ -105,6 +105,12 @@ rebuild_db(const char *oldkey, const char *newkey, const struct otp *otp, struct cdbmaker_sa mkr; int r = 0; + /* oldkey & newkey : renaming/editing an entry + * oldkey & NULL : removing an entry + * NULL & newkey : adding an entry + * NULL & NULL : isn't allowed + */ + if (newkey) { nlen = strlen(newkey) + 1; vlen = sizeof(*otp) + otp->slen + strlen(otp->data + otp->slen) + 1; diff --git a/src/ssp/edit.c b/src/ssp/edit.c new file mode 100644 index 0000000..7f7d796 --- /dev/null +++ b/src/ssp/edit.c @@ -0,0 +1,207 @@ +/* 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 <limb/base32.h> +#include <limb/bytestr.h> +#include <limb/command.h> +#include <limb/esc.h> +#include <limb/exitcode.h> +#include <limb/output.h> +#include <limb/parseopt.h> +#include <limb/u16.h> +#include "ssp.h" + +struct edit { + struct otp otp; + const char *comments; + const char *secret; +}; + +COMMAND(edit, "Edit an entry", + "<entry> OPTION[..]", +" -a, --algo ALGO Set ALGO as hashing algorithm [sha256]\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" +" -s, --secret SECRET Set SECRET as entry's secret\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 edit *edit) +{ + 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 ('s', "secret", 0, OPTID_SHORTOPT), + OPTION_ARG_OPT ('t', "time", 0, OPTID_SHORTOPT), + OPTION_DONE + }; + struct parseopt po = { 0 }; + + int c; + while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) { + case 'a': + { + int i, first = -1; + i = byte_get_match(&first, po.arg, strlen(po.arg), algos); + if (i < 0) { + if (!strcmp(po.arg, "list")) { + out("Available algorithms:"); + for (i = 0; algos[i]; ++i) + out("- ", algos[i]); + diecmdusage(0, usage, &command_edit); + } + warn("invalid algorithm: ", po.arg); + if (first > -1) + list_matches(out_putmsg, OLVL_NORMAL, "did you mean ", + NULL, " or ", " ?", po.arg, strlen(po.arg), + first, algos); + diecmdusage(EX_USAGE, usage, &command_edit); + } + edit->otp.algo = i; + } + break; + case 'c': + edit->comments = po.arg; + break; + case 'd': + edit->otp.type = TYPE_COUNTER; + if (!po.arg) { + edit->otp.digits = 6; + } else { + edit->otp.digits = *po.arg - '0'; + if (po.arg[1] || edit->otp.digits < 5 || edit->otp.digits > 9) { + warn("invalid number of digits, must be between 5 and 9: ", po.arg); + diecmdusage(EX_USAGE, usage, &command_edit); + } + } + break; + case 's': + edit->secret = po.arg; + break; + case 't': + edit->otp.type = TYPE_TIME; + if (!po.arg) { + edit->otp.precision = 30; + } else { + u16 u; + if (!u160_scan(&u, po.arg) || u < 10 || u > 120) { + warn("invalid precision argument, must be between 10 and 120: ", po.arg); + diecmdusage(EX_USAGE, usage, &command_edit); + } + edit->otp.precision = u; + } + break; + case -1: + diecmdusage(EX_USAGE, usage, &command_edit); + default: + die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt"); + }; + + return po.cur; +} + +int +edit_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_) +{ + struct ssp *ctx = ctx_; + struct edit edit = { .otp.type = TYPE_UNKNOWN, .otp.algo = ALGO_UNKNOWN, }; + const char *entry; + int i; + + if (argc == 1) { + warn("argument \"entry\" missing"); + diecmdusage(EX_USAGE, usage, &command_edit); + } + entry = get_entry_name(argv[1], ctx); + if (!entry) + diefusys(exitcode_from_errno(errno), "parse entry's name"); + if (*entry == '-') { + warn("argument \"entry\" cannot start with a '-'"); + diecmdusage(EX_DATA_ERR, usage, &command_edit); + } + + if (argc == 2) die(0, "nothing to do"); + + --argc; + ++argv; + i = parse_cmdline(argc, argv, usage, &edit); + + if (argc > i) { + warn("too many arguments"); + diecmdusage(EX_USAGE, usage, &command_edit); + } + + 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"); + + const struct otp *cur = db_otp(ctx); + + ssize_t slen; + if (!edit.secret) { + slen = cur->slen; + } else { + slen = base32_scan(NULL, edit.secret, strlen(edit.secret)); + if (slen < 0) + dief(EX_DATA_ERR, "invalid secret: Not in base32"); + if (!slen) + dief(EX_DATA_ERR, "secret is empty"); + } + + size_t clen; + if (!edit.comments) + clen = strlen(cur->data + cur->slen); + else + clen = strlen(edit.comments); + + 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->slen = slen; + if (edit.secret) + base32_scan(p->data, edit.secret, strlen(edit.secret)); + else + memcpy(p->data, cur->data, slen); + + if (edit.comments) { + if (clen >= 2 && edit.comments[0] == '"' && edit.comments[clen - 1] == '"') { + ++edit.comments; + clen -= 2; + slen = unesc(p->data + slen, clen, edit.comments, clen); + if (slen < 0) + diefusys(EX_DATA_ERR, "read comments"); + } else { + memcpy(p->data + slen, edit.comments, clen + 1); + } + } else { + memcpy(p->data + slen, cur->data + slen, clen + 1); + } + + out("Editing entry ", ESC, entry, ESC, "... "); + rebuild_db(entry, entry, p, ctx); + + return 0; +}