mirror of https://github.com/skeeto/enchive.git
Add a key agent.
parent
a9dad33fef
commit
67b82606c6
8
Makefile
8
Makefile
|
@ -9,10 +9,10 @@ headers = config.h docs.h chacha.h sha256.h optparse.h
|
|||
|
||||
enchive: $(objects)
|
||||
$(CC) $(LDFLAGS) -o $@ $(objects) $(LDLIBS)
|
||||
enchive.o: enchive.c docs.h
|
||||
chacha.o: chacha.c
|
||||
curve25519-donna.o: curve25519-donna.c
|
||||
sha256.o: sha256.c
|
||||
enchive.o: enchive.c config.h docs.h
|
||||
chacha.o: chacha.c config.h
|
||||
curve25519-donna.o: curve25519-donna.c config.h
|
||||
sha256.o: sha256.c config.h
|
||||
|
||||
enchive-cli.c: $(sources) $(headers)
|
||||
cat $(headers) $(sources) | sed -r 's@^(#include +".+)@/* \1 */@g' > $@
|
||||
|
|
19
README.md
19
README.md
|
@ -59,6 +59,20 @@ option with `keygen`. It will load the secret key as if it were going
|
|||
to "extract" an archive, then write it back out with the new options.
|
||||
This mode will also regenerate the public key file.
|
||||
|
||||
Enchive has a built-in protection key agent that keeps the protection
|
||||
key in memory for a configurable period of time (default: 15 minutes)
|
||||
after a protection passphrase has been read. This allows any files to
|
||||
be decrypted inside this window with only a single passphrase prompt.
|
||||
Use the `--agent` (`-a`) global option to enable it. If it's enabled
|
||||
by default, use `--no-agent` to turn it off.
|
||||
|
||||
$ enchive --agent extract file.enchive
|
||||
|
||||
Unlike gpg-agent and ssh-agent, this agent need not be started ahead
|
||||
of time. It is started on demand, shuts down on timeout, and does not
|
||||
coordinate with environment variables. One agent is created per unique
|
||||
secret key file. This feature requires a unix-like system.
|
||||
|
||||
## Notes
|
||||
|
||||
There's no effort at error recovery. It bails out on early on the
|
||||
|
@ -87,8 +101,3 @@ The process for decrypting a file:
|
|||
4. Initialize ChaCha20 with the shared secret as the key.
|
||||
5. Decrypt the ciphertext using ChaCha20.
|
||||
6. Verify `sha256(key + sha256(plaintext))`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Decrypt multiple files in a short period: some kind of key agent?
|
||||
* Improve key generation.
|
||||
|
|
16
config.h
16
config.h
|
@ -23,6 +23,22 @@
|
|||
# endif
|
||||
#endif
|
||||
|
||||
#ifndef ENCHIVE_OPTION_AGENT
|
||||
# if defined(__unix__) || defined(__APPLE__)
|
||||
# define ENCHIVE_OPTION_AGENT 1
|
||||
# else
|
||||
# define ENCHIVE_OPTION_AGENT 0
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifndef ENCHIVE_AGENT_TIMEOUT
|
||||
# define ENCHIVE_AGENT_TIMEOUT 900 /* 15 minutes */
|
||||
#endif
|
||||
|
||||
#ifndef ENCHIVE_AGENT_DEFAULT_ENABLED
|
||||
# define ENCHIVE_AGENT_DEFAULT_ENABLED 0
|
||||
#endif
|
||||
|
||||
/* Required for correct builds */
|
||||
|
||||
#ifndef _POSIX_SOURCE
|
||||
|
|
9
docs.h
9
docs.h
|
@ -1,5 +1,8 @@
|
|||
static const char *docs_usage[] = {
|
||||
"usage enchive [-p|--pubkey <file>] [-s|--seckey <file>]",
|
||||
#if ENCHIVE_OPTION_AGENT
|
||||
" [-a|--agent[=seconds]] [--no-agent]",
|
||||
#endif
|
||||
#if ENCHIVE_OPTION_RANDOM_DEVICE
|
||||
" [--random-device <file>]",
|
||||
#endif
|
||||
|
@ -11,8 +14,14 @@ static const char *docs_usage[] = {
|
|||
" extract extract from an archive using the secret key",
|
||||
" help get help on a specific topic",
|
||||
"",
|
||||
#if ENCHIVE_OPTION_AGENT
|
||||
" --agent[=seconds] run the key agent after reading a passphrase ["
|
||||
STR(ENCHIVE_AGENT_TIMEOUT) "]",
|
||||
#endif
|
||||
#if ENCHIVE_OPTION_RANDOM_DEVICE
|
||||
" --random-device <file> device for secure entropy ["
|
||||
STR(ENCHIVE_RANDOM_DEVICE) "]",
|
||||
#endif
|
||||
" --pubkey <file>, -p set the public key file [~/.enchive.pub]",
|
||||
" --seckey <file>, -s set the secret key file [~/.enchive.sec]",
|
||||
"",
|
||||
|
|
205
enchive.c
205
enchive.c
|
@ -19,6 +19,12 @@ int curve25519_donna(u8 *p, const u8 *s, const u8 *b);
|
|||
static char *global_pubkey = 0;
|
||||
static char *global_seckey = 0;
|
||||
|
||||
#if ENCHIVE_AGENT_DEFAULT_ENABLED
|
||||
static int global_agent_timeout = ENCHIVE_AGENT_TIMEOUT;
|
||||
#else
|
||||
static int global_agent_timeout = 0;
|
||||
#endif
|
||||
|
||||
static struct {
|
||||
char *name;
|
||||
FILE *file;
|
||||
|
@ -55,11 +61,151 @@ fatal(const char *fmt, ...)
|
|||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void
|
||||
warning(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
fprintf(stderr, "warning: ");
|
||||
vfprintf(stderr, fmt, ap);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
#if ENCHIVE_OPTION_AGENT
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
static int
|
||||
agent_addr(struct sockaddr_un *addr, const u8 *iv)
|
||||
{
|
||||
char *tmpdir;
|
||||
addr->sun_family = AF_UNIX;
|
||||
if (!(tmpdir = getenv("TMPDIR")))
|
||||
tmpdir = "/tmp";
|
||||
if (strlen(tmpdir) + 40 + 1 > sizeof(addr->sun_path)) {
|
||||
warning("$TMPDIR too long -- %s", tmpdir);
|
||||
return 0;
|
||||
} else {
|
||||
sprintf(addr->sun_path, "%s/%02x%02x%02x%02x%02x%02x%02x%02x", tmpdir,
|
||||
iv[0], iv[1], iv[2], iv[3], iv[4], iv[5], iv[6], iv[7]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the protection key from a unix socket identified by its IV.
|
||||
*/
|
||||
static int
|
||||
agent_read(u8 *key, const u8 *iv)
|
||||
{
|
||||
int success;
|
||||
struct sockaddr_un addr;
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (!agent_addr(&addr, iv)) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
success = read(fd, key, 32) == 32;
|
||||
close(fd);
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the protection key on a unix socket identified by its IV.
|
||||
*/
|
||||
static int
|
||||
agent_run(const u8 *key, const u8 *iv)
|
||||
{
|
||||
struct pollfd pfd = {-1, POLLIN, 0};
|
||||
struct sockaddr_un addr;
|
||||
pid_t pid;
|
||||
|
||||
pfd.fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (pfd.fd == -1) {
|
||||
warning("could not create agent socket");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!agent_addr(&addr, iv))
|
||||
return 0;
|
||||
|
||||
pid = fork();
|
||||
if (pid == -1) {
|
||||
warning("could not fork() agent -- %s", strerror(errno));
|
||||
return 0;
|
||||
} else if (pid != 0) {
|
||||
return 1;
|
||||
}
|
||||
close(0);
|
||||
close(1);
|
||||
|
||||
umask(~(S_IRUSR | S_IWUSR));
|
||||
if (bind(pfd.fd, (struct sockaddr *)&addr, sizeof(addr))) {
|
||||
warning("could not bind agent socket %s -- %s",
|
||||
addr.sun_path, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (listen(pfd.fd, SOMAXCONN)) {
|
||||
if (errno != EADDRINUSE)
|
||||
fatal("could not listen on agent socket -- %s", strerror(errno));
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
close(2);
|
||||
for (;;) {
|
||||
int cfd;
|
||||
int r = poll(&pfd, 1, global_agent_timeout * 1000);
|
||||
if (r < 0) {
|
||||
unlink(addr.sun_path);
|
||||
fatal("agent poll failed -- %s", strerror(errno));
|
||||
}
|
||||
if (r == 0) {
|
||||
unlink(addr.sun_path);
|
||||
fputs("info: agent timeout\n", stderr);
|
||||
close(pfd.fd);
|
||||
break;
|
||||
}
|
||||
cfd = accept(pfd.fd, 0, 0);
|
||||
if (cfd != -1) {
|
||||
if (write(cfd, key, 32) != 32)
|
||||
warning("agent write failed");
|
||||
close(cfd);
|
||||
}
|
||||
}
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
#else
|
||||
static int
|
||||
agent_read(const u8 *key, const u8 *id)
|
||||
{
|
||||
(void)key;
|
||||
(void)id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
agent_run(const u8 *key, const u8 *id)
|
||||
{
|
||||
(void)key;
|
||||
(void)id;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void
|
||||
get_passphrase_dumb(char *buf, size_t len, char *prompt)
|
||||
{
|
||||
size_t passlen;
|
||||
fprintf(stderr, "warning: reading passphrase from stdin with echo\n");
|
||||
warning("reading passphrase from stdin with echo");
|
||||
fputs(prompt, stderr);
|
||||
fflush(stderr);
|
||||
if (!fgets(buf, len, stdin))
|
||||
|
@ -447,8 +593,8 @@ load_seckey(char *file, u8 *seckey)
|
|||
SHA256_CTX sha[1];
|
||||
u8 buf[8 + 4 + 20 + 32];
|
||||
u8 empty[8] = {0};
|
||||
u8 keyhash[SHA256_BLOCK_SIZE];
|
||||
u8 key[32];
|
||||
u8 protect_hash[SHA256_BLOCK_SIZE];
|
||||
u8 protect[32];
|
||||
|
||||
secfile = fopen(file, "rb");
|
||||
if (!secfile)
|
||||
|
@ -458,23 +604,28 @@ load_seckey(char *file, u8 *seckey)
|
|||
fclose(secfile);
|
||||
|
||||
if (memcmp(buf, empty, sizeof(empty)) != 0) {
|
||||
char pass[PASSPHRASE_MAX];
|
||||
unsigned long iterations =
|
||||
((unsigned long)buf[8] << 24) |
|
||||
((unsigned long)buf[9] << 16) |
|
||||
((unsigned long)buf[10] << 8) |
|
||||
((unsigned long)buf[11] << 0);
|
||||
get_passphrase(pass, sizeof(pass), "passphrase: ");
|
||||
int agent_success = agent_read(protect, buf);
|
||||
if (!agent_success) {
|
||||
char pass[PASSPHRASE_MAX];
|
||||
unsigned long iterations =
|
||||
((unsigned long)buf[8] << 24) |
|
||||
((unsigned long)buf[9] << 16) |
|
||||
((unsigned long)buf[10] << 8) |
|
||||
((unsigned long)buf[11] << 0);
|
||||
get_passphrase(pass, sizeof(pass), "passphrase: ");
|
||||
|
||||
key_derive(pass, key, iterations, buf);
|
||||
key_derive(pass, protect, iterations, buf);
|
||||
|
||||
sha256_init(sha);
|
||||
sha256_update(sha, key, 32);
|
||||
sha256_final(sha, keyhash);
|
||||
if (memcmp(keyhash, buf + 12, 20) != 0)
|
||||
fatal("wrong passphrase");
|
||||
sha256_init(sha);
|
||||
sha256_update(sha, protect, 32);
|
||||
sha256_final(sha, protect_hash);
|
||||
if (memcmp(protect_hash, buf + 12, 20) != 0)
|
||||
fatal("wrong passphrase");
|
||||
}
|
||||
if (!agent_success && global_agent_timeout)
|
||||
agent_run(protect, buf);
|
||||
|
||||
chacha_keysetup(cha, key, 256);
|
||||
chacha_keysetup(cha, protect, 256);
|
||||
chacha_ivsetup(cha, buf);
|
||||
chacha_encrypt_bytes(cha, buf + 32, seckey, 32);
|
||||
} else {
|
||||
|
@ -853,6 +1004,10 @@ int
|
|||
main(int argc, char **argv)
|
||||
{
|
||||
static const struct optparse_long global[] = {
|
||||
#if ENCHIVE_OPTION_AGENT
|
||||
{"agent", 'a', OPTPARSE_OPTIONAL},
|
||||
{"no-agent", 'A', OPTPARSE_NONE},
|
||||
#endif
|
||||
#if ENCHIVE_OPTION_RANDOM_DEVICE
|
||||
{"random-device", 'r', OPTPARSE_REQUIRED},
|
||||
#endif
|
||||
|
@ -870,6 +1025,22 @@ main(int argc, char **argv)
|
|||
|
||||
while ((option = optparse_long(options, global, 0)) != -1) {
|
||||
switch (option) {
|
||||
#if ENCHIVE_OPTION_AGENT
|
||||
case 'a':
|
||||
if (options->optarg) {
|
||||
char *arg = options->optarg;
|
||||
char *endptr;
|
||||
errno = 0;
|
||||
global_agent_timeout = strtol(arg, &endptr, 10);
|
||||
if (*endptr || errno)
|
||||
fatal("invalid --agent argument -- %s", arg);
|
||||
} else
|
||||
global_agent_timeout = ENCHIVE_AGENT_TIMEOUT;
|
||||
break;
|
||||
case 'A':
|
||||
global_agent_timeout = 0;
|
||||
break;
|
||||
#endif
|
||||
#if ENCHIVE_OPTION_RANDOM_DEVICE
|
||||
case 'r':
|
||||
global_random_device = options->optarg;
|
||||
|
|
Loading…
Reference in New Issue