/* 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/hasher.h> /* ALGO_* */
#include <limb/loadopt.h>
#include <limb/output.h>
#include <limb/stralloc.h>
#include <limb/u16.h>
#include "ssp.h"
struct edit {
stralloc sa;
const char *secret;
size_t cmtoff;
size_t entoff;
struct otp otp;
};
COMMAND(edit, "Edit an entry", "-e ENTRY OPTION[..]",
" -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"
" -c, --counter[=NUM] Set to counter-based (HOTP) with counter of NUM [1]\n"
" -d, --digits NUM Set to return an OTP of NUM digits [6]\n"
" -e, --entry ENTRY Edit entry ENTRY\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 *ctx)
{
const struct option options[] = {
OPTION_ARG_REQ ('a', "algo", 0, OPTID_SHORTOPT),
OPTION_ARG_REQ ('C', "comments", OPT_PATH, OPTID_SHORTOPT),
OPTION_ARG_OPT ('c', "count", 0, OPTID_SHORTOPT),
OPTION_ARG_REQ ('d', "digits", 0, OPTID_SHORTOPT),
OPTION_ARG_REQ ('e', "entry", OPT_PATH | OPT_REQ, OPTID_SHORTOPT),
OPTION_ARG_REQ ('s', "secret", 0, OPTID_SHORTOPT),
OPTION_ARG_OPT ('t', "time", 0, OPTID_SHORTOPT),
LOADOPT_DONE
};
struct loadopt lo = LOADOPT_ZERO;
int c, r;
while ((c = loadopt(&ctx->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
case 'a':
{
if (!strcmp(LO_ARG(&lo), "list")) {
out("Available algorithms:");
for (r = 0; algos[r]; ++r)
out("- ", algos[r]);
diecmdusage(0, usage, &command_edit);
}
int first = -1;
r = validate_algo(&first, LO_ARG(&lo), strlen(LO_ARG(&lo)));
if (r < 0) {
if (first > -1)
list_matches(obuffer_1, OLVL_NORMAL, "did you mean ",
NULL, " or ", " ?", LO_ARG(&lo), strlen(LO_ARG(&lo)),
first, (const char **) algos);
diecmdusage(EX_USAGE, usage, &command_edit);
}
ctx->otp.algo = r;
}
break;
case 'C':
ctx->cmtoff = LO_OFF(&lo);
break;
case 'c':
if (!LO_ARG(&lo)) {
ctx->otp.n = 1;
} else {
if (validate_counter(&ctx->otp.n, LO_ARG(&lo), strlen(LO_ARG(&lo))) < 0)
diecmdusage(EX_USAGE, usage, &command_edit);
}
ctx->otp.type = TYPE_COUNTER;
break;
case 'd':
r = validate_digits(LO_ARG(&lo), strlen(LO_ARG(&lo)));
if (r < 0)
diecmdusage(EX_USAGE, usage, &command_edit);
ctx->otp.digits = r;
break;
case 'e':
ctx->entoff = LO_OFF(&lo);
break;
case 's':
ctx->secret = LO_ARG(&lo);
break;
case 't':
if (!LO_ARG(&lo)) {
ctx->otp.n = 30;
} else {
r = validate_precision(LO_ARG(&lo), strlen(LO_ARG(&lo)));
if (r < 0)
diecmdusage(EX_USAGE, usage, &command_edit);
ctx->otp.n = r;
}
ctx->otp.type = TYPE_TIME;
break;
case -1:
diecmdusage(EX_USAGE, usage, &command_edit);
default:
die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
};
return LO_CUR(&lo);
}
int
edit_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
{
struct ssp *ssp = ssp_;
struct edit ctx = { .sa = STRALLOC_ZERO, .otp.type = TYPE_UNKNOWN,
.otp.algo = ALGO_UNKNOWN, .cmtoff = (size_t) -1 };
(void) env;
parse_cmdline(argc, argv, usage, &ctx);
const char *entry = ctx.sa.s + ctx.entoff;
int r = open_db(ssp, 1);
if (r < 0)
diefu(exitcode_from_errno(errno), "open database");
r = db_find(entry, ssp);
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(ssp);
ssize_t slen;
if (!ctx.secret) {
slen = cur->slen;
} else {
slen = base32_scan(NULL, ctx.secret, strlen(ctx.secret), 0);
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 (ctx.cmtoff == (size_t) -1)
clen = strlen(cur->data + cur->slen);
else
clen = strlen(ctx.sa.s + ctx.cmtoff);
size_t otplen = sizeof(ctx.otp) + slen + clen + 1;
if (!stralloc_readyplus(&ctx.sa, otplen))
diefusys(EX_TEMPFAIL, "ctx entry ", ESC, entry, ESC);
/* re-align */
entry = ctx.sa.s + ctx.entoff;
dbg("constructing edited entry");
struct otp *p = (struct otp *) (ctx.sa.s + ctx.sa.len);
ctx.sa.len += otplen;
p->algo = (ctx.otp.algo != ALGO_UNKNOWN) ? ctx.otp.algo : cur->algo;
p->type = (ctx.otp.type != TYPE_UNKNOWN) ? ctx.otp.type : cur->type;
p->digits = (ctx.otp.digits) ? ctx.otp.digits : cur->digits;
p->n = (ctx.otp.type != TYPE_UNKNOWN) ? ctx.otp.n : cur->n;
p->slen = slen;
if (ctx.secret)
base32_scan(p->data, ctx.secret, strlen(ctx.secret), 0);
else
memcpy(p->data, cur->data, slen);
if (ctx.cmtoff == (size_t) -1)
memcpy(p->data + slen, cur->data + cur->slen, clen + 1);
else
memcpy(p->data + slen, ctx.sa.s + ctx.cmtoff, clen + 1);
if (cur->slen == p->slen && strlen(cur->data + cur->slen) == clen
&& !memcmp(p, cur, otplen))
dief(EX_DATA_ERR, "nothing to do");
quiet("Editing entry ", ESC, entry, ESC, "... ");
rebuild_db(entry, entry, p, ssp);
dbg("showing edited entry");
close_db(ssp);
const char *av[] = { NULL, entry };
run_command("show", sizeof(av) / sizeof(*av), av, env, ssp);
stralloc_free(&ctx.sa);
return 0;
}