/* [ʞ] soda - libsodium front end
* ~ lexi hale <lexi@hale.su>
* © AGPLv3
* @ vim: ft=c
*/
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <stdio.h>
#include <errno.h> /* boo hiss */
#include <stdint.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>
#include <sodium.h>
#include <fcntl.h>
#define sz(x) (sizeof(x)/sizeof((x)[0]))
#define _IAIA_FN_ITOA str_of_int
#define _IAIA_FN_ATOI int_of_str
#include "clib/iaia.c"
#ifdef _WIN32
# define open _open
# define close _close
# define read _read
# define write _write
#endif
#define _self_name "soda"
typedef unsigned long long word;
typedef enum atom {
/* commands */
atom_none,
atom_key,
atom_gen,
atom_pub,
atom_psk,
atom_pair,
atom_cert,
atom_sign,
atom_verify,
atom_enc,
atom_dec,
atom_wrap,
atom_unwrap,
atom_help,
_atom_cmd_n,
/* flags */
atom_verbose,
atom_quiet,
atom_noise,
atom_ascii_armor,
atom_color_on,
atom_color_off,
atom_out, atom_outfd,
atom_in, atom_infd,
atom_pw, atom_pwfd, atom_pwtty,
atom_sig, atom_sigfd,
atom_symmetric,
atom_raw,
atom_fd,
atom_fmtv,
_atom_flag_n,
/* debug values */
atom_debug,
atom_info,
atom_warn,
atom_fatal,
atom_silent,
_atom_n,
atom_capture = _atom_n
} atom;
enum noise {
noise_debug,
noise_info,
noise_warn,
noise_fatal,
noise_silent,
_noise_n
};
struct { const char* label; unsigned char color; }
noise_level_text[] = {
[noise_debug] = {"(debug",5},
[noise_info ] = {" (info", 4},
[noise_warn ] = {" (warn", 3},
[noise_fatal] = {"(fatal",1},
};
struct cmd_spec { const char* desc; const char** params; struct cmd_spec* sub; }
cmd_specs[_atom_cmd_n] = { {NULL,NULL,NULL},
[atom_key] = { "create and manipulate keypairs for public-key crypto", NULL, (struct cmd_spec[_atom_cmd_n]){
[atom_gen] = { "generate new keypair", NULL, NULL },
[atom_pub] = { "extract the public key portion from a keypair", NULL, NULL},
[atom_wrap] = { "convert a binary key to its ascii-armor representation", NULL, NULL},
}},
[atom_psk] = { "create and manipulate keys for secret-key crypto", NULL, (struct cmd_spec[_atom_cmd_n]){
[atom_gen] = { "generate new PSK", NULL, NULL },
}},
[atom_help] = { "display usage listings for commands",
(const char*[]){"commands…",NULL}, NULL },
[atom_sign] = { "sign data using public-key crypto",
(const char*[]){"keyfile",NULL}, NULL },
[atom_verify] = { "verify signature on data",
(const char*[]){"pubfile",NULL}, NULL },
[atom_enc] = { "encrypt data", NULL, (struct cmd_spec[_atom_cmd_n]){
[atom_pub] = { "encrypt against public key", (const char*[]){
"pubfile", NULL
}, NULL},
[atom_psk] = { "encrypt against symmetric, pre-shared key", (const char*[]){
"pskfile", NULL
}, NULL},
}},
[atom_wrap] = {"wrap binary input with ascii armor so it can survive transmission mechanisms that are not 8-bit clean", NULL,NULL},
[atom_unwrap] = {"translate ascii-armored input back to the original binary", NULL,NULL},
};
enum say { say_stop, say_string, say_string_list, say_char, say_uint, say_sint, say_hex };
struct sayp {
enum say kind; union {
const char* str;
const char** strlist;
char ch;
long long unsigned int uint;
long long signed int sint;
};
};
enum feature {
feature_off = 0,
feature_on = 1,
feature_auto = 2,
};
struct ctx {
size_t argc; const char** argv;
size_t cmdc; atom* cmdv;
const char* bin;
struct {
enum noise noise;
enum feature
ascii_armor,
color;
bool fd;
bool pw_force_tty;
const char* infile,* outfile,* pwfile,* sigfile;
word infd, outfd, pwfd, sigfd;
bool symcrypt;
word fmtv;
} flags;
};
bool
isws(const char c) {
/* TODO: maybe ignore all characters outside the ascii armory? */
return (c == ' ' || c == '\t' || c == '\n');
}
enum { _buffer_max = 512 };
struct buffer {
int fd; /* where to flush */
size_t sz;
char bytes[_buffer_max];
};
void buffer_flush(struct buffer* b) { write(b->fd, b->bytes, b->sz); b->sz = 0; }
#define _buffer_write(s,b) buffer_write(s, b, sizeof(b))
void buffer_write(struct buffer* b, const char* str, size_t sz) {
if (sz == 0) {
size_t i;
for (i = 0; str[i] != 0; ++i) {
const size_t ofs = (b->sz + i) % _buffer_max;
b->bytes[ofs] = str[i];
if (ofs == 0 && i != 0) buffer_flush(b);
}
b -> sz = (b->sz + i) % _buffer_max;
} else if (sz > _buffer_max) {
buffer_flush(b);
write(b->fd, str, sz);
b->sz = sz;
} else if (_buffer_max - b->sz < sz) {
buffer_flush(b);
memcpy(b->bytes, str, sz);
b->sz = sz;
} else {
memcpy(b->bytes + b->sz, str, sz);
b->sz += sz;
}
}
void buffer_push(struct buffer* b, const char c) { buffer_write(b, &c, 1); }
void say(struct ctx c, enum noise kind, struct sayp* s) {
struct buffer o = {2,0};
if (c.flags.noise > kind) return;
if (noise_level_text -> label != NULL)
if (c.flags.color) {
buffer_write(&o,"\x1b[1;3",0);
buffer_push(&o, 0x30 + noise_level_text[kind].color);
_buffer_write(&o,"m");
buffer_write(&o, noise_level_text[kind].label, 0);
_buffer_write(&o,")\x1b[m");
} else {
buffer_write(&o, noise_level_text[kind].label, 0);
buffer_push(&o,')');
}
bool bright = false;
while(s -> kind != say_stop) {
if (c.flags.color) {
if (bright) _buffer_write(&o," \x1b[1m");
else _buffer_write(&o," \x1b[m");
} else {
buffer_push(&o,' ');
}
char intbuf[128];
char* const intbuf_end = intbuf + sizeof intbuf;
char* start;
explain: switch(s -> kind) {
case say_string: buffer_write(&o, s -> str, 0); break;
case say_char: buffer_push(&o, s -> ch); break;
case say_uint:
str_of_int(10, s -> uint, intbuf, intbuf_end - 1, &start, true);
buffer_write(&o, start, intbuf_end - start);
break;
case say_sint:
/* TODO oops! iaia doesn't have signed int support yet 🙃 */
/* str_of_int(10, s -> uint, intbuf, intbuf + sizeof intbuf, &end, true) */
/* buffer_write(&o, intbuf, end - intbuf); */
break;
case say_hex:
str_of_int(16, s -> uint, intbuf, intbuf_end - 1, &start, true);
_buffer_write(&o, "0x");
buffer_write(&o, start, intbuf_end - start);
break;
case say_string_list:
buffer_push(&o,'[');
for(size_t i = 0; s -> strlist[i] != NULL; ++i) {
if (i != 0) dprintf(2,", ");
_buffer_write(&o,"“");
buffer_write(&o,s -> strlist[i],0);
_buffer_write(&o,"”");
}
buffer_push(&o,']');
break;
}
++s; bright = !bright;
}
if (c.flags.color) _buffer_write(&o, "\x1b[m\n");
else buffer_push(&o, '\n');
buffer_flush(&o);
}
enum { _atom_max_str_len = 12 };
struct { atom val; const char* str;}
/* first name listed for an atom is treated as the canonical name
* by reconstitute() and displayed in usage listings */
atom_strings[] = {
/* commands */
{atom_key, "key"}, {atom_key, "privkey"}, {atom_key, "priv"},
{atom_pub, "pub"}, {atom_pub, "pubkey"},
{atom_psk, "psk"},
{atom_cert, "cert"}, {atom_cert, "certificate"},
{atom_sign, "sign"},
{atom_verify, "verify"}, {atom_verify, "vfy"}, {atom_verify, "v"},
{atom_enc, "enc"}, {atom_enc, "encipher"}, {atom_enc, "encrypt"},
{atom_dec, "dec"}, {atom_dec, "decipher"}, {atom_dec, "decrypt"},
{atom_wrap, "wrap"}, {atom_wrap, "armor"},
{atom_unwrap, "unwrap"}, {atom_unwrap, "dearmor"}, {atom_unwrap, "disarmor"},
{atom_gen, "gen"}, {atom_gen, "generate"}, {atom_gen, "create"}, {atom_gen, "new"},
{atom_help, "help"},
/* flags */
{atom_quiet, "quiet"},
{atom_ascii_armor, "ascii-armor"},
{atom_raw, "raw"},
{atom_sig, "sig"}, {atom_sig, "sigfile"},
{atom_sigfd, "sig-fd"}, {atom_sig, "sigfd"},
{atom_verbose, "verbose"},
{atom_noise, "noise"},
{atom_color_on, "color"},
{atom_color_off, "no-color"},
{atom_out, "out"}, {atom_out, "output"}, {atom_out, "outfile"},
{atom_outfd, "out-fd"}, {atom_outfd, "outfd"},
{atom_in, "in"}, {atom_out, "input"}, {atom_out, "infile"},
{atom_infd, "in-fd"}, {atom_infd, "infd"},
{atom_pw, "pw"}, {atom_pw, "passphrase"}, {atom_pw, "password"},
{atom_pwfd, "pw-fd"}, {atom_pwfd, "pwfd"},
{atom_pwtty, "pw-tty"}, {atom_pwtty, "pwtty"},
{atom_fd, "fd"},
{atom_symmetric, "symmetric"}, {atom_symmetric, "sym"}, {atom_symmetric, "symcrypt"},
{atom_fmtv, "format"}, {atom_fmtv, "fmt"}, {atom_fmtv, "format-version"},
/* debug levels */
{atom_silent, "silent"},
{atom_debug, "debug"}, {atom_debug, "dbg"},
{atom_info, "info"},
{atom_warn, "warn"}, {atom_warn, "warnings"},
{atom_fatal, "fatal"}, {atom_fatal, "error"},
};
atom
atomize(const char* str) {
for(size_t i = 0; i < sz(atom_strings); ++i) {
if (strncmp(atom_strings[i].str, str, _atom_max_str_len) == 0) {
return atom_strings[i].val;
}
}
return atom_none;
}
const char*
reconstitute(atom a) {
for(size_t i = 0; i < sz(atom_strings); ++i) {
if (atom_strings[i].val == a) return atom_strings[i].str;
}
return NULL;
}
struct flag {
enum flag_kind {
flag_none,
_flag_noparam,
flag_on, flag_off, flag_switch,
flag_inc, flag_dec, flag_sdec,
_flag_param,
flag_atom, flag_str,
flag_uint, flag_sint,
flag_raw_atom,
flag_value
} kind;
char shortname;
const char* desc;
struct { atom first; atom last; } opts;
size_t offset;
}
#define _declflag_raw(a,k,s,o,rs,re,d) [atom_##a] = { \
.kind = flag_##k, \
.shortname = s, \
.desc = d, \
.opts = { rs,re }, \
.offset = (size_t)&(((struct ctx*)0) -> flags.o) \
},
#define _declflag(a,k,s,o,d) _declflag_raw(a,k,s,o,0,0,d)
#define _declflaga(a,rs,re,s,o,d) _declflag_raw(a,atom,s,o,rs,re,d)
flags[_atom_n] = {
{flag_none},
_declflag(ascii_armor, value + feature_on, 'a', ascii_armor,
"emit ascii-encoded data instead of raw binary "
"data (default if stdout is a TTY)")
_declflag(raw, value + feature_off, 'r', ascii_armor,
"emit binary data instead of raw ascii-armored "
"data (default if stdout is not a TTY)")
_declflag(in, str, 'i', infile,
"read data from a file instead of stdin")
_declflag(infd, uint, 'I', infd,
"read data from a file instead of stdin")
_declflag(out, str, 'o', outfile,
"write data to a file instead of stdout")
_declflag(outfd, uint, 'O', outfd,
"write data to a file instead of stdout")
_declflag(pw, str, 'p', pwfile,
"load passphrase from file instead of stdin")
_declflag(pwfd, uint, 'P', pwfd,
"load passphrase from file instead of stdin")
_declflag(pwtty, switch, 'W', pw_force_tty,
"always read password from the input TTY unless a file is specified instead")
_declflag(sig, str, 'g', sigfile,
"read signature from specified file instead of extracting it from input data")
_declflag(sigfd, uint, 'G', sigfd,
"read signature from specified file descriptor instead of extracting it from input data")
_declflag(symmetric, switch, 's', symcrypt,
"secure sensitive key material with symmetric cryptography")
_declflag(fd, switch, 'f', fd,
"interpret filename arguments to commands as file descriptor numbers")
_declflag(color_on, value + feature_on, 'c', color,
"enable colorized output (default if stderr is a TTY)")
_declflag(color_off, value + feature_off, 'C', color,
"disable colorized output")
_declflag(verbose, dec, 'v', noise,
"increase noise level")
_declflag(quiet, value + noise_silent, 'q', noise,
"emit nothing to stderr")
_declflaga(noise, atom_debug, atom_silent, 'n', noise,
"set noise level")
_declflag(fmtv, uint, 'F', fmtv,
"use a specific data file format version instead of the native version (0)")
};
const char** flag_enum_desc[_atom_n] = {
[atom_noise] = (const char*[_noise_n]){
[noise_silent] = "run in complete silence",
[noise_debug] = "emit all notices, including those useful only for debugging",
[noise_info] = "report on the progress of the operation",
[noise_warn] = "emit non-fatal warnings",
[noise_fatal] = "only emit fatal errors",
},
};
#undef _declflag
#undef _declflaga
#undef _declflag_raw
/* the casts here are a bit wacky. this is unfortunately necessary as C
* insists on evaluating each branch of the generic expression regardless
* of which type is picked. why anyone thought this was a good idea is
* beyond me. */
#define _t(x) (_Generic((x), \
const char**: (struct sayp){say_string_list, .strlist = (const char**)(size_t)(x)}, \
char: (struct sayp){say_char, .ch = (char)(size_t)(x)}, \
char*: (struct sayp){say_string, .str = (const char*)(size_t)(x)}, \
const char*: (struct sayp){say_string, .str = (const char*)(size_t)(x)}, \
int: (struct sayp){say_sint, .sint = (long long)(x)}, \
short: (struct sayp){say_sint, .sint = (long long)(x)}, \
long: (struct sayp){say_sint, .sint = (long long)(x)}, \
long long: (struct sayp){say_sint, .sint = (long long)(x)}, \
unsigned: (struct sayp){say_uint, .uint = (long long unsigned)(x)}, \
unsigned short: (struct sayp){say_uint, .uint = (long long unsigned)(x)}, \
long unsigned: (struct sayp){say_uint, .uint = (long long unsigned)(x)}, \
long long unsigned: (struct sayp){say_uint, .uint = (long long unsigned)(x)}, \
default: (struct sayp){say_hex, .uint = (size_t)(x)})),
#define _say(k,...) say(c, noise_##k, ((struct sayp[]){__VA_ARGS__ {say_stop}}))
#define _report(k,s) _say(k, _t(s))
void
usage(struct ctx c, const struct cmd_spec* tree) {
const char* bin = c.bin;
const char* fmt_flag,* fmt_usage,* fmt_enum,* head_start,* head_fin;
if (c.flags.color) {
fmt_usage = "\x1b[1musage:\x1b[m %s \x1b[32m[flags]\x1b[m%s \x1b[33mcommand\x1b[m\n";
fmt_flag = " \x1b[1;36m%c%c \x1b[;95m--%s\x1b[m: %s\n";
fmt_enum = " \x1b[94m%s\x1b[m: %s\n";
head_start = "\x1b[1m"; head_fin = "\x1b[m";
} else {
fmt_usage = "usage: %s [flags]%s <command>\n";
fmt_flag = " %c%c --%s: %s\n";
fmt_enum = " %s: %s\n";
head_start = head_fin = "";
}
char ccmd[32]; char *cptr = ccmd; *cptr = 0;
if (tree == NULL) tree = cmd_specs;
else if(c.cmdc > 1) for (size_t i = 1; i<c.cmdc; ++i) {
*(cptr++)=' ';
cptr=stpcpy(cptr, reconstitute(c.cmdv[i]));
}
dprintf(2,fmt_usage,bin,ccmd);
dprintf(2,"%scommands:%s\n",head_start,head_fin);
for (size_t i = 0; i<_atom_cmd_n; ++i) {
if (tree[i].desc == NULL) continue;
dprintf(2, " %s", reconstitute(i));
if (tree[i].params != NULL) {
for (size_t j = 0; tree[i].params[j] != NULL; ++j) {
dprintf(2," <%s>", tree[i].params[j]);
}
}
dprintf(2,"%s: %s\n",
tree[i].sub != NULL ? " …" : "",
tree[i].desc);
}
dprintf(2,"%sflags:%s\n",head_start,head_fin);
for (size_t i = _atom_cmd_n + 1; i<_atom_flag_n; ++i) {
/* if (flags[i].kind == flag_none) continue; */
dprintf(2,fmt_flag,
flags[i].shortname != 0 ? '-' : ' ',
flags[i].shortname != 0 ? flags[i].shortname : ' ',
reconstitute(i), flags[i].desc);
if (flags[i].kind == flag_atom || flags[i].kind == flag_raw_atom) {
for (atom a = flags[i].opts.first; a < flags[i].opts.last + 1; ++a) {
dprintf(2,fmt_enum,reconstitute(a),flag_enum_desc[i][a - flags[i].opts.first]);
}
}
}
}
bool
atom_match(struct ctx c, size_t* match) {
/* primitive pattern-matching function */
for (size_t i = 0; i < c.cmdc; ++i) {
if (match[i] == 0) return false;
if (match[i] < atom_capture) {
if (match[i] != c.cmdv[i]) return false;
} else if (c.cmdv[i] >= _atom_n) {
const char** dest = (const char**)(match[i] - atom_capture);
*dest = c.argv[c.cmdv[i] - _atom_n];
} else {
return false;
}
}
return match[c.cmdc] == 0;
}
/* the ascii armory is intentionally limited to characters that are
* not only printable but that are unlikely to have special meaning
* such that they would require escaping for any form of input.
* while this is a more limited set than base64, it's significantly
* stronger armor -- a user can, for instance, simply paste it into
* most shells without quoting. */
const char ascii_armory[] = "0123456789/abcdefghijklmopqrstuvwxyz."
"-ABCDEFGHIJKLMOPQRSTUVWXYZ_";
const char ascii_value[] =
"\x25\x24\x0a\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\00\00\00"
"\00\00\00\00\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
"\x31\x32\00\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e"
"\00\00\00\00\x3f\00\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13"
"\x14\x15\x16\x17\00\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21"
"\x22";
/* generated by a script. do not touch. */
enum { armor_split_every = 48 };
char*
armor(const unsigned char* src, char* dest, size_t sz, size_t brstart, bool format) {
char* bufptr = dest;
unsigned char carry = 0;
for(size_t i = 0; i<sz; ++i) {
/* insert line breaks every so many chars */
*(bufptr++)=ascii_armory[src[i] % 64];
if (format) if ((brstart + (bufptr - dest)) % armor_split_every == 0)
*(bufptr++)='\n', *(bufptr++)=' ';
carry<<=2;
carry |= (src[i] / 64);
if (i && (i%3 == 0)) { /* :( */
*(bufptr++)=ascii_armory[carry];
carry = 0;
}
if (format) if ((brstart + (bufptr - dest)) % armor_split_every == 0)
*(bufptr++)='\n', *(bufptr++)=' ';
}
/* if(carry != 0) */ *(bufptr++)=ascii_armory[carry];
return bufptr;
}
char* disarmor(const unsigned char* src, char* dest, size_t sz) {
/* transforms ascii armor into binary. can transform in place. */
for(size_t i = 0; i<sz; i += 3) {
while (isws(src[i])) ++i;
const char* s = src + i;
unsigned char carry = ascii_value[s[2] - '-'],
b1 = ascii_value[s[0] - '-'] + (64 * ((carry & 12) >> 2)),
b2 = ascii_value[s[1] - '-'] + (64 * ( carry & 3));
*(dest++)=b1; *(dest++)=b2;
}
return dest;
}
struct pstr {
size_t len;
const char* str;
};
#define _p(x) {sizeof(x)-1,(x)}
struct encoding { struct pstr bin; struct pstr asc; const char* desc;};
enum doc {
doc_none,
doc_header,
doc_footer,
_doc_versions,
doc_version_1,
doc_version_2,
doc_version_3,
_doc_types,
doc_type_priv /* private key */,
doc_type_pub /* public key */,
doc_type_psk /* shared secret key */,
doc_type_asc /* arbitrary ascii-armored data */,
doc_type_sig /* separate signature */,
doc_type_signed /* document prefixed with signature */,
doc_type_master /* master key used to derive keys deterministically */,
_doc_n
};
const struct encoding docbytes[_doc_n] = { {0,NULL},
[doc_header] = {_p("\x26\xe9\x11\x1a"), _p("[" _self_name)},
[doc_footer] = {_p(""), _p("]")},
[doc_version_1] = {_p("\01"), _p("1-")},
[doc_version_2] = {_p("\02"), _p("2-")},
[doc_version_3] = {_p("\03"), _p("3-")},
[doc_type_priv] = {_p("\01"), _p("priv-"),"private key material"},
[doc_type_pub] = {_p("\02"), _p("pub-"), "public key material"},
[doc_type_psk] = {_p("\03"), _p("psk-"), "shared key material"},
[doc_type_asc] = {_p("\04"), _p("asc-"), "ascii-armored binary data"},
[doc_type_sig] = {_p("\05"), _p("sig-"), "separate signature"},
[doc_type_signed] = {_p("\06"), _p("signed-"), "data with signature"},
[doc_type_master] = {_p("\07"), _p("master-"), "master key"},
};
char*
compare_wsi(const char* s, char* d, size_t sz) {
for (size_t i=0; i<sz; ++i) {
while(isws(*s)) ++s;
while(isws(*d)) ++d;
if (*s != *d) return NULL;
++s; ++d;
}
return d;
}
bool
file_cmp(enum doc fld, char** s, bool binary) {
/* compares against a field and increments the callee's
* pointer appropriately if a match is found */
if (binary) {
if (memcmp(*s, docbytes[fld].bin.str, docbytes[fld].bin.len) == 0) {
*s += docbytes[fld].bin.len;
return true;
} else return false;
} else {
char* p = compare_wsi(docbytes[fld].asc.str, *s, docbytes[fld].asc.len);
if (p != NULL) {
*s = p;
return true;
} else return false;
}
}
struct file {
enum doc version, type;
union {
struct file_signed {
char* sig, *text;
} signtext;
struct file_key {
char* priv, *pub;
} key;
char* pub;
} data;
char* datastart;
size_t rawlen, datlen;
char raw [];
}* load_file(struct ctx c, const int fd) {
_say(debug, _t("reading in file from fd") _t(fd));
size_t run = 512; size_t len = 0;
struct file* buf = malloc(run + sizeof(struct file)); /* :( */
int e; while(e = read(fd, buf->raw + len, run - len)) {
if (e == -1) {
switch(errno) {
case EBADF: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("does not reference an open, readable file")); break;
case EFAULT: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("references a file outside your accessible address space")); break;
case EIO: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("could not be read due to an I/O error")); break;
case EISDIR: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("is a directory, not a readable file")); break;
default: _say(fatal, _t("file descriptor") _t((unsigned)fd) _t("could not be opened due to an unknown error - consult errno table for code") _t((unsigned)errno)); break;
}
goto err;
}
len += e;
if (len == run) {
run *= 2; buf = realloc(buf, run + sizeof(struct file));
} else if (len < run) continue;
}
buf -> rawlen = len;
buf -> type = doc_none;
return buf;
err: free(buf);
return NULL;
}
struct file* read_file(struct ctx c, const int fd) {
struct file* buf = load_file(c, fd);
bool binary; size_t len = buf -> rawlen;
char* cur = buf -> raw;
if (file_cmp(doc_header,&cur,true)) {
_report(debug, "parsing binary data");
binary = true;
} else if (file_cmp(doc_header,&cur,false)) {
_report(debug, "parsing ascii-armored data");
binary = false;
} else {
_say(fatal, _t("cannot parse data from fd") _t((unsigned)fd));
goto err;
}
/* write(2, cur, run - (cur - buf->raw)); */
for (enum doc i = _doc_versions + 1; i < _doc_types; ++ i) {
if (file_cmp(i, &cur, binary)) {
_say(debug, _t("file is format version") _t((unsigned) i - _doc_versions));
buf -> version = i; goto found_version;
}
} /* else */ {
_say(fatal, _t("unrecognized file format version; are you using the most recent release of " _self_name "?"));
goto err;
}
found_version:;
for (enum doc i = _doc_types + 1; i < _doc_n; ++ i) {
if (file_cmp(i, &cur, binary)) {
_say(debug, _t("file contains") _t(docbytes[i].desc));
buf -> type = i; goto found_type;
}
} /* else */ {
_say(fatal, _t("unrecognized file type; are you using the most recent release of " _self_name "?"));
goto err;
}
found_type: if (!binary) {
size_t s = (len - (cur - buf->raw)) - docbytes[doc_footer].asc.len;
_say(debug, _t("disarmoring") _t(s) _t("bytes of encoded text"));
char* end = disarmor(cur, buf -> raw, s);
buf -> datlen = end - cur;
} else {
buf -> datlen = (len - (cur - buf->raw)) - docbytes[doc_footer].bin.len;
}
buf -> datastart = cur;
return buf;
err: free(buf);
return NULL;
}
void
write_armored_v1(struct ctx c, const unsigned char* data, size_t sz) {
struct buffer o = {c.flags.outfd,0};
# define _wra_fld(f) buffer_write(&o, docbytes[doc_##f].asc.str,\
docbytes[doc_##f].asc.len);
_wra_fld(header);
_wra_fld(version_1);
_wra_fld(type_asc);
char buf[sz*2];
char* end = armor(data,buf,sz,13, isatty(c.flags.outfd));
buffer_write(&o, buf, end - buf);
_wra_fld(footer);
# undef _wra_fld
if (isatty(c.flags.outfd)) buffer_push(&o, '\n');
buffer_flush(&o);
}
void
write_privkey_v1(struct ctx c, const unsigned char* material) {
struct buffer o = {c.flags.outfd,0};
# define _wrp_fld(f) buffer_write(&o, \
(c.flags.ascii_armor ? docbytes[doc_##f].asc.str \
: docbytes[doc_##f].bin.str), \
(c.flags.ascii_armor ? docbytes[doc_##f].asc.len \
: docbytes[doc_##f].bin.len))
_wrp_fld(header);
_wrp_fld(version_1);
_wrp_fld(type_priv);
if (c.flags.ascii_armor) {
char buf[crypto_sign_SECRETKEYBYTES * 2];
char* end = armor(material,buf,crypto_sign_SECRETKEYBYTES,13,
isatty(c.flags.outfd));
buffer_write(&o, buf, end - buf);
} else {
buffer_write(&o, material, crypto_sign_SECRETKEYBYTES);
}
_wrp_fld(footer);
if (isatty(c.flags.outfd)) buffer_push(&o, '\n');
buffer_flush(&o);
}
typedef void (*write_privkey_t)(struct ctx, const unsigned char*);
typedef void (*write_armored_t)(struct ctx, const unsigned char*, size_t);
write_privkey_t write_privkey[] = {
[0] = write_privkey_v1, /* current version */
[1] = write_privkey_v1,
};
write_armored_t write_armored[] = {
[0] = write_armored_v1, /* current version */
[1] = write_armored_v1,
};
int
route(struct ctx c) {
if (sodium_init() < 0) {
_report(fatal, "could not initialize libsodium");
return 2;
}
switch (c.cmdv[0]) {
case atom_key: {
switch (c.cmdv[1]) {
case atom_gen: {
if (c.cmdc != 2) { usage(c,NULL); return 1; }
_report(info, "generating new private key");
if (c.flags.symcrypt == false) {
_say(warn, _t("not using symmetric encryption; key will be emitted in") _t("plain text"));
} else {
/* read passphrase from stdin/file */
}
/* libsodium is a bit weird. it uses different key types for
* encrypting and signing. we emit a signing (ed25519) key
* because it can be converted to an encryption key, but not
* vice-versa. libsodium also does not have any means to
* derive a public key from a private key, even though this
* is trivial, but instead stores the public key in the second
* half of the private key (despite emitting it AGAIN into the
* second parameter). so we generate a signing keypair, discard
* the returned public key, and save the "private key" (including
* its copy of the public key). this is extremely awkward and
* hopefully a better solution will be found in the fullness of
* time. */
unsigned char skey[crypto_sign_SECRETKEYBYTES];
{ unsigned char _[crypto_sign_PUBLICKEYBYTES];
crypto_sign_keypair(_,skey); }
(*write_privkey[c.flags.fmtv])(c, skey);
} break;
case atom_pub: {
if (c.cmdc != 2) { usage(c,NULL); return 1; }
struct file* skey = read_file(c, c.flags.infd);
if (skey == NULL) return 6;
free(skey);
} break;
case atom_wrap: {
if (c.cmdc != 2) { usage(c,NULL); return 1; }
struct file* key = read_file(c, c.flags.infd);
c.flags.ascii_armor = feature_on;
if (key -> type == doc_type_priv) {
(*write_privkey[c.flags.fmtv])(c, key -> datastart);
} else if (key -> type == doc_type_pub) {
/* (*write_pubkey[c.flags.fmtv])(c, key); */
} else {
_report(fatal, "file is not a " _self_name " key");
free(key); return 8;
}
free(key); return 0;
} break;
default: goto no_cmd;
}
} break;
case atom_wrap: {
if (c.cmdc != 1) { usage(c,NULL); return 1; }
struct file* data = load_file(c, c.flags.infd);
if (data == NULL) return 6;
(*write_armored[c.flags.fmtv])(c, data->raw, data->rawlen);
free(data); return 0;
} break;
case atom_unwrap: {
if (c.cmdc != 1) { usage(c,NULL); return 1; }
struct file* data = read_file(c, c.flags.infd);
if (data == NULL) return 6;
if (isatty(c.flags.outfd)) _say(warn, _t("printing binary values to tty"));
write(c.flags.outfd, data->datastart, data->datlen);
free(data); return 0;
} break;
case atom_help: {
if (c.cmdc == 1) usage(c,NULL);
else {
const struct cmd_spec* tree = cmd_specs;
for (size_t i = 1; i < c.cmdc; ++i) {
if (c.cmdv[i] < _atom_n) {
if (tree[c.cmdv[i]].sub != NULL) {
tree = tree[c.cmdv[i]].sub;
} else {
_say(fatal, _t("no help available for subcommand") _t(reconstitute(c.cmdv[i])));
return 3;
}
} else {
_say(fatal, _t("no help available;") _t(c.argv[c.cmdv[i] - _atom_n]) _t("is not a recognized command"));
return 4;
}
}
usage(c,tree);
}
return 0;
} break;
default: goto no_cmd;
}
return 0;
no_cmd: _say(fatal, _t("no such command"));
usage(c,NULL);
return 1;
}
int
open_file(struct ctx c, const char* path, int flags, word* retfd) {
int fd = open(path,flags,0600);
if (fd == -1) {
switch (errno) {
case EACCES: _say(fatal, _t("file") _t(path) _t("could not be opened because you do not have permission to read it")); break;
case EFAULT: _say(fatal, _t("file") _t(path) _t("could not be opened because the path is outside your accessible address space")); break;
case EISDIR: _say(fatal, _t("file") _t(path) _t("could not be opened because it is a directory, not a regular file")); break;
case ELOOP: _say(fatal, _t("file") _t(path) _t("could not be opened because too many symbolic links were encountered")); break;
case EMFILE: _say(fatal, _t("file") _t(path) _t("could not be opened because the per-process file limit is too low")); break;
case ENOENT: _say(fatal, _t("file") _t(path) _t("could not be opened because it does not exist")); break;
case ENOMEM: _say(fatal, _t("file") _t(path) _t("could not be opened, either because not enough memory remains to make use of a FIFO or because the kernel is running out of memory (!)")); break;
case ENOTDIR: _say(fatal, _t("file") _t(path) _t("could not be opened because a file was specified in place of a directory in the path")); break;
case ENXIO: _say(fatal, _t("file") _t(path) _t("could not be opened because it is not a regular file or FIFO")); break;
case EPERM: _say(fatal, _t("file") _t(path) _t("could not be opened because it is sealed against access")); break;
case EROFS: _say(fatal, _t("file") _t(path) _t("could not be opened for writing because it is located on a read-only filesystem")); break;
default: _say(fatal, _t("file") _t(path) _t("could not be opened for unclear reasons - consult errno entry for code") _t((unsigned)errno)); break;
}
return errno;
}
*retfd = fd; return 0;
}
int
main(unsigned int argc, const char** argv) {
atom cmdlist[argc];
const char* paramlist[argc];
size_t paramcount = 0;
struct ctx c = {
.argv = paramlist,
.cmdc = 0,
.cmdv = cmdlist,
.bin = (argc > 0 ? argv[0] : NULL),
.flags = {
.ascii_armor = feature_auto,
.color = feature_auto,
.noise = noise_fatal,
.infile = NULL, .infd = 0,
.outfile = NULL, .outfd = 1,
.pwfile = NULL, .pwfd = 0,
.pw_force_tty = false,
.sigfile = NULL, .sigfd = 0,
/* if sigfd == infd, extract sig from input stream */
.fd = false,
.symcrypt = false,
.fmtv = 0, /* 0 = native version */
},
};
if (argc == 0) {
_report(fatal, "invoked incorrectly or your environment is not sane; the 0th value of the argument array should be the name of or path to the program you are attempting to execute");
return -1;
}
struct flag* paramstack[argc];
size_t stackptr = 0;
enum {
reading_params,
reading_commands,
reading_flags
} reading_mode = reading_flags;
_say(debug, _t("parsing command line") _t(argv));
for (size_t i = 1; i < argc; ++i) {
const char* const p = argv[i];
if (reading_mode >= reading_flags && p[0] == '-') { /* flag */
bool longflag;
if (p[1] == '-') {
if (p[2] == 0) {
/* "--" has been passed; all further arguments must be
* interpreted as parameters or commands, not flags */
reading_mode = reading_commands;
continue;
} else if (p[2] == '-' && p[3] == 0) {
/* "---" has been passed; all further arguments must be
* interpreted as parameters, not flags or commands */
reading_mode = reading_params;
continue;
} else longflag = true;
} else longflag = false;
_say(debug, _t("parsing flag") _t(p));
for (size_t o = 1; p[o] != 0; ++o) {
atom option = atom_none;
if (longflag) option = atomize(p+2);
else { /* short flag */
_say(debug, _t("parsing short flag") _t(p[o]));
for (size_t j = 0; j < sz(flags); ++ j)
if (flags[j].kind != flag_none && flags[j].shortname == p[o]) {
option = (atom)j; break;
}
}
if (option == atom_none) {
if (!longflag && p[o] == '-') {
if (p[o+1] == '-' && p[o+2] == 0) {
/* shorthand for --- */
reading_mode = reading_params;
continue;
} else if (p[o+1] == 0) {
/* shorthand for -- */
reading_mode = reading_commands;
continue;
}
}
char ftext[3] = {'-',p[o],0};
_say(fatal, _t("flag") _t(longflag ? p+2 : ftext) _t("not meaningful"));
goto parse_fail;
}
handle_flag:;
struct flag* f = flags + option;
void* fld = ((unsigned char*)&c) + (unsigned char)(f -> offset);
if (f -> kind > _flag_param) {
if (f->kind>=flag_value) {
*((unsigned*)fld) = f->kind - flag_value;
} else paramstack[stackptr++] = f;
} else if (f -> kind > _flag_noparam) {
switch(f -> kind) {
case flag_on: *((bool*)fld) = true; break;
case flag_off: *((bool*)fld) = false; break;
case flag_switch: *((bool*)fld) = !*((bool*)fld); break;
/* in order to avoid creepy behavior in pathological cases,
* we enforce saturation arithmetic for the increment/decrement
* flag types */
case flag_inc: {
unsigned* v = fld;
if (*v < UINT_MAX) ++*v;
} break;
case flag_dec: {
unsigned* v = fld;
if (*v > 0) --*v;
} break;
case flag_sdec: {
signed* v = fld;
if (*v > INT_MIN) --*v;
} break;
}
}
if (p[1] == '-') break;
}
continue;
}
if (stackptr > 0) {
struct flag* f = paramstack[--stackptr];
_say(debug, _t("handling argument for flag") _t(reconstitute(f - flags)));
void* fld = ((unsigned char*)&c) + (unsigned char)(f -> offset);
switch(f -> kind) {
case flag_str: *((const char**)fld) = p; break;
case flag_uint: {
iaia_error_type e = int_of_str(10, p, (unsigned long long*)fld);
switch (e) {
case iaia_e_ok: continue;
case iaia_e_domain:
_say(fatal, _t("argument") _t(p) _t("outside domain for base"));
}
return 5;
} break;
case flag_atom: case flag_raw_atom: {
atom a = atomize(p);
if (a == atom_none || a < f -> opts.first || a > f -> opts.last) {
_say(fatal, _t("invalid argument") _t(p) _t("for flag")
_t(reconstitute(f - flags)));
goto parse_fail;
}
*((atom*)fld) = f->kind == flag_raw_atom ? a : a - f -> opts.first;
} break;
}
continue;
}
if (reading_mode >= reading_commands) {
if (p[0] == '=') { /* parameter even if atomizable */
paramlist[paramcount] = p + 1;
cmdlist[c.cmdc] = _atom_n + paramcount;
++c.cmdc; ++ paramcount;
continue;
} else {
atom a = atomize(p);
if (a != atom_none) {
cmdlist[c.cmdc] = a;
++ c.cmdc;
continue;
}
}
}
/* only parameters remain to be read */
paramlist[paramcount] = p;
cmdlist[c.cmdc] = _atom_n + paramcount;
++ c.cmdc; ++ paramcount;
}
parse_done: _say(debug, _t("stored") _t(c.cmdc) _t("arguments and") _t(paramcount) _t("parameters"));
if (c.flags.noise <= noise_debug) for (size_t i = 0; i<c.cmdc; ++i) {
if (cmdlist[i] < _atom_n) {
dprintf(2, "\t%llu. atom %u (normal form “%s”)\n", i, cmdlist[i], reconstitute(cmdlist[i]));
} else {
dprintf(2,"\t%llu. parameter “%s”\n", i, paramlist[cmdlist[i] - _atom_n]);
}
}
if (c.flags.fmtv >= sz(write_privkey)) {
_say(fatal, _t("data format version") _t(c.flags.fmtv) _t("is not known. the highest available version is") _t(sz(write_privkey) - 1) _t("- are you sure you're using the latest release of " _self_name "?"));
return -2;
}
int err;
if (c.flags.pwfile != NULL) {
err = open_file(c,c.flags.pwfile, O_RDONLY, &c.flags.pwfd);
if(err) return err;
}
if (c.flags.infile != NULL) {
err = open_file(c,c.flags.infile, O_RDONLY, &c.flags.infd);
if(err) return err;
}
if (c.flags.outfile != NULL) {
err = open_file(c,c.flags.outfile, O_WRONLY | O_TRUNC | O_CREAT, &c.flags.outfd);
if(err) return err;
}
if (c.flags.sigfile != NULL) {
err = open_file(c,c.flags.sigfile, O_RDONLY, &c.flags.sigfd);
if(err) return err;
}
if (c.flags.color == feature_auto)
c.flags.color = isatty(2);
if (c.flags.ascii_armor == feature_auto)
c.flags.ascii_armor = isatty(c.flags.outfd);
/* this is hateful but i can't figure out a better way */
if (c.flags.pw_force_tty && !isatty(c.flags.pwfd)) {
_say(debug, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as it is not a tty and -W was passed;") _t("/dev/tty") _t("will be used instead"));
} else if (c.flags.pwfd == c.flags.infd) {
_say(debug, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as input data is already being read from that descriptor;") _t("/dev/tty") _t("will be used instead"));
} else if (c.flags.pwfd == c.flags.sigfd) {
_say(warn, _t("passphrases cannot be read from fd") _t(c.flags.pwfd) _t("as the signature is already being read from that descriptor;") _t("/dev/tty") _t("will be used instead"));
} else goto skip_pwfd_change;
pwfd_change: {
err = open_file(c,"/dev/tty", O_WRONLY, &c.flags.pwfd);
if(err) return err;
}
skip_pwfd_change:
_say(debug, _t("input fd =") _t(c.flags.infd));
_say(debug, _t("output fd =") _t(c.flags.outfd));
_say(debug, _t("pw fd =") _t(c.flags.pwfd));
if (c.cmdc > 0) return route(c);
parse_fail: if (c.flags.noise < noise_silent) usage(c,NULL);
return 1;
}