author | Olivier Brunel
<jjk@jjacky.com> 2023-04-17 17:59:49 UTC |
committer | Olivier Brunel
<jjk@jjacky.com> 2023-04-17 21:20:20 UTC |
parent | 8ff1ef0f791e14cb765ff962639322ee0480bc86 |
.gitignore | +3 | -0 |
skalibs | +0 | -1 |
src/include/ssp.h | +84 | -0 |
src/ssp/add.c | +141 | -0 |
src/ssp/commands.c | +17 | -0 |
src/ssp/database.c | +187 | -0 |
src/ssp/list.c | +114 | -0 |
src/ssp/show.c | +134 | -0 |
src/ssp/ssp.c | +97 | -43 |
diff --git a/.gitignore b/.gitignore index 07124e2..7094b31 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ /src/include/config.h /build /limb +/limb.built +/skalibs +/skalibs.built /ssp diff --git a/skalibs b/skalibs deleted file mode 120000 index e7d8e9f..0000000 --- a/skalibs +++ /dev/null @@ -1 +0,0 @@ -../skalibs \ No newline at end of file diff --git a/src/include/ssp.h b/src/include/ssp.h new file mode 100644 index 0000000..0086654 --- /dev/null +++ b/src/include/ssp.h @@ -0,0 +1,84 @@ +/* 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_SSP_H +#define SSP_SSP_H + +#include <skalibs/stralloc.h> +#include <limb/cdb.h> + +enum { + OPT_HELP = 1 << 0, +}; + +#define SSP_MAGIC 0x53535000 +#define SSP_MAGIC_INT 0x73737042 + +#define KEY_LEN 32 +#define SALT_LEN 32 +#define NONCE_LEN 12 +#define PWD_MAX 136 +#define ITER 1000 + +struct ssp { + stralloc sa; + cdb cdb; + cdb_data key; + cdb_data val; + const char *db; + unsigned int options; + u32 pos; + char pwd[PWD_MAX + 1]; +}; + +enum { + TYPE_COUNTER = 0, + TYPE_TIME +}; + +enum { + ALGO_SHA1 = 0, + ALGO_SHA256, + ALGO_SHA512, + ALGO_SHA3_224, + ALGO_SHA3_256, + ALGO_SHA3_512, + ALGO_BLAKE3, +}; + +extern const char *algos[]; + +struct otp +{ + size_t slen; /* length of secret within data; or offset to comments */ + u8 type : 2; + u8 algo : 4; + u8 _unused : 2; + union { + u8 precision; + u8 digits; + }; + char data[]; +}; + +/* ssp.c */ +int exitcode_from_errno(int e); +const char *get_site_name(const char *s, struct ssp *ctx); + +/* database.c */ +void rebuild_db(const char *oldkey, const char *newkey, struct otp *otp, struct ssp *ctx); +int open_db(struct ssp *ctx); + +#define db_file(ctx) (((ctx)->db) ? (ctx)->db : (ctx)->sa.s) +#define db_reset(ctx) (ctx)->pos = CDB_TRAVERSE_INIT() +#define db_next(ctx) cdb_traverse_next(&(ctx)->cdb, &(ctx)->key, &(ctx)->val, &(ctx)->pos) +#define db_find(site,ctx) ((ctx)->key.s = site, (ctx)->key.len = strlen(site) + 1, \ + cdb_find(&(ctx)->cdb, &(ctx)->val, (ctx)->key.s, (ctx)->key.len)) +#define db_site(ctx) (ctx)->key.s +#define db_otp(ctx) (const struct otp *) (ctx)->val.s + +/* show.c */ +void show_site(unsigned options, struct ssp *ctx); +void show_site_ini(struct ssp *ctx); + +#endif /* SSP_SSP_H */ diff --git a/src/ssp/add.c b/src/ssp/add.c new file mode 100644 index 0000000..a9104d6 --- /dev/null +++ b/src/ssp/add.c @@ -0,0 +1,141 @@ +/* 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/exitcode.h> +#include <limb/output.h> +#include <limb/parseopt.h> +#include <limb/u16.h> +#include "ssp.h" + +COMMAND(add, "Add a new site to the database", + "<site> [OPTION..] <secret>", +" -a, --algo ALGO Set ALGO as hashing algorithm [sha256]\n" +" Use 'list' to list available algorithms\n" +" -c, --comments COMMENTS Set COMMENTS as site's comments\n" +" -d, --digits NUM Set to counter-based (HOTP) using NUM digits [6]\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 otp *otp) +{ + const struct option options[] = { + OPTION_ARG_REQ ('a', "algo", 0, OPTID_SHORTOPT), + OPTION_ARG_REQ ('c', "comments", 0, OPTID_SHORTOPT), + OPTION_ARG_REQ ('d', "digits", 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_add); + } + 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_add); + } + otp->algo = i; + } + break; + case 'd': + otp->type = TYPE_COUNTER; + otp->digits = *po.arg - '0'; + if (po.arg[1] || otp->digits < 5 || otp->digits > 9) { + warn("invalid number of digits, must be between 5 and 9: ", po.arg); + diecmdusage(EX_USAGE, usage, &command_add); + } + break; + case 't': + if (!po.arg) { + 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_add); + } + otp->precision = u; + } + otp->type = TYPE_TIME; + break; + case -1: + diecmdusage(EX_USAGE, usage, &command_add); + default: + die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt"); + }; + + return po.cur; +} + +int +add_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_) +{ + struct ssp *ctx = ctx_; + struct otp otp = { .type = TYPE_COUNTER, .algo = ALGO_SHA256, .digits = 6 }; + const char *site; + int i; + + if (argc == 1) { + warn("argument \"site\" missing"); + diecmdusage(EX_USAGE, usage, &command_add); + } + site = get_site_name(argv[1], ctx); + if (!site) + diefusys(exitcode_from_errno(errno), "parse site's name"); + if (*site == '-') { + warn("argument \"site\" cannot start with a '-'"); + diecmdusage(EX_DATA_ERR, usage, &command_add); + } + + --argc; + ++argv; + i = parse_cmdline(argc, argv, usage, &otp); + + if (i == argc) { + warn("argument \"secret\" missing"); + diecmdusage(EX_USAGE, usage, &command_add); + } else if (i + 1 < argc) { + warn("too many arguments"); + diecmdusage(EX_USAGE, usage, &command_add); + } + + const char *secret = argv[i]; + ssize_t l = base32_scan(NULL, secret, strlen(secret)); + if (l < 0) + dief(EX_DATA_ERR, "invalid secret: Not in base32"); + if (!l) + dief(EX_DATA_ERR, "secret is empty"); + + char buf[sizeof(otp) + l + 1]; + struct otp *p = (struct otp *) buf; + memcpy(p, &otp, sizeof(otp)); + p->slen = l; + base32_scan(p->data, secret, strlen(secret)); + p->data[l] = 0; + + if (open_db(ctx) < 0) + diefu(exitcode_from_errno(errno), "open database"); + out("Adding site ", ESC, site, ESC, "... "); + rebuild_db(NULL, site, p, ctx); + + return 0; +} diff --git a/src/ssp/commands.c b/src/ssp/commands.c new file mode 100644 index 0000000..e6a2ac8 --- /dev/null +++ b/src/ssp/commands.c @@ -0,0 +1,17 @@ +/* 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/command.h> + +extern struct command command_add; +extern struct command command_list; +extern struct command command_show; + +struct command *commands[] = { + &command_add, + &command_list, + &command_show, + 0 +}; + +N_COMMANDS; diff --git a/src/ssp/database.c b/src/ssp/database.c new file mode 100644 index 0000000..f440687 --- /dev/null +++ b/src/ssp/database.c @@ -0,0 +1,187 @@ +/* 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 <string.h> +#include <skalibs/random.h> +#include <limb/djbunix.h> +#include <limb/bytestr.h> +#include <limb/cdb.h> +#include <limb/cdbmake.h> +#include <limb/chacha20.h> +#include <limb/exitcode.h> +#include <limb/hasher_sha3_256.h> +#include <limb/output.h> +#include <limb/pbkdf2.h> +#include <limb/term.h> +#include <limb/u32.h> +#include "ssp.h" + +int +get_key(char key[KEY_LEN], const char salt[SALT_LEN], const char *prompt, struct ssp *ctx) +{ + ssize_t plen; + if (!*ctx->pwd) { + plen = ask_password(ctx->pwd, sizeof(ctx->pwd), prompt); + if (plen < 0) return 0; + } else { + plen = strlen(ctx->pwd); + } + + /* get encryption key */ + pbkdf2(key, KEY_LEN, sha3_256, ctx->pwd, plen, salt, SALT_LEN, ITER); + + return 1; +} + +int +open_db(struct ssp *ctx) +{ + int fd = open_read(db_file(ctx)); + if (fd < 0) { + /* no file is not an error at this point */ + if (errno == ENOENT) { + ctx->cdb = cdb_zero; + return 0; + } + warnusys("open ", ESC, db_file(ctx), ESC); + return -1; + } + + size_t salen = ctx->sa.len; + if (!slurp(&ctx->sa, fd)) { + warnusys("read ", ESC, db_file(ctx), ESC); + fd_close(fd); + return -1; + } + fd_close(fd); + + /* check our magic */ + u32p_be((u32 *) (ctx->sa.s + salen)); + if (* (u32 *) (ctx->sa.s + salen) != SSP_MAGIC) { + warnu("read ", ESC, db_file(ctx), ESC, ": not an SSP database"); + return (errno = ENOMSG, -1); + } + /* set off past the magic, to the salt */ + size_t off = salen + sizeof(u32); + char salt[SALT_LEN]; + memcpy(salt, ctx->sa.s + off, sizeof(salt)); + /* move off past salt, to encrypted data */ + off += sizeof(salt); + + /* check the fileis big enough to contain up to encrypted data */ + if (ctx->sa.len <= off) { + warnu("read ", ESC, db_file(ctx), ESC, ": file corrupted"); + return (errno = EBADE, -1); + } + + char key[KEY_LEN]; + if (!get_key(key, salt, "Enter database password: ", ctx)) { + warnusys("read ", ESC, db_file(ctx), ESC, ": cannot read password"); + return -1; + } + + /* decrypt data in-place */ + char nonce[NONCE_LEN] = { 0 }; + chacha20(ctx->sa.s + off, key, nonce, ctx->sa.s + off, ctx->sa.len - off); + + /* check internal magic, to "validate" the password */ + u32p_be((u32 *) (ctx->sa.s + off)); + if (* (u32 *) (ctx->sa.s + off) != SSP_MAGIC_INT) { + warnu("read ", ESC, db_file(ctx), ESC, ": wrong password"); + return (errno = EINVAL, -1); + } + /* move off past internal magic, to actual cdb content */ + off += sizeof(u32); + + cdb_init_frommem(&ctx->cdb, ctx->sa.s + off, ctx->sa.len - off); + return 1; +} + +void +rebuild_db(const char *oldkey, const char *newkey, struct otp *otp, struct ssp *ctx) +{ + size_t olen = 0, nlen; + size_t vlen = sizeof(*otp) + otp->slen + strlen(otp->data + otp->slen) + 1; + cdbmaker_sa mkr; + int r = 0; + + nlen = strlen(newkey) + 1; + if (oldkey) olen = strlen(oldkey) + 1; + + if (!cdbmaker_sa_start(&mkr)) + diefusys(EX_TEMPFAIL, "save database"); + /* new file? */ + if (!ctx->cdb.map) { + if (!cdbmaker_sa_add(&mkr, newkey, nlen, (char *) otp, vlen)) + diefusys(EX_TEMPFAIL, "save database"); + } else { + int a = 0; + db_reset(ctx); + while ((r = db_next(ctx)) == 1) { + if (!a && olen && olen == ctx->key.len && !strcmp(oldkey, ctx->key.s)) { + /* replace otp */ + if (!cdbmaker_sa_add(&mkr, ctx->key.s, ctx->key.len, (char *) otp, vlen)) + diefusys(EX_TEMPFAIL, "save database"); + a = 1; + } else if (!a && !olen && nlen == ctx->key.len && !strcmp(newkey, ctx->key.s)) { + diefu(EX_DATA_ERR, "add site ", ESC, newkey, ESC, ": Site already exists"); + } else { + if (!a && !olen && strcoll(ctx->key.s, newkey) > 0) { + /* insert otp at the right place */ + if (!cdbmaker_sa_add(&mkr, newkey, nlen, (char *) otp, vlen)) + diefusys(EX_TEMPFAIL, "save database"); + a = 1; + } + if (!cdbmaker_sa_add(&mkr, ctx->key.s, ctx->key.len, ctx->val.s, ctx->val.len)) + diefusys(EX_TEMPFAIL, "save database"); + } + } + if (r < 0) diefu(EX_DATA_ERR, "read database: file corrupted"); + if (!a) { + /* insert last */ + if (!cdbmaker_sa_add(&mkr, newkey, nlen, (char *) otp, vlen)) + diefusys(EX_TEMPFAIL, "save database"); + } + } + if (!cdbmaker_sa_finish(&mkr)) + diefusys(EX_TEMPFAIL, "save database"); + + char salt[SALT_LEN]; + char enckey[KEY_LEN]; + char nonce[NONCE_LEN] = { 0 }; + + /* each time we save the db, we generate a new key to encrypt it */ + random_buf(salt, sizeof(salt)); + if (!get_key(enckey, salt, "Enter new database password: ", ctx)) + diefusys(EX_DATA_ERR, "save database: cannot read password"); + + u32 magic_int = SSP_MAGIC_INT; + u32p_be(&magic_int); + + chacha20_ctx chacha; + chacha20_init(enckey, nonce, &chacha); + chacha20_crypt(&magic_int, &magic_int, sizeof(magic_int), &chacha); + chacha20_crypt(mkr.sa.s, mkr.sa.s, mkr.sa.len, &chacha); + chacha20_clear(&chacha); + + + u32 magic = SSP_MAGIC; + u32p_be(&magic); + + struct iovec v[4]; + v[0].iov_base = &magic; + v[0].iov_len = sizeof(magic); + v[1].iov_base = salt; + v[1].iov_len = sizeof(salt); + v[2].iov_base = &magic_int; + v[2].iov_len = sizeof(magic_int); + v[3].iov_base = mkr.sa.s; + v[3].iov_len = mkr.sa.len; + + if (!openwritevnclose(db_file(ctx), v, 4)) + diefusys(EX_CANTCREAT, "save database to ", ESC, db_file(ctx), ESC); + + cdbmaker_sa_free(&mkr); + out("Database saved to ", ESC, db_file(ctx), ESC); +} diff --git a/src/ssp/list.c b/src/ssp/list.c new file mode 100644 index 0000000..c18f9ed --- /dev/null +++ b/src/ssp/list.c @@ -0,0 +1,114 @@ +/* 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 <fnmatch.h> +#include <limb/command.h> +#include <limb/exitcode.h> +#include <limb/output.h> +#include <limb/parseopt.h> +#include "ssp.h" + +enum { + OPT_DETAILS = 1 << 0, + OPT_SECRET = 1 << 1, /* must be the same as in show */ + OPT_FMT_INI = 1 << 2, +}; + +struct list { + unsigned options; + const char *sep; + const char *ptrn; +}; + +COMMAND(list, "List all sites in the database", + "[OPTION..] [<pattern>]", +" -d, --details Show sites' details\n" +" -f, --format Output in INI-like format\n" +" -S, --secret Show sites' secrets (implies --details)\n" +" -s, --sep SEP Use SEP as separator\n" +); + +static int +parse_cmdline(int argc, const char *argv[], const char usage[], struct list *ctx) +{ + const struct option options[] = { + OPTION_ARG_NONE('d', "details", 0, OPTID_SHORTOPT), + OPTION_ARG_NONE('f', "format", 0, OPTID_SHORTOPT), + OPTION_ARG_NONE('S', "secret", 0, OPTID_SHORTOPT), + OPTION_ARG_REQ ('s', "sep", 0, OPTID_SHORTOPT), + OPTION_DONE + }; + struct parseopt po = { 0 }; + + int c; + while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) { + case 'd': + ctx->options |= OPT_DETAILS; + break; + case 'f': + ctx->options |= OPT_FMT_INI; + break; + case 'S': + ctx->options |= OPT_DETAILS | OPT_SECRET; + break; + case 's': + ctx->sep = po.arg; + break; + case -1: + diecmdusage(EX_USAGE, usage, &command_list); + default: + die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt"); + }; + + return po.cur; +} + +int +list_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_) +{ + struct ssp *ctx = ctx_; + struct list list = { 0, "\n", NULL }; + + int i = parse_cmdline(argc, argv, usage, &list); + + if (i + 1 == argc) + list.ptrn = argv[i]; + + if (i + 1 < argc) { + warn("too many arguments"); + diecmdusage(EX_USAGE, usage, &command_list); + } + + int r = open_db(ctx); + if (r < 0) + diefu(exitcode_from_errno(errno), "open database"); + if (!r) + dief(0, "no database"); + + i = 0; + db_reset(ctx); + while ((r = db_next(ctx)) == 1) { + if (list.ptrn && fnmatch(list.ptrn, db_site(ctx), 0)) + continue; + if (list.options & OPT_FMT_INI) { + show_site_ini(ctx); + } else if (list.options & OPT_DETAILS) { + show_site(list.options & OPT_SECRET, ctx); + } else { + if (i) add(list.sep); + add(db_site(ctx)); + } + i = 1; + } + if (r < 0) + diefu(EX_DATA_ERR, "read database: file corrupted"); + if (i) { + /* add new line + flush buffers */ + if (!(list.options & (OPT_DETAILS | OPT_FMT_INI))) out(NULL); + } else { + out("no site matching ", ESC, list.ptrn, ESC); + } + + return 0; +} diff --git a/src/ssp/show.c b/src/ssp/show.c new file mode 100644 index 0000000..3a87b8c --- /dev/null +++ b/src/ssp/show.c @@ -0,0 +1,134 @@ +/* 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/command.h> +#include <limb/exitcode.h> +#include <limb/output.h> +#include <limb/parseopt.h> +#include "ssp.h" + +enum { + OPT_SECRET = 1 << 1, /* must be the same as in list */ + OPT_FMT_INI = 1 << 2, +}; + +struct show { + unsigned options; +}; + +COMMAND(show, "Show details of a site", + "[OPTION..] <site>", +" -f, --format Output in INI-like format\n" +" -S, --secret Show site's secret\n" +); + +static int +parse_cmdline(int argc, const char *argv[], const char usage[], struct show *ctx) +{ + const struct option options[] = { + OPTION_ARG_NONE('f', "format", 0, OPTID_SHORTOPT), + OPTION_ARG_NONE('S', "secret", 0, OPTID_SHORTOPT), + OPTION_DONE + }; + struct parseopt po = { 0 }; + + int c; + while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) { + case 'f': + ctx->options |= OPT_FMT_INI; + break; + case 'S': + ctx->options |= OPT_SECRET; + break; + case -1: + diecmdusage(EX_USAGE, usage, &command_show); + default: + die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt"); + }; + + return po.cur; +} + +void +show_site(unsigned options, struct ssp *ctx) +{ + const char *types[] = { "Counter", "Time" }; + const struct otp *otp = db_otp(ctx); + out("- Site ", ESC, db_site(ctx), ESC); + out(" Algo: ", algos[otp->algo]); + add(" ", types[otp->type], "-based; "); + if (otp->type == TYPE_COUNTER) + out("Digits: ", PUTMSG_UINT(otp->digits)); + else /* TYPE_TIME */ + out("Precision: ", PUTMSG_UINT(otp->precision), "s"); + if (options & OPT_SECRET) { + size_t l = base32_fmt(NULL, otp->data, otp->slen, 0); + char buf[l+1]; + base32_fmt(buf, otp->data, otp->slen, 0); + buf[l] = 0; + out(" Secret: ", buf); + } +} + +void +show_site_ini(struct ssp *ctx) +{ + const struct otp *otp = db_otp(ctx); + + out("[", PUTMSG_TOGGLE_ESC, db_site(ctx), PUTMSG_TOGGLE_ESC, "]"); + out("algo=", algos[otp->algo]); + if (otp->type == TYPE_TIME) { + out("time=1"); + out("precision=", PUTMSG_UINT(otp->precision)); + } else { + out("digits=", PUTMSG_UINT(otp->digits)); + } + size_t l = base32_fmt(NULL, otp->data, otp->slen, 0); + char buf[l + 1]; + base32_fmt(buf, otp->data, otp->slen, 0); + buf[l] = 0; + out("secret=", buf); +} + +int +show_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_) +{ + struct ssp *ctx = ctx_; + struct show show = { 0 }; + + int i = parse_cmdline(argc, argv, usage, &show); + + if (i == argc) { + warn("argument \"site\" missing"); + diecmdusage(EX_USAGE, usage, &command_show); + } + + if (i + 1 < argc) { + warn("too many arguments"); + diecmdusage(EX_USAGE, usage, &command_show); + } + + int r = open_db(ctx); + if (r < 0) + diefu(exitcode_from_errno(errno), "open database"); + if (!r) + dief(0, "no database"); + + const char *site = get_site_name(argv[i], ctx); + if (!site) + diefusys(exitcode_from_errno(errno), "parse site's name"); + + r = db_find(site, ctx); + if (r < 0) + diefu(EX_DATA_ERR, "find site ", ESC, site, ESC); + if (!r) + out("no site ", ESC, site, ESC, " in database"); + else if (show.options & OPT_FMT_INI) + show_site_ini(ctx); + else + show_site(show.options, ctx); + + return 0; +} diff --git a/src/ssp/ssp.c b/src/ssp/ssp.c index 39be0dd..be77bbd 100644 --- a/src/ssp/ssp.c +++ b/src/ssp/ssp.c @@ -1,76 +1,130 @@ /* 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/autoopt.h> +#include <errno.h> +#include <locale.h> +#include <skalibs/env.h> +#include <limb/command.h> +#include <limb/esc.h> #include <limb/exitcode.h> -#include <limb/loadopt.h> +#include <limb/parseopt.h> #include <limb/output.h> +#include "ssp.h" #include "config.h" const char *PROG = "ssp"; +const char *algos[] = { "sha1", "sha256", "sha512", "sha3-224", "sha3-256", + "sha3-512", "blake3", 0 }; + enum { - OPTID_DEBUG = OPTID_FIRST, - OPTID_VERSION, + OPTID_VERSION = OPTID_FIRST, }; -static void -parse_cmdline(int argc, const char *argv[], const char *file, const char *section) +int +exitcode_from_errno(int e) { - const char usage[] = "[-h] [OPTION..]"; + switch (e) { + case ENOMEM: return EX_TEMPFAIL; + case EINVAL: + case ENOMSG: + case EBADE: + return EX_DATA_ERR; + default: + return EX_IOERR; + } +} + +const char * +get_site_name(const char *s, struct ssp *ctx) +{ + ssize_t l = strlen(s); + + if (*s != '"' || s[l - 1] != '"') + /* not quoted, return as-is */ + return s; + + size_t salen = ctx->sa.len; + l -= 2; + if (!stralloc_readyplus(&ctx->sa, l + 1)) + return NULL; + /* unescape it */ + l = esc_scan(ctx->sa.s + ctx->sa.len, l, s + 1, l); + if (l < 0) + return NULL; + ctx->sa.s[l++] = 0; + ctx->sa.len += l; + return ctx->sa.s + salen; +} + +static struct command * +parse_cmdline(int *argc, const char **argv[], struct ssp *ctx) +{ + const char usage[] = "[-h] [-D database] command [...]"; const struct option options[] = { - OPTION_ARG_OPT( 0 , "debug", 0, OPTID_DEBUG), - OPTION_ARG_NONE('h', "help", OPT_SKIP, OPTID_SHORTOPT), - OPTION_ARG_REQ( 'O', "log-file", 0, OPTID_SHORTOPT), - OPTION_ARG_NONE('q', "quiet", 0, OPTID_SHORTOPT), - OPTION_ARG_NONE('v', "verbose", 0, OPTID_SHORTOPT), + OPTION_ARG_REQ ('D', "database", 0, OPTID_SHORTOPT), + OPTION_ARG_NONE('h', "help", 0, OPTID_SHORTOPT), OPTION_ARG_NONE( 0 , "version", 0, OPTID_VERSION), - LOADOPT_DONE + OPTION_DONE }; - struct loadopt lo = { 0 }; + struct parseopt po = { 0 }; int c; - while ((c = loadopt(argc, argv, options, file, section, 0, &lo))) switch (c) { - case OPTID_DEBUG: - if (!autoopt_debug(&options[lo.idx], lo.arg)) - dieusage(EX_USAGE, usage); + while ((c = parseopt(*argc, *argv, options, 0, &po))) switch (c) { + case 'D': + ctx->db = po.arg; break; case 'h': - diehelp(0, usage, -" --debug [[@[level]:]+FD|FILE] Enable debug output (to FD|FILE)\n" -" -O, --output [@[level]:]+FD|FILE Set output log to FD|FILE\n" -"\n" -" -q, --quiet Enable quiet mode\n" -" -v, --verbose Enable verbose mode\n" -"\n" -" -h, --help Show this help screen and exit\n" -" --version Show version information and exit\n" -); - case 'O': - if (!autoopt_log(&options[lo.idx], lo.arg)) - dieusage(EX_USAGE, usage); - break; - case 'q': - autoopt_quiet(&options[lo.idx], lo.arg); - break; - case 'v': - autoopt_verbose(&options[lo.idx], lo.arg); + ctx->options |= OPT_HELP; break; case OPTID_VERSION: dieversion(SSP_VERSION, "2023", SSP_CURYEAR, SSP_AUTHOR, SSP_URL, NULL); case -1: dieusage(EX_USAGE, usage); default: - die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from loadopt"); + die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt"); }; + + /* no command specified */ + if (po.cur == *argc) + dienocommand(EX_USAGE, usage, (ctx->options & OPT_HELP) ? +" -D, --database FILE Use FILE as database\n" +"\n" +" -h, --help Show (command's) help screen and exit\n" +" --version Show version information and exit\n" +: NULL); + + *argc -= po.cur; + *argv += po.cur; + + return getcommandordie(EX_USAGE, usage, **argv); } int -main(int argc, const char *argv[]) +main(int argc, const char *argv[], const char *env[]) { - const char config[] = "/etc/ssp.conf"; - parse_cmdline(argc, argv, config, NULL); + const char usage[] = "[-h] [-D database] "; + struct ssp ctx = { 0 }; + + setlocale(LC_ALL, ""); + + struct command *command = parse_cmdline(&argc, &argv, &ctx); + + if (!ctx.db) { + const char *home = env_get2(env, "HOME"); + if (!home || !*home) { + warn("$HOME not defined, using current directory instead"); + home = "."; + } + if (!stralloc_cats(&ctx.sa, home) + || !stralloc_cats(&ctx.sa, "/ssp.db") || !stralloc_0(&ctx.sa)) + diefusys(EX_TEMPFAIL, "set database path"); + } + + if (ctx.options & OPT_HELP) + diecmdhelp(0, usage, command); - out("done."); - return 0; + int r = command->main(argc, argv, env, usage, &ctx); + stralloc_free(&ctx.sa); + return r; }