Compare commits

...

9 Commits

Author SHA1 Message Date
Juhani Krekelä a3079f5eda Warn is passphrase file is world-readable 2021-04-09 23:08:22 +03:00
Juhani Krekelä 41a74402f0 Add support for specifying the input and output file 2021-04-09 23:00:56 +03:00
Juhani Krekelä 5393e64c18 Add support for reading passphrase from file 2021-04-09 22:54:54 +03:00
Juhani Krekelä 6079530e1d Add UI to puer 2021-04-09 22:35:10 +03:00
Juhani Krekelä 4b5ef70bce Upgrade message counter to 64 bits 2021-04-09 22:34:05 +03:00
Juhani Krekelä f445783a44 Make passphrase_prompt() return -1 on failure so that caller can clean up 2021-04-09 20:52:04 +03:00
Juhani Krekelä 0cb02aaf14 Use the correct counter values with encryption/decryption 2021-04-09 20:49:21 +03:00
Juhani Krekelä 138cc5d2f5 Make cc_{en,de}crypt stay within passed message length 2021-04-09 20:37:41 +03:00
Juhani Krekelä 4ec4a06776 Have a shared work buffer and derive the number of KDF round from size of buffer 2021-04-09 20:16:54 +03:00
1 changed files with 343 additions and 67 deletions

410
puer.c
View File

