From 643cd327f40a016089fe21b4f3db4dc7c38ff41d Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Sun, 29 Dec 2019 00:47:36 -0500 Subject: [PATCH] Add support for compiling against openssl OR libsodium, and tests for both against each other --- .gitignore | 4 + Makefile | 14 ++- pegh.c | 289 +++++++++++++++++++++++++++++++++++++++++------------ test.sh | 47 +++++++-- 4 files changed, 280 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 8dd6e5b..2b734c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ pegh pegh.exe +pegh.libsodium +pegh.openssl bla.txt + +*.kate-swp \ No newline at end of file diff --git a/Makefile b/Makefile index 0fefd14..37a2de0 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,21 @@ CFLAGS += -Wall -Wextra -Werror -std=c89 -pedantic \ -Wstrict-prototypes -Wold-style-definition -Wconversion \ - -Wno-missing-prototypes -Wno-missing-noreturn \ + -Wno-missing-prototypes -Wno-missing-noreturn \ -O3 + +ifdef PEGH_OPENSSL +CFLAGS += -DPEGH_OPENSSL LDFLAGS += -lcrypto +else +ifdef PEGH_LIBSODIUM +CFLAGS += -DPEGH_LIBSODIUM +LDFLAGS += -lsodium +else +CFLAGS += -DPEGH_OPENSSL +LDFLAGS += -lcrypto +endif +endif all : pegh diff --git a/pegh.c b/pegh.c index 9a8448c..1f60a98 100644 --- a/pegh.c +++ b/pegh.c @@ -19,15 +19,11 @@ /* compile with: cc pegh.c -lcrypto -O3 -o pegh */ -#include -#include -#include -#include - #include #include #include #include +#include /* * tweak default scrypt hardness params here @@ -38,10 +34,10 @@ #define SCRYPT_N 32768 #define SCRYPT_R 8 #define SCRYPT_P 1 -#define SCRYPT_MAX_MEM_MB 64 +#define SCRYPT_MAX_MEM 1024 * 1024 * 64 /* 64 megabytes */ /* tweak buffer sizes here, memory use will be twice this */ -#define BUFFER_SIZE_MB 16 +#define BUFFER_SIZE_MB 32 /* * pegh file format, numbers are inclusive 0-based byte array indices @@ -80,6 +76,21 @@ #define IV_LEN 12 #define GCM_TAG_LEN 16 +/* default of OpenSSL for now... */ +#if !defined(PEGH_OPENSSL) && !defined(PEGH_LIBSODIUM) +#define PEGH_OPENSSL 1 +#endif + +#ifdef PEGH_OPENSSL + +#include +#include +#include +#include + +/* this is because we read up to buffer_size at once, and then send that value to openssl which uses int instead of size_t, limit of 2gb */ +static const size_t CHUNK_SIZE_MAX = INT_MAX; + /* * returns 1 on success, 0 on failure * @@ -93,7 +104,7 @@ * ciphertext must have the capacity of at least plaintext_len * tag must have the capacity of at least GCM_TAG_LEN */ -int gcm_encrypt(const unsigned char *plaintext, const int plaintext_len, +int gcm_encrypt(const unsigned char *plaintext, const size_t plaintext_len, const unsigned char *key, const unsigned char *iv, unsigned char *ciphertext, @@ -125,7 +136,7 @@ int gcm_encrypt(const unsigned char *plaintext, const int plaintext_len, * Provide the message to be encrypted, and obtain the encrypted output. * EVP_EncryptUpdate can be called multiple times if necessary */ - if(1 != EVP_EncryptUpdate(ctx, ciphertext, &ciphertext_written, plaintext, plaintext_len)) + if(1 != EVP_EncryptUpdate(ctx, ciphertext, &ciphertext_written, plaintext, (int) plaintext_len)) break; /* if this isn't true, GCM is broken, we probably don't need to check... @@ -167,7 +178,7 @@ int gcm_encrypt(const unsigned char *plaintext, const int plaintext_len, * these will be written into: * plaintext must have the capacity of at least ciphertext_len */ -int gcm_decrypt(const unsigned char *ciphertext, const int ciphertext_len, +int gcm_decrypt(const unsigned char *ciphertext, const size_t ciphertext_len, const unsigned char *key, const unsigned char *iv, unsigned char *tag, @@ -199,7 +210,7 @@ int gcm_decrypt(const unsigned char *ciphertext, const int ciphertext_len, * Provide the message to be decrypted, and obtain the plaintext output. * EVP_DecryptUpdate can be called multiple times if necessary */ - if(!EVP_DecryptUpdate(ctx, plaintext, &plaintext_written, ciphertext, ciphertext_len)) + if(!EVP_DecryptUpdate(ctx, plaintext, &plaintext_written, ciphertext, (int) ciphertext_len)) break; /* if this isn't true, GCM is broken, we probably don't need to check... @@ -228,6 +239,146 @@ int gcm_decrypt(const unsigned char *ciphertext, const int ciphertext_len, return ret; } +/* returns 1 on success, 0 on error */ +int scrypt_derive_key(char *password, size_t password_len, + uint32_t scrypt_max_mem, uint32_t N, + uint8_t r, uint8_t p, unsigned char *salt, unsigned char *key, FILE *err) { + /* derive key using salt, password, and scrypt parameters */ + if (EVP_PBE_scrypt( + password, password_len, + salt, SALT_LEN, + (uint64_t) N, (uint64_t) r, (uint64_t) p, + (uint64_t) scrypt_max_mem, + key, KEY_LEN + ) <= 0) { + if(NULL != err) { + fprintf(err, "scrypt key derivation error\n"); + ERR_print_errors_fp(err); + } + return 0; + } + return 1; +} + +/* returns 1 on success, 0 on error */ +int random_salt(unsigned char *salt) { + return RAND_bytes(salt, SALT_LEN) <= 0 ? 0 : 1; +} + +void wipe_memory(void * const ptr, const size_t len) { + OPENSSL_cleanse(ptr, len); +} + +#endif +#ifdef PEGH_LIBSODIUM + +#include + +/* unlike openssl, libsodium uses proper types, so we can go all the way up to the "aes-gcm-256 is still secure" limit of around 32gb */ +static const size_t CHUNK_SIZE_MAX = 1024UL * 1024 * 1024 * 32; + +/* + * returns 1 on success, 0 on failure + * + * these will be read from: + * plaintext + * plaintext_len + * key must be length KEY_LEN + * iv must be length IV_LEN + * + * these will be written into: + * ciphertext must have the capacity of at least plaintext_len + * tag must have the capacity of at least GCM_TAG_LEN + */ +int gcm_encrypt(const unsigned char *plaintext, const size_t plaintext_len, + const unsigned char *key, + const unsigned char *iv, + unsigned char *ciphertext, + unsigned char *tag + ) +{ + crypto_aead_aes256gcm_encrypt_detached(ciphertext, + tag, NULL, + plaintext, plaintext_len, + NULL, 0, + NULL, iv, key); + return 1; +} + +/* + * returns 1 on success, 0 on failure + * + * these will be read from: + * ciphertext + * ciphertext_len + * key must be length KEY_LEN + * iv must be length IV_LEN + * tag must be length GCM_TAG_LEN + * + * these will be written into: + * plaintext must have the capacity of at least ciphertext_len + */ +int gcm_decrypt(const unsigned char *ciphertext, const size_t ciphertext_len, + const unsigned char *key, + const unsigned char *iv, + unsigned char *tag, + unsigned char *plaintext + ) +{ + return crypto_aead_aes256gcm_decrypt_detached(plaintext, + NULL, + ciphertext, (size_t) ciphertext_len, + tag, + NULL, 0, + iv, key) != 0 ? 0 : 1; +} + +/* returns 1 on success, 0 on error */ +int scrypt_derive_key(char *password, size_t password_len, + uint32_t scrypt_max_mem, uint32_t N, + uint8_t r, uint8_t p, unsigned char *salt, unsigned char *key, FILE *err) { + size_t needed_memory; + /* derive key using salt, password, and scrypt parameters */ + + /* this is how crypto_pwhash_scryptsalsa208sha256_ll calculates the memory needed, so do it here first and check */ + needed_memory = (size_t) 128 * r * p; + needed_memory += (size_t) 128 * r * (size_t) N; + needed_memory += (size_t) 256 * r + 64; + + if (needed_memory > scrypt_max_mem) { + if(NULL != err) { + /* +1 is to round up here and avoid math.h and ceil()... */ + fprintf(err, "scrypt key derivation error, needed memory %lu mb, allowed memory %d mb, increase -m\n", (needed_memory / 1024 / 1024) + 1, scrypt_max_mem / 1024 / 1024); + } + return 0; + } + + if (crypto_pwhash_scryptsalsa208sha256_ll( + (const uint8_t *) password, password_len, + salt, SALT_LEN, + (uint64_t) N, (uint32_t) r, (uint32_t) p, + key, KEY_LEN + ) < 0) { + if(NULL != err) { + fprintf(err, "scrypt key derivation error\n"); + } + return 0; + } + return 1; +} + +/* returns 1 on success, 0 on error */ +int random_salt(unsigned char *salt) { + randombytes_buf(salt, SALT_LEN); + return 1; +} + +void wipe_memory(void * const ptr, const size_t len) { + sodium_memzero(ptr, len); +} + +#endif + /* returns 1 on success, 0 on failure */ int iv_increment_forbid_zero(unsigned char *n, const size_t nlen, FILE *err) { @@ -256,7 +407,7 @@ int gcm_encrypt_stream(const unsigned char *key, unsigned char *iv, size_t buffe while ((plaintext_read = fread(plaintext, 1, buffer_size, in)) > 0) { - if(1 != gcm_encrypt(plaintext, (int) plaintext_read, key, iv, ciphertext, ciphertext + plaintext_read)) + if(1 != gcm_encrypt(plaintext, plaintext_read, key, iv, ciphertext, ciphertext + plaintext_read)) return 0; if(1 != iv_increment_forbid_zero(iv, IV_LEN, err)) @@ -286,7 +437,7 @@ int gcm_decrypt_stream(const unsigned char *key, unsigned char *iv, size_t buffe ciphertext_read -= GCM_TAG_LEN; - if(1 != gcm_decrypt(ciphertext, (int) ciphertext_read, key, iv, ciphertext + ciphertext_read, plaintext)) + if(1 != gcm_decrypt(ciphertext, ciphertext_read, key, iv, ciphertext + ciphertext_read, plaintext)) return 0; if(1 != iv_increment_forbid_zero(iv, IV_LEN, err)) @@ -321,10 +472,15 @@ int gcm_stream(const unsigned char *key, size_t buffer_size, int exit_code = 0; - if(buffer_size > INT_MAX) { - /* this is because we read up to buffer_size at once, and then send that value to openssl which uses int instead of size_t */ - if(NULL != err) - fprintf(err, "due to openssl API buffer_size can at most be %d\n", INT_MAX); + if(buffer_size > CHUNK_SIZE_MAX) { + if(NULL != err) { +#ifdef PEGH_OPENSSL + fprintf(err, "due to openssl API limitation, buffer_size can at most be %ld\n", CHUNK_SIZE_MAX); +#endif +#ifdef PEGH_LIBSODIUM + fprintf(err, "due to AES-256-GCM security constraints, buffer_size can at most be %ld\n", CHUNK_SIZE_MAX); +#endif + } return 0; } @@ -349,35 +505,16 @@ int gcm_stream(const unsigned char *key, size_t buffer_size, free(ciphertext); if(NULL != err && exit_code != 1) { +#ifdef PEGH_OPENSSL /* print openssl errors */ ERR_print_errors_fp(err); +#endif fprintf(err, "%scryption failed\n", decrypt ? "de" : "en"); } return exit_code; } -/* returns 1 on success, 0 on error */ -int scrypt_derive_key(char *password, - uint32_t scrypt_max_mem_mb, uint32_t N, - uint8_t r, uint8_t p, unsigned char *salt, unsigned char *key, FILE *err) { - /* derive key using salt, password, and scrypt parameters */ - if (EVP_PBE_scrypt( - password, strlen(password), - salt, SALT_LEN, - (uint64_t) N, (uint64_t) r, (uint64_t) p, - (uint64_t) scrypt_max_mem_mb * 1024 * 1024, - key, KEY_LEN - ) <= 0) { - if(NULL != err) { - fprintf(err, "scrypt key derivation error\n"); - ERR_print_errors_fp(err); - } - return 0; - } - return 1; -} - /* buf must be at least 4 bytes */ uint32_t read_uint32_big_endian(const unsigned char *buf) { return (uint32_t) ((buf[0] & 0xFF) << 24) @@ -394,13 +531,34 @@ void write_uint32_big_endian(uint32_t val, unsigned char *buf) { buf[3] = val & 0xFF; } +/* returns 1 on success, 0 on failure */ +int scrypt_derive_key_gcm_stream(char *password, + uint32_t scrypt_max_mem, size_t buffer_size, + FILE *in, FILE *out, FILE *err, + uint32_t N, uint8_t r, uint8_t p, unsigned char *salt, int decrypt) { + unsigned char key[KEY_LEN] = {0}; + int ret; + size_t password_len; + + password_len = strlen(password); + + ret = scrypt_derive_key(password, password_len, scrypt_max_mem, N, r, p, salt, key, err); + wipe_memory(password, password_len); + + if(ret == 1) + ret = gcm_stream(key, buffer_size, decrypt, in, out, err); + + wipe_memory(key, KEY_LEN); + return ret; +} + /* returns 1 on success, 0 on failure */ int pegh_encrypt(char *password, - uint32_t scrypt_max_mem_mb, size_t buffer_size, + uint32_t scrypt_max_mem, size_t buffer_size, FILE *in, FILE *out, FILE *err, uint32_t N, uint8_t r, uint8_t p) { - unsigned char key[KEY_LEN] = {0}, salt[SALT_LEN] = {0}; + unsigned char salt[SALT_LEN] = {0}; /* first write the version and parameters */ salt[0] = 0; @@ -411,27 +569,26 @@ int pegh_encrypt(char *password, fwrite(salt, 1, PRE_SALT_LEN, out); /* generate random salt, then write it out */ - if (RAND_bytes(salt, SALT_LEN) <= 0) { + if (random_salt(salt) != 1) { if(NULL != err) { fprintf(err, "random salt generation error\n"); +#ifdef PEGH_OPENSSL ERR_print_errors_fp(err); +#endif } return 0; } fwrite(salt, 1, SALT_LEN, out); - if(1 != scrypt_derive_key(password, scrypt_max_mem_mb, N, r, p, salt, key, err)) - return 0; - - return gcm_stream(key, buffer_size, 0, in, out, err); + return scrypt_derive_key_gcm_stream(password, scrypt_max_mem, buffer_size, in, out, err, N, r, p, salt, 0); } /* returns 1 on success, 0 on failure */ int pegh_decrypt(char *password, - uint32_t scrypt_max_mem_mb, size_t max_buffer_size, + uint32_t scrypt_max_mem, size_t max_buffer_size, FILE *in, FILE *out, FILE *err) { - unsigned char key[KEY_LEN] = {0}, salt[SALT_LEN] = {0}; + unsigned char salt[SALT_LEN] = {0}; size_t header_read, buffer_size; @@ -468,10 +625,7 @@ int pegh_decrypt(char *password, return 0; } - if(1 != scrypt_derive_key(password, scrypt_max_mem_mb, N, r, p, salt, key, err)) - return 0; - - return gcm_stream(key, buffer_size, 1, in, out, err); + return scrypt_derive_key_gcm_stream(password, scrypt_max_mem, buffer_size, in, out, err, N, r, p, salt, 1); } int help(int exit_code) { @@ -492,11 +646,11 @@ usage: pegh [options...] password\n\ fprintf(stderr, "\ only allocated after scrypt is finished so max usage will be\n\ the highest of these only, not both combined,\n\ - max: %d, default: %d\n\ + max: %ld, default: %d\n\ -m maximum megabytes of ram to use when deriving key from password\n\ with scrypt, applies for encryption AND decryption, must\n\ almost linearly scale with -N, if too low operation will fail,\n\ - default: %d\n", INT_MAX / 1024 / 1024, BUFFER_SIZE_MB, SCRYPT_MAX_MEM_MB); + default: %d\n", CHUNK_SIZE_MAX / 1024 / 1024, BUFFER_SIZE_MB, SCRYPT_MAX_MEM / 1024 / 1024); fprintf(stderr, "\ -N scrypt parameter N, only applies for encryption, default %d\n\ this is rounded up to the next highest power of 2\n\ @@ -567,12 +721,23 @@ int main(int argc, char **argv) { int optind, decrypt = 0, append = 0, exit_code = 2; char *password = NULL; - uint32_t N = SCRYPT_N, scrypt_max_mem_mb = SCRYPT_MAX_MEM_MB, buffer_size = BUFFER_SIZE_MB * 1024 * 1024, scale = 1; + uint32_t N = SCRYPT_N, scrypt_max_mem = SCRYPT_MAX_MEM, buffer_size = BUFFER_SIZE_MB * 1024 * 1024, scale = 1; uint8_t r = SCRYPT_R, p = SCRYPT_P; FILE *in = stdin, *out = stdout, *err = stderr; char *in_filename = NULL, *out_filename = NULL; +#ifdef PEGH_LIBSODIUM + if (sodium_init() == -1) { + fprintf(stderr, "Error: libsodium could not be initialized, compile/use openssl version?\n"); + return 2; + } + if (crypto_aead_aes256gcm_is_available() == 0) { + fprintf(stderr, "Error: libsodium does not support AES-256-GCM on this CPU, compile/use openssl version?\n"); + return 2; + } +#endif + for (optind = 1; optind < argc; ++optind) { if(strlen(argv[optind]) == 2 && argv[optind][0] == '-') { @@ -608,13 +773,13 @@ int main(int argc, char **argv) break; case 'c': buffer_size = parse_int_arg(++optind, argc, argv) * 1024 * 1024; - if(buffer_size > INT_MAX) { - fprintf(stderr, "Error: %s chunk size cannot exceed %d megabytes\n", argv[optind - 1], INT_MAX / 1024 / 1024); + if(buffer_size > CHUNK_SIZE_MAX) { + fprintf(stderr, "Error: %s chunk size cannot exceed %ld megabytes\n", argv[optind - 1], CHUNK_SIZE_MAX / 1024 / 1024); return help(2); } break; case 'm': - scrypt_max_mem_mb = parse_int_arg(++optind, argc, argv); + scrypt_max_mem = parse_int_arg(++optind, argc, argv) * 1024 * 1024; break; case 'N': N = next_highest_power_of_2(parse_int_arg(++optind, argc, argv)); @@ -663,11 +828,11 @@ int main(int argc, char **argv) /* apply scale */ N *= scale; - scrypt_max_mem_mb *= scale; + scrypt_max_mem *= scale; /* - fprintf (stderr, "decrypt = %d, key = %s, scrypt_max_mem_mb = %d, N = %d, r = %d, p = %d, scale = %d\n", - decrypt, password, scrypt_max_mem_mb, N, r, p, scale); + fprintf (stderr, "decrypt = %d, key = %s, scrypt_max_mem = %d, N = %d, r = %d, p = %d, scale = %d\n", + decrypt, password, scrypt_max_mem, N, r, p, scale); return 0; */ @@ -689,9 +854,9 @@ int main(int argc, char **argv) } if(decrypt) - exit_code = pegh_decrypt(password, scrypt_max_mem_mb, buffer_size, in, out, err); + exit_code = pegh_decrypt(password, scrypt_max_mem, buffer_size, in, out, err); else - exit_code = pegh_encrypt(password, scrypt_max_mem_mb, buffer_size, in, out, err, N, r, p); + exit_code = pegh_encrypt(password, scrypt_max_mem, buffer_size, in, out, err, N, r, p); if(NULL != in_filename) fclose(in); diff --git a/test.sh b/test.sh index f758e98..fce09e6 100755 --- a/test.sh +++ b/test.sh @@ -1,45 +1,70 @@ #!/bin/bash +export dummy_file="$1" +shift +export dummy_mb="$1" + +[ "$dummy_file" = "" ] && export dummy_file='/dev/shm/randombytes' +[ "$dummy_mb" = "" ] && export dummy_mb='100' + set -euo pipefail # try different size files to encrypt/decrypt -[ -e /dev/shm/randombytes ] || dd if=/dev/urandom bs=1M count=100 of=/dev/shm/randombytes +[ -e "$dummy_file" ] || dd if=/dev/urandom bs=1M "count=$dummy_mb" of="$dummy_file" # try make if it's installed, otherwise fall back to cc -make || cc pegh.c -lcrypto -O3 -o pegh -#cargo build --release +bins="./pegh.openssl ./pegh.libsodium" +#bins="./pegh.libsodium ./pegh.openssl" +rm -f pegh $bins -export key="$(openssl rand -base64 20)" +# compile against openssl +make PEGH_OPENSSL=1 || cc pegh.c -DPEGH_OPENSSL -lcrypto -O3 -o pegh +mv pegh pegh.openssl + +# compile against libsodium +make PEGH_LIBSODIUM=1 || cc pegh.c -DPEGH_LIBSODIUM -lsodium -O3 -o pegh +mv pegh pegh.libsodium + +export key="$(< /dev/urandom tr -dc 'a-z0-9' | head -c12)" echo "key: $key" test () { bin="$1" + bin_decrypt="${2:-$bin}" + + echo "testing bins: $bin bin_decrypt: $bin_decrypt" echo 'encrypting then decrypting with the same key should succeed' - "$bin" -e "$key" < /dev/shm/randombytes | "$bin" -d "$key" | cmp - /dev/shm/randombytes + "$bin" -e "$key" < "$dummy_file" | "$bin_decrypt" -d "$key" | cmp - "$dummy_file" echo 'test with -s 32 requiring 2gb of ram should succeed' # can send -s 32 or -m 2048 to decrypt command with identical effect - "$bin" -e "$key" -s 32 < /dev/shm/randombytes | "$bin" -d "$key" -m 2048 | cmp - /dev/shm/randombytes + "$bin" -e "$key" -s 32 < "$dummy_file" | "$bin_decrypt" -d "$key" -m 2048 | cmp - "$dummy_file" set +e # these should fail echo 'encrypting with one key and decrypting with another should fail' - "$bin" -e "$key" -i /dev/shm/randombytes | "$bin" -d "$key-wrongkey" | cmp - /dev/shm/randombytes && echo "ERROR: appending -wrongkey to key somehow still worked" && exit 1 + "$bin" -e "$key" -i "$dummy_file" | "$bin_decrypt" -d "$key-wrongkey" | cmp - "$dummy_file" && echo "ERROR: appending -wrongkey to key somehow still worked" && exit 1 echo 'large values of N without enough memory should fail' - "$bin" -e "$key" -N 2000000 -i /dev/shm/randombytes >/dev/null && echo "ERROR: N of 2 million without extra memory worked" && exit 1 - "$bin" -d "$key" -N 2000000 -i /dev/shm/randombytes >/dev/null && echo "ERROR: N of 2 million without extra memory worked" && exit 1 + "$bin" -e "$key" -N 2000000 -i "$dummy_file" >/dev/null && echo "ERROR: N of 2 million without extra memory worked" && exit 1 + "$bin_decrypt" -d "$key" -N 2000000 -i "$dummy_file" >/dev/null && echo "ERROR: N of 2 million without extra memory worked" && exit 1 # todo: can we also make this the case for stdout? needs some buffering... echo 'bad decryption should result in output file being deleted' - echo 'hopefully this doesnt make it to disk' | "$bin" "$key" | cat - <(echo -n a) | "$bin" -d "$key" -o bla.txt && exit 1 + echo 'hopefully this doesnt make it to disk' | "$bin" "$key" | cat - <(echo -n a) | "$bin_decrypt" -d "$key" -o bla.txt && exit 1 [ -s bla.txt ] && echo "ERROR: bla.txt should not exist" && exit 1 set -e } -time test ./pegh +for bin in $bins +do + for bin_decrypt in $bins + do + time test $bin $bin_decrypt + done +done echo "successful test run!"