/* timelock -- timed release encryption utility
Copyright (C) 2017 Arun I <arunisaac@systemreboot.net>
This file is part of timelock.
timelock is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
timelock is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with timelock. If not, see <http://www.gnu.org/licenses/>. */
// TODO: Implement in-place
// TODO: Strengthen randomness
#include <stdio.h>
#include <math.h>
#include <gcrypt.h>
#include <inttypes.h>
#include <time.h>
#include <argp.h>
#include <error.h>
#include <boost/preprocessor/seq.hpp>
#include "config.h"
#define KEY_SIZE 32
#define NONCE_SIZE 8
#define READ_BUFFER_SIZE 1024
#define PRIME_BITS 1024
#define KEY_BITS (KEY_SIZE * 8)
#define SQUARINGS_PER_ITERATION 1023
#define TEST_ITERATIONS 10000
#define DEFAULT_TIME 600
// Magic bytes
#define MAGIC_BYTES "timelock"
// Output format version number
#define OUTPUT_VERSION 0
// Chunk types in output
enum chunk_type {END_OF_CHUNKS, CHUNK_N, CHUNK_A, CHUNK_TIMES, CHUNK_CKEY};
void print_parameter (enum chunk_type, gcry_mpi_t);
// Cipher algorithms
enum cipher_algo {SALSA20};
#define _ALLOCATE_MPI(r, _, var) gcry_mpi_t var = gcry_mpi_new(1);
#define ALLOCATE_MPI(mpi) BOOST_PP_SEQ_FOR_EACH(_ALLOCATE_MPI, _, mpi)
#define _ALLOCATE_SECURE_MPI(r, _, var) gcry_mpi_t var = gcry_mpi_snew(1);
#define ALLOCATE_SECURE_MPI(mpi) BOOST_PP_SEQ_FOR_EACH(_ALLOCATE_SECURE_MPI, _, mpi)
#define _CLEANUP(r, _, var) _Generic((var), gcry_mpi_t: gcry_mpi_release, \
gcry_cipher_hd_t: gcry_cipher_close, \
default: free)(var);
#define CLEANUP(vars) BOOST_PP_SEQ_FOR_EACH(_CLEANUP, _, vars)
#define WITH_MPI(mpi, body) { ALLOCATE_MPI(mpi); body; CLEANUP(mpi); }
#define TRY_CATCH_GCRYPT(try, message, handle) if (handle_gcrypt_error(try, message)) { handle; exit(1); }
#define SAFE_FREAD(buffer, len, handle) \
if (fread(buffer, 1, len, stdin) < len) \
{ handle; error(1, 0, "Incomplete input\n"); }
#define SAFE_FWRITE(buffer, len, handle) \
if (fwrite(buffer, 1, len, stdout) < len) \
{ handle; error(1, 0, "Error writing to standard output"); }
#define SAFE_MALLOC(ptr, size, handle) if (!(ptr = malloc(size))) { handle; perror(__func__); exit(1); }
#define SAFE_REALLOC(ptr, size, handle) if (!(ptr = realloc(ptr, size))) { handle; perror(__func__); exit(1); }
// gcrypt helper functions
gcry_mpi_t mpi_set_ulli (gcry_mpi_t, unsigned long long);
unsigned long long mpi_to_ulli (gcry_mpi_t w);
gcry_error_t generate_prime (gcry_mpi_t*);
int handle_gcrypt_error (gcry_error_t, const char*);
gcry_error_t open_cipher (gcry_cipher_hd_t* handle, const void *key, const unsigned char *nonce);
size_t read_stdin (unsigned char**);
void read_parameters (gcry_mpi_t* n, gcry_mpi_t* a, unsigned long long* times, gcry_mpi_t* ckey);
void lock (unsigned long);
void unlock (int);
void info ();
int create_puzzle (gcry_mpi_t, unsigned long long, gcry_mpi_t, gcry_mpi_t, gcry_mpi_t);
void solve_puzzle (gcry_mpi_t, gcry_mpi_t, gcry_mpi_t, unsigned long long, gcry_mpi_t, int);
double squarings_per_second ();
const char* argp_program_version = PACKAGE_STRING;
const char* argp_program_bug_address = PACKAGE_BUGREPORT;
static char doc[] = "timelock -- encrypt files which can be opened only in the future";
static char args_doc[] = "[lock|unlock]";
static struct argp_option options[] = {
{"time", 't', "SECONDS", 0, "Time (in seconds) for which the input should be locked", 0},
{"quiet", 'q', 0, 0, "Don't produce any output", 0},
{"help", 'h', 0, 0, "Give this help list", 0},
{"usage", 1, 0, OPTION_HIDDEN, "Give a short usage message", 0},
{ 0 }
};
struct arguments {
enum {LOCK, UNLOCK, INFO} command;
unsigned long seconds;
int quiet;
char* input_file;
};
static error_t parse_opt (int key, char *arg, struct argp_state *state)
{
struct arguments *arguments = state->input;
switch (key) {
case 't':
sscanf(arg, "%lu", &arguments->seconds);
break;
case 'i':
arguments->input_file = arg;
break;
case 'q':
arguments->quiet = 1;
break;
case 'h':
argp_state_help(state, stderr, ARGP_HELP_STD_HELP);
break;
case 1:
argp_state_help(state, stderr, ARGP_HELP_STD_USAGE);
break;
case ARGP_KEY_ARG:
if (state->arg_num >= 1)
argp_usage(state);
if (!strcmp(arg, "lock"))
arguments->command = LOCK;
else if (!strcmp(arg, "unlock"))
arguments->command = UNLOCK;
else {
fprintf(stderr, "%s: Invalid command\n", arg);
argp_usage(state);
exit(1);
}
break;
case ARGP_KEY_END:
if (((arguments->command == UNLOCK) || (arguments->command == INFO)) && (arguments->seconds != 0)) {
fprintf(stderr, "-t|--times: Option only valid while locking\n");
argp_usage(state);
exit(1);
}
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static struct argp argp = {options, parse_opt, args_doc, doc, NULL, NULL, NULL};
int main (int argc, char *argv[])
{
struct arguments arguments;
arguments.command = INFO;
arguments.seconds = 0;
arguments.input_file = "-";
arguments.quiet = 0;
argp_parse(&argp, argc, argv, ARGP_NO_HELP, 0, &arguments);
if (!gcry_check_version (GCRYPT_VERSION))
error(2, 0, "libgcrypt version mismatch\n");
gcry_control(GCRYCTL_SUSPEND_SECMEM_WARN);
gcry_control(GCRYCTL_INIT_SECMEM, 16384, 0);
gcry_control(GCRYCTL_RESUME_SECMEM_WARN);
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
if (arguments.command == LOCK)
lock(arguments.seconds == 0 ? DEFAULT_TIME : arguments.seconds);
else if (arguments.command == UNLOCK) unlock(arguments.quiet != 1);
else if (arguments.command == INFO) info();
return 0;
}
size_t read_stdin (unsigned char **buffer)
{
// Allocate memory for read buffer
SAFE_MALLOC(*buffer, READ_BUFFER_SIZE, return 0);
size_t bytes_read = 0;
// Read READ_BUFFER_SIZE bytes at a time
for (size_t size=READ_BUFFER_SIZE; ; size += READ_BUFFER_SIZE) {
size_t len = fread(*buffer + size - READ_BUFFER_SIZE, 1, READ_BUFFER_SIZE, stdin);
bytes_read += len;
if (len < READ_BUFFER_SIZE)
if (ferror(stdin)) {
perror(__func__);
CLEANUP((*buffer));
return 0;
}
else return bytes_read;
else SAFE_REALLOC(*buffer, size + READ_BUFFER_SIZE, return 0);
}
}
gcry_error_t generate_prime (gcry_mpi_t* p)
{
return gcry_prime_generate(p, PRIME_BITS, 0, NULL, NULL, NULL, GCRY_WEAK_RANDOM, 0);
}
void print_parameter (enum chunk_type type, gcry_mpi_t param)
{
unsigned char* buffer;
size_t nwritten;
gcry_mpi_aprint(GCRYMPI_FMT_PGP, &buffer, &nwritten, param);
SAFE_FWRITE(&type, 1, );
SAFE_FWRITE(buffer, nwritten, );
CLEANUP((buffer));
}
gcry_mpi_t mpi_set_ulli (gcry_mpi_t w, unsigned long long u)
{
unsigned long long u_be = htobe64(u);
gcry_mpi_scan(&w, GCRYMPI_FMT_USG, &u_be, sizeof(u_be), NULL);
return w;
}
unsigned long long mpi_to_ulli (gcry_mpi_t w)
{
unsigned long long u;
size_t nwritten;
gcry_mpi_print(GCRYMPI_FMT_USG, (unsigned char*)&u, sizeof(u), &nwritten, w);
u = be64toh(u) >> (sizeof(u) - nwritten)*8;
return u;
}
void lock (const unsigned long seconds)
{
unsigned char key_bytes[KEY_SIZE];
WITH_MPI
((n)(a)(times_mpi)(ckey)(key_mpi),
unsigned long long times = seconds * squarings_per_second();
// Create and output puzzle
if (create_puzzle(key_mpi, times, n, a, ckey)) {
CLEANUP((n)(a)(ckey)(key_mpi));
exit(1);
}
printf("%s%c", MAGIC_BYTES, OUTPUT_VERSION);
print_parameter(CHUNK_N, n);
print_parameter(CHUNK_A, a);
times_mpi = mpi_set_ulli(times_mpi, times);
print_parameter(CHUNK_TIMES, times_mpi);
print_parameter(CHUNK_CKEY, ckey);
printf("%c", END_OF_CHUNKS);
TRY_CATCH_GCRYPT(gcry_mpi_print(GCRYMPI_FMT_USG, key_bytes, KEY_SIZE, NULL, key_mpi),
"Convert MPI key to bytes", CLEANUP((n)(a)(times_mpi)(ckey)(key_mpi))););
// Read plaintext of message
unsigned char* buffer;
size_t len = read_stdin(&buffer);
if (!len) exit(1);
// Generate and print nonce
unsigned char nonce[NONCE_SIZE];
gcry_create_nonce(nonce, NONCE_SIZE);
printf("%c", SALSA20);
SAFE_FWRITE(nonce, NONCE_SIZE, CLEANUP((buffer)));
// Write ciphertext of message
gcry_cipher_hd_t handle;
TRY_CATCH_GCRYPT(open_cipher(&handle, key_bytes, nonce),
"Open cipher handle", CLEANUP((handle)(buffer)));
TRY_CATCH_GCRYPT(gcry_cipher_encrypt(handle, buffer, len, NULL, 0),
"Encrypt message", CLEANUP((handle)(buffer)));
SAFE_FWRITE(buffer, len, CLEANUP((handle)(buffer)));
CLEANUP((handle)(buffer));
}
double squarings_per_second ()
{
double squarings = 0;
WITH_MPI
((n)(a)(ckey)(key),
if (create_puzzle(key, TEST_ITERATIONS, n, a, ckey)) {
CLEANUP((n)(a)(ckey)(key));
exit(1);
}
clock_t start = clock();
solve_puzzle(key, n, a, TEST_ITERATIONS, ckey, 0);
clock_t stop = clock();
squarings = CLOCKS_PER_SEC * TEST_ITERATIONS / ((double)(stop - start)););
return squarings;
}
void unlock (int progress)
{
unsigned char key_bytes[KEY_SIZE];
unsigned long long times;
WITH_MPI
((n)(a)(ckey)(key),
read_parameters(&n, &a, ×, &ckey);
solve_puzzle(key, n, a, times, ckey, progress);
TRY_CATCH_GCRYPT(gcry_mpi_print(GCRYMPI_FMT_USG, key_bytes, KEY_SIZE, NULL, key),
"Convert MPI key to bytes", CLEANUP((n)(a)(ckey)(key))););
// Read nonce
enum cipher_algo cipher;
SAFE_FREAD(&cipher, 1, );
if ((uint8_t)cipher != SALSA20) {
error(1, 0, "Cipher algorithm not supported\n");
exit(1);
}
unsigned char nonce[NONCE_SIZE];
SAFE_FREAD(nonce, NONCE_SIZE, );
// Open cipher handle
gcry_cipher_hd_t handle;
TRY_CATCH_GCRYPT(open_cipher(&handle, key_bytes, nonce),
"Open cipher handle", CLEANUP((handle)));
// Read stdin
unsigned char* buffer;
size_t len = read_stdin(&buffer);
if (!len) CLEANUP((handle));
// Decrypt
TRY_CATCH_GCRYPT(gcry_cipher_decrypt(handle, buffer, len, NULL, 0),
"Decrypt message", CLEANUP((handle)(buffer)));
SAFE_FWRITE(buffer, len, );
CLEANUP((handle)(buffer));
}
int create_puzzle (const gcry_mpi_t key, const unsigned long long _times, gcry_mpi_t n, gcry_mpi_t a, gcry_mpi_t ckey)
{
ALLOCATE_SECURE_MPI((p)(q)(phi)(two)(times));
times = mpi_set_ulli(times, _times);
two = mpi_set_ulli(two, 2);
int rv = 1;
// Primes p, q; n, phi and a
TRY_CATCH_GCRYPT((generate_prime(&p) || generate_prime(&q)), "Generate primes", goto cleanup);
gcry_mpi_mul(n, p, q);
gcry_mpi_sub_ui(p, p, 1);
gcry_mpi_sub_ui(q, q, 1);
gcry_mpi_mul(phi, p, q);
while (!((gcry_mpi_cmp_ui(a, 1) > 0) && (gcry_mpi_cmp(a, n) < 0)))
gcry_mpi_randomize(a, gcry_mpi_get_nbits(n), GCRY_WEAK_RANDOM);
// ciphertext of key
gcry_mpi_powm(ckey, two, times, phi);
gcry_mpi_powm(ckey, a, ckey, n);
while (!((gcry_mpi_cmp_ui(key, 1) > 0) && (gcry_mpi_cmp(key, n) < 0)))
gcry_mpi_randomize(key, KEY_BITS, GCRY_WEAK_RANDOM);
gcry_mpi_addm(ckey, key, ckey, n);
rv = 0;
cleanup:
CLEANUP((p)(q)(phi)(two)(times));
return rv;
}
void solve_puzzle (gcry_mpi_t key, const gcry_mpi_t n, const gcry_mpi_t a, const unsigned long long times, const gcry_mpi_t ckey, int progress)
{
// Compute number of squarings
unsigned long long full = times / SQUARINGS_PER_ITERATION;
unsigned long long partial = times % SQUARINGS_PER_ITERATION;
// Perform squarings
gcry_mpi_set(key, a);
WITH_MPI
((one)(exponent),
one = mpi_set_ulli(one, 1);
gcry_mpi_mul_2exp(exponent, one, SQUARINGS_PER_ITERATION);
for (unsigned long long i=0; i<full; i++) {
if (progress) fprintf(stderr, "\rProgress: %.2f%%", (100.0*i)/full);
gcry_mpi_powm(key, key, exponent, n);
}
gcry_mpi_mul_2exp(exponent, one, partial);
gcry_mpi_powm(key, key, exponent, n);
if (progress) fprintf(stderr, "\rProgress: 100.00%%\n");
gcry_mpi_subm(key, ckey, key, n););
}
gcry_error_t open_cipher (gcry_cipher_hd_t* handle, const void *key, const unsigned char *nonce)
{
gcry_error_t err;
if (gcry_cipher_open(handle, GCRY_CIPHER_SALSA20, GCRY_CIPHER_MODE_STREAM, 0)) return err;
if (gcry_cipher_setkey(*handle, key, KEY_SIZE)) return err;
if (gcry_cipher_setiv(*handle, nonce, NONCE_SIZE)) return err;
return 0;
}
int handle_gcrypt_error (gcry_error_t err, const char* function)
{
if (err) fprintf(stderr, "%s: %s/%s\n", function, gcry_strsource(err), gcry_strerror(err));
return err ? 1 : 0;
}
void info ()
{
unsigned long long times;
double sps = squarings_per_second();
if (sps == 0) exit(1);
WITH_MPI
((n)(a)(ckey),
read_parameters(&n, &a, ×, &ckey);
printf("n: %u bits\n", gcry_mpi_get_nbits(n));
printf("a: %u bits\n", gcry_mpi_get_nbits(a));
printf("squarings: %llu [ETA: %.0f second(s)]\n", times, round(times/sps));
printf("ckey: %u bits\n", gcry_mpi_get_nbits(ckey)););
}
void read_parameters (gcry_mpi_t* n, gcry_mpi_t* a, unsigned long long* times, gcry_mpi_t* ckey)
{
unsigned char* buffer;
SAFE_MALLOC(buffer, strlen(MAGIC_BYTES) + 1, exit(1));
ALLOCATE_MPI((times_mpi));
// Check for magic bytes
buffer[strlen(MAGIC_BYTES)] = 0;
SAFE_FREAD(buffer, strlen(MAGIC_BYTES), );
if (!strstr((char*)buffer, MAGIC_BYTES))
error(1, 0, "Magic bytes missing. Input data is invalid.\n");
// Check for output version
uint8_t version = fgetc(stdin);
if (version > OUTPUT_VERSION)
error(1, 0, "Input data format version %d not supported. Please upgrade %s.\n", version, PACKAGE);
// Read parameters
enum chunk_type type;
gcry_mpi_t* params[] = {NULL, n, a, ×_mpi, ckey};
SAFE_REALLOC(buffer, 2, exit(1));
while ((type = fgetc(stdin))) {
SAFE_FREAD(buffer, 2, );
size_t len = be16toh(*(uint16_t*)buffer);
len = (len / 8) + (len % 8 ? 1 : 0);
SAFE_REALLOC(buffer, len + 2, exit(1));
SAFE_FREAD(buffer + 2, len, );
TRY_CATCH_GCRYPT(gcry_mpi_scan(params[type], GCRYMPI_FMT_PGP, buffer, len + 2, NULL),
"Read parameter", exit(1));
}
*times = mpi_to_ulli(times_mpi);
CLEANUP((times_mpi)(buffer));
}