@ -10,6 +10,10 @@
#include <termios.h>
#include <unistd.h>
// Adjusting this will render the file format incompatible
// The minimum possible buffer size is 64
unsigned char workbuf[8 * 1024 * 1024];
void xxtea128(uint32_t const key[4], uint32_t block[4]) {
// Encryption half of the XXTEA algorithm, with block size limited
// to 128 bits or 4 words. This avoids all the weaknesses that
@ -268,9 +272,8 @@ void hmac(unsigned char output[32], unsigned char key[], size_t keylen, unsigned
finalize_hash(&state, output);
}
// KDF_ROUNDS must be at least 2
#define KDF_ROUNDS 100000
unsigned char kdf_buf[KDF_ROUNDS * 32];
#define KDF_ROUNDS (sizeof(workbuf) / 32)
void kdf(unsigned char key[16], unsigned char salt[32], unsigned char passphrase[], size_t passphraselen) {
// This is based on the design of PBKDF2 but aims to be memory hard
// This is achieved by storing all the hashes in a buffer and the
@ -288,23 +291,26 @@ void kdf(unsigned char key[16], unsigned char salt[32], unsigned char passphrase
// include the counter i from PBKDF2 since we will ever only
// produce one block of output
size_t index = KDF_ROUNDS*32 - 32;
hmac(&kdf_buf[index], passphrase, passphraselen, salt, 32);
hmac(&workbuf[index], passphrase, passphraselen, salt, 32);
index -= 32;
// Walk back along the buffer, at each step hashing the previous
// hashes
while (index > 0) {
hmac(&kdf_buf[index], passphrase, passphraselen, &kdf_buf[index+32], 32);
hmac(&workbuf[index], passphrase, passphraselen, &workbuf[index+32], 32);
index -= 32;
}
hmac(kdf_buf, passphrase, passphraselen, &kdf_buf[32], 32);
hmac(workbuf, passphrase, passphraselen, &workbuf[32], 32);
// Perform the final hash
unsigned char final_hash[32];
hmac(final_hash, passphrase, passphraselen, kdf_buf, KDF_ROUNDS * 32);
hmac(final_hash, passphrase, passphraselen, workbuf, KDF_ROUNDS * 32);
// Use first 128 bits of final hash as the key
memcpy(key, final_hash, 16);
// Empty the buffer
explicit_bzero(workbuf, sizeof(workbuf));
}
// 16 bit authentication tag
@ -312,7 +318,7 @@ const int mprime = (16-2)/2;
// 32 bit = 4 byte length field
const int lprime = 4-1;
void ccm_mac(unsigned char mac[16], uint32_t key[4], uint32_t messageindex, unsigned char message[], uint32_t length) {
void ccm_mac(unsigned char mac[16], uint32_t key[4], uint64_t messageindex, unsigned char message[], uint32_t length) {
// CCM specifies that the length field is big endian while we are
// natively little endian. Flip it.
unsigned char length_bytes[4];
@ -323,7 +329,7 @@ void ccm_mac(unsigned char mac[16], uint32_t key[4], uint32_t messageindex, unsi
uint32_t be_length = bytes2word(length_bytes);
// First block is special
uint32_t mac_words[4] = {mprime<<3 | lprime, 0, messageindex, be_length};
uint32_t mac_words[4] = {mprime<<3 | lprime, messageindex, messageindex >> 32, be_length};
xxtea128(key, mac_words);
// Process all full blocks
@ -358,7 +364,7 @@ void ccm_mac(unsigned char mac[16], uint32_t key[4], uint32_t messageindex, unsi
words2block(mac, mac_words);
}
void ccm_xor_block(unsigned char block[16], uint32_t key[4], uint32_t messageindex, uint32_t counter) {
void ccm_xor_block(unsigned char block[16], uint32_t key[4], uint64_t messageindex, uint32_t counter) {
// CCM specifies that the counter field is big endian while we are
// natively little endian. Flip it.
unsigned char counter_bytes[4];
@ -368,7 +374,7 @@ void ccm_xor_block(unsigned char block[16], uint32_t key[4], uint32_t messageind
counter_bytes[3] = counter;
uint32_t be_counter = bytes2word(counter_bytes);
uint32_t words[4] = {lprime, 0, messageindex, be_counter};
uint32_t words[4] = {lprime, messageindex, messageindex >> 32, be_counter};
xxtea128(key, words);
unsigned char keystream[16];
words2block(keystream, words);
@ -378,10 +384,7 @@ void ccm_xor_block(unsigned char block[16], uint32_t key[4], uint32_t messageind
}
}
// IMPORTANT: The underlying message[] must be addressable until the index
// ceil(length / 16)*16, but the length only reflects the actual message
// length
void ccm_encrypt(unsigned char key[16], uint32_t messageindex, unsigned char message[], uint32_t length, unsigned char mac[16]) {
void ccm_encrypt(unsigned char key[16], uint64_t messageindex, unsigned char message[], uint32_t length, unsigned char mac[16]) {
uint32_t key_words[4];
block2words(key_words, key);
@ -391,25 +394,44 @@ void ccm_encrypt(unsigned char key[16], uint32_t messageindex, unsigned char mes
// Encrypt
// MAC is xored with first block of keystream
ccm_xor_block(mac, key_words, messageindex, 0);
// Xor the message
for (uint32_t index = 0; index < length; index += 16) {
// Message blocks are numbered from index 1 onwards
ccm_xor_block(&message[index], key_words, messageindex, index + 1);
// Xor full blocks
size_t index = 0;
uint32_t counter = 1;
for (; index + 16 <= length; index += 16) {
ccm_xor_block(&message[index], key_words, messageindex, counter++);
}
// Xor partial block, if any
if (index < length) {
unsigned char fullblock[16];
memcpy(fullblock, &message[index], length - index);
ccm_xor_block(fullblock, key_words, messageindex, counter++);
memcpy(&message[index], fullblock, length - index);
}
}
// Same requirements for message[] hold as with ccm_encrypt
bool ccm_decrypt(unsigned char key[16], uint32_t messageindex, unsigned char message[], uint32_t length, unsigned char mac[16]) {
bool ccm_decrypt(unsigned char key[16], uint64_t messageindex, unsigned char message[], uint32_t length, unsigned char mac[16]) {
uint32_t key_words[4];
block2words(key_words, key);
// Decrypt
// MAC is xored with first block of keystream
ccm_xor_block(mac, key_words, messageindex, 0);
// Xor the message
for (uint32_t index = 0; index < length; index += 16) {
// Xor full blocks
size_t index = 0;
uint32_t counter = 1;
for (; index + 16 <= length; index += 16) {
// Message blocks are numbered from index 1 onwards
ccm_xor_block(&message[index], key_words, messageindex, index + 1);
ccm_xor_block(&message[index], key_words, messageindex, counter++);
}
// Xor partial block, if any
if (index < length) {
unsigned char fullblock[16];
memset(fullblock, 0, 16);
memcpy(fullblock, &message[index], length - index);
ccm_xor_block(fullblock, key_words, messageindex, counter++);
memcpy(&message[index], fullblock, length - index);
}
// Compute the expected authentication tag
@ -433,31 +455,34 @@ bool ccm_decrypt(unsigned char key[16], uint32_t messageindex, unsigned char mes
return true;
}
size_t passphrase_prompt(unsigned char *passphrase, size_t size, const char *prompt) {
// Read from controlling TTY, even if stdion has been redirected
ssize_t passphrase_prompt(unsigned char *passphrase, size_t size, const char *prompt) {
// Read from controlling TTY, even if stdio has been redirected
int tty = open("/dev/tty", O_RDWR);
if (tty == -1) {
perror("Failed to open controlling tty");
exit(1);
return -1;
}
if (write(tty, prompt, strlen(prompt)) == -1) {
perror("Failed to write to terminal");
exit(1);
close(tty);
return -1;
}
// Turn off echo
struct termios saved;
if (tcgetattr(tty, &saved) != 0) {
perror("tcgetattr");
exit(1);
perror("Failed to get terminal attributes");
close(tty);
return -1;
}
struct termios altered;
altered = saved;
altered.c_lflag &= ~ECHO;
if (tcsetattr(tty, TCSANOW, &altered) != 0) {
perror("tcsetattr");
exit(1);
perror("Failed to turn echoing off");
close(tty);
return -1;
}
// Read until newline
@ -476,18 +501,21 @@ size_t passphrase_prompt(unsigned char *passphrase, size_t size, const char *pro
}
}
tcsetattr(tty, TCSANOW, &saved);
exit(1);
close(tty);
return -1;
}
ssize_t bytes_read = read(tty, &passphrase[index], size - index);
if (bytes_read == -1) {
perror("Failed to read passphrase");
tcsetattr(tty, TCSANOW, &saved);
exit(1);
close(tty);
return -1;
} else if (bytes_read == 0) {
fprintf(stderr, "Unexpected EOF\n");
tcsetattr(tty, TCSANOW, &saved);
exit(1);
close(tty);
return -1;
}
index += bytes_read;
@ -501,52 +529,300 @@ size_t passphrase_prompt(unsigned char *passphrase, size_t size, const char *pro
// terminal settings
if (write(tty, "\n", 1) == -1) {
perror("Failed to write to terminal");
exit(1);
tcsetattr(tty, TCSANOW, &saved);
close(tty);
return -1;
}
if (tcsetattr(tty, TCSANOW, &saved) != 0) {
perror("Failed to restore terminal state");
close(tty);
return -1;
}
tcsetattr(tty, TCSANOW, &saved);
close(tty);
return index - 1;
}
int main(void) {
unsigned char salt[32];
if (getentropy(salt, 32) != 0) {
perror("getentropy");
ssize_t passphrase_file(char *passfilepath, unsigned char passphrase[], size_t size) {
int file = open(passfilepath, O_RDONLY);
// Check permissions
struct stat statbuf;
if (fstat(file, &statbuf) != 0) {
perror("Could not stat passphrase file");
close(file);
return -1;
}
if (statbuf.st_mode & S_IROTH) {
fprintf(stderr, "Warning: Passphrase file is world-readable\n");
}
unsigned char passphrase[128] = {0};
size_t passphrase_len = passphrase_prompt(passphrase, sizeof(passphrase), "passphrase: ");
// Read until newline
size_t index = 0;
for (;;) {
if (index >= size) {
fprintf(stderr, "Passphrase too long, maximum size is %zu bytes\n", size - 1);
close(file);
return -1;
}
ssize_t bytes_read = read(file, &passphrase[index], size - index);
if (bytes_read == -1) {
perror("Failed to read passphrase");
close(file);
return -1;
} else if (bytes_read == 0) {
fprintf(stderr, "Unexpected EOF\n");
close(file);
return -1;
}
index += bytes_read;
if (passphrase[index-1] == '\n') {
// Got end of line
break;
}
}
close(file);
return index - 1;
}
void usage(char *name) {
fprintf(stderr, "Usage: %s -d | -e [-f] [-p passfile] [-i infile] [-o outfile]\n\n", name);
fprintf(stderr, "-d Decrypt\n");
fprintf(stderr, "-e Encrypt\n");
fprintf(stderr, "-f Force output to terminal\n");
fprintf(stderr, "-p passfile Read passphrase from a file instead of the terminal.\n");
fprintf(stderr, "-i infile Read from a file instead of the terminal.\n");
fprintf(stderr, "-o outfile Write to a file instead of the terminal.\n");
}
int main(int argc, char *argv[]) {
bool encrypting = false;
bool decrypting = false;
bool force = false;
char *passfilepath = NULL;
char *infilepath = NULL;
char *outfilepath = NULL;
int opt;
while ((opt = getopt(argc, argv, "defp:i:o:")) != -1) {
switch (opt) {
case 'd':
decrypting = true;
break;
case 'e':
encrypting = true;
break;
case 'f':
force = true;
break;
case 'p':
passfilepath = optarg;
break;
case 'i':
infilepath = optarg;
break;
case 'o':
outfilepath = optarg;
break;
default:
usage(argv[0]);
exit(1);
}
}
if (optind != argc) {
usage(argv[0]);
exit(1);
}
if ((!encrypting && !decrypting) || (encrypting && decrypting)) {
usage(argv[0]);
exit(1);
}
FILE *infile = stdin;
FILE *outfile = stdout;
if (infilepath != NULL) {
infile = fopen(infilepath, "r");
if (infile == NULL) {
perror("Failed to open input file");
exit(1);
}
}
if (outfilepath != NULL) {
outfile = fopen(outfilepath, "w");
if (outfile == NULL) {
perror("Failed to open output file");
exit(1);
}
}
if (encrypting && !force && isatty(fileno(outfile))) {
fprintf(stderr, "Refusing to print encrypted (binary) data to terminal. Use -f to force output.\n");
exit(1);
}
// Get the salt for key derivation
unsigned char salt[32];
if (encrypting) {
// Generate salt randomly
if (getentropy(salt, 32) != 0) {
perror("Could not generate salt (getentropy)");
exit(1);
}
// Write salt to the beginning of the file
if (fwrite(&salt, 32, 1, outfile) != 1) {
fprintf(stderr, "Could not write salt\n");
exit(1);
}
} else {
// Read salt from the beginning of the file
if (fread(&salt, 32, 1, infile) != 1) {
fprintf(stderr, "Could not read salt\n");
exit(1);
}
}
// Read passphrase
unsigned char passphrase[128];
ssize_t passphrase_len;
if (passfilepath == NULL) {
// Read from terminal if no passfile specified
passphrase_len = passphrase_prompt(passphrase, sizeof(passphrase), "passphrase: ");
if (passphrase_len == -1) {
explicit_bzero(passphrase, sizeof(passphrase));
exit(1);
}
if (encrypting) {
// Have the user confirm the passphrase if encrypting, to avoid losing data
unsigned char confirm[sizeof(passphrase)];
ssize_t confirm_len = passphrase_prompt(confirm, sizeof(confirm), "confirm passphrase: ");
if (confirm_len == -1) {
explicit_bzero(passphrase, sizeof(passphrase));
explicit_bzero(confirm, sizeof(confirm));
exit(1);
}
if (confirm_len != passphrase_len || memcmp(passphrase, confirm, passphrase_len) != 0) {
fprintf(stderr, "Passphrases do not match\n");
explicit_bzero(passphrase, sizeof(passphrase));
explicit_bzero(confirm, sizeof(confirm));
exit(1);
}
explicit_bzero(confirm, sizeof(confirm));
}
} else {
passphrase_len = passphrase_file(passfilepath, passphrase, sizeof(passphrase));
if (passphrase_len == -1) {
explicit_bzero(passphrase, sizeof(passphrase));
exit(1);
}
}
// Derive key
unsigned char key[16];
kdf(key, salt, passphrase, passphrase_len);
for (size_t i = 0; i < 16; i++) {
printf("%02hhx ", key[i]);
}
printf("\n\n");
explicit_bzero(passphrase, sizeof(passphrase));
char *message = "Syökää parsaa ja palvokaa saatanaa";
unsigned char buf[64] = {69};
strcpy((char*)buf, message);
uint64_t messageindex = 0;
if (encrypting) {
for (;;) {
// Leave space for the MAC in the work buffer
size_t bytes = fread(workbuf, 1, sizeof(workbuf) - 16, infile);
if (bytes == 0 && ferror(infile)) {
perror("Failure reading");
explicit_bzero(key, sizeof(key));
exit(1);
}
ccm_encrypt(key, 25, buf, strlen(message), &buf[48]);
for (size_t i = 0; i < sizeof(buf); i++) {
printf("%02hhx ", buf[i]);
if (i % 16 == 15) printf("\n");
}
printf("\n");
// MAC is after the message
unsigned char *mac = &workbuf[bytes];
memset(&buf[strlen(message)], 0x0f, 48-strlen(message));
for (size_t i = 0; i < sizeof(buf); i++) {
printf("%02hhx ", buf[i]);
if (i % 16 == 15) printf("\n");
}
printf("\n");
ccm_encrypt(key, messageindex++, workbuf, bytes, mac);
bool auth = ccm_decrypt(key, 25, buf, strlen(message), &buf[48]);
printf("auth %s\n", auth ? "succeeded" : "failed");
for (size_t i = 0; i < sizeof(buf); i++) {
printf("%02hhx ", buf[i]);
if (i % 16 == 15) printf("\n");
size_t written = fwrite(workbuf, 1, bytes + 16, outfile);
if (written != bytes + 16) {
perror("Failure writing");
explicit_bzero(key, sizeof(key));
exit(1);
}
// If this chunk was short, that means we're done
if (bytes < sizeof(workbuf) - 16) {
break;
}
if (messageindex == 0) {
// We will not run into this, but I feel like it's cleaner to check
fprintf(stderr, "Chunk counter overflow\n");
explicit_bzero(key, sizeof(key));
exit(1);
}
}
} else {
for (;;) {
size_t bytes = fread(workbuf, 1, sizeof(workbuf), infile);
if (bytes == 0 && ferror(infile)) {
perror("Failure reading");
explicit_bzero(key, sizeof(key));
exit(1);
} else if (bytes < 16) {
fprintf(stderr, "Chunk too short. File likely corrupt.\n");
explicit_bzero(key, sizeof(key));
exit(1);
}
// MAC is after the message
unsigned char *mac = &workbuf[bytes - 16];
bool auth = ccm_decrypt(key, messageindex++, workbuf, bytes - 16, mac);
if (!auth) {
if (messageindex == 1) {
// First chunk
fprintf(stderr, "Authentication failed. Either the passphrase is wrong or the file is corrupt.\n");
} else {
fprintf(stderr, "Authentication failed. The file is likely corrupt.\n");
}
explicit_bzero(key, sizeof(key));
exit(1);
}
size_t written = fwrite(workbuf, 1, bytes - 16, outfile);
if (written != bytes - 16) {
perror("Failure writing");
explicit_bzero(key, sizeof(key));
exit(1);
}
// If this chunk was short, that means we're done
if (bytes < sizeof(workbuf)) {
break;
}
if (messageindex == 0) {
// We will not run into this, but I feel like it's cleaner to check
fprintf(stderr, "Chunk counter overflow\n");
explicit_bzero(key, sizeof(key));
exit(1);
}
}
}
// Remove the key from memory
explicit_bzero(key, sizeof(key));
if (fflush(stdout) != 0) {
perror("Failure writing");
exit(1);
}
return 0;
}