puer/puer.c

578 lines
17 KiB
C
Raw Normal View History

2021-04-04 20:05:31 +00:00
#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
2021-04-04 20:05:31 +00:00
#include <stdint.h>
2021-04-08 19:45:08 +00:00
#include <stdio.h>
#include <stdlib.h>
2021-04-04 20:05:31 +00:00
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
2021-04-04 20:05:31 +00:00
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
// Wikipedia knows of, since both depend on only running 6 rounds
// per block, and we will run 6 + 52//4 = 6 + 13 = 19
uint32_t roundconstant = 0;
for (unsigned round = 0; round < 19; round++) {
// This took a while to puzzle out since the original
// specification is a mess, and the mess is only added to
// by needing to support custom blockwidths.
//
// The algorithm is as follows:
//
// 1. Set the round constant (sum) to round * 0x9e3779b9
// (implemented by addition)
//
// 2. Create a reduced version of the round constant (e)
// which is the bits 3…2 of the round constant. The
// reduced version is needed for changing the pattern of
// key accesses, since key is only 4 words long
//
// 3. Go through each word in the block and derive its new
// value based on its current value (v[p]), the next (y)
// and the previous word (z), wrapping around the ends
// of the block as needed.
2021-04-04 20:05:31 +00:00
//
// The function for deriving the new value of a word is a
// xor of sums of xors, followed by an in-place addition.
// The first sum adds together combinations of the next and
// previous word, and the second sum adds together
// previous/next combined with a value dependant on the
// round constant. The key is also mixed into the word in
// the first xor of second sum. After this the result is
// added back into the original word.
2021-04-04 20:05:31 +00:00
//
// I have changed the operand order in the second xor of
// first add and in the second add. This is to keep the
// part dependant of previous word on the left and the part
// dependant on the next word on the right.
roundconstant += 0x9e3779b9;
uint32_t reduced = (roundconstant >> 2) & 3;
block[0] += ((block[3]>>5 ^ block[1]<<2) + (block[3]<<4 ^ block[1]>>3)) ^ ((key[reduced ^ 0] ^ block[3]) + (roundconstant ^ block[1]));
block[1] += ((block[0]>>5 ^ block[2]<<2) + (block[0]<<4 ^ block[2]>>3)) ^ ((key[reduced ^ 1] ^ block[0]) + (roundconstant ^ block[2]));
block[2] += ((block[1]>>5 ^ block[3]<<2) + (block[1]<<4 ^ block[3]>>3)) ^ ((key[reduced ^ 2] ^ block[1]) + (roundconstant ^ block[3]));
block[3] += ((block[2]>>5 ^ block[0]<<2) + (block[2]<<4 ^ block[0]>>3)) ^ ((key[reduced ^ 3] ^ block[2]) + (roundconstant ^ block[0]));
}
}
uint32_t bytes2word(unsigned char const bytes[4]) {
return bytes[0] | bytes[1]<<8 | bytes[2]<<16 | bytes[3]<<24;
}
void word2bytes(unsigned char *bytes, uint32_t word) {
bytes[0] = word;
bytes[1] = word>>8;
bytes[2] = word>>16;
2021-04-04 20:05:31 +00:00
bytes[3] = word>>24;
}
2021-04-08 21:37:10 +00:00
void block2words(uint32_t words[4], unsigned char const bytes[16]) {
words[0] = bytes2word(&bytes[0]);
words[1] = bytes2word(&bytes[4]);
words[2] = bytes2word(&bytes[8]);
words[3] = bytes2word(&bytes[12]);
}
void words2block(unsigned char bytes[16], uint32_t const words[4]) {
word2bytes(&bytes[0], words[0]);
word2bytes(&bytes[4], words[1]);
word2bytes(&bytes[8], words[2]);
word2bytes(&bytes[12], words[3]);
}
2021-04-04 20:05:31 +00:00
struct hashstate {
// A_n and B_n of the MDC-2 algorithm
uint32_t a[4];
uint32_t b[4];
// Buffer to hold data until next full block
unsigned char buffer[16];
size_t length;
// Counter that keeps tracks of how much data we've hashed
uint64_t totalbits;
};
void initialize_hash(struct hashstate *state) {
// Hash function is MDC-2 with xxtea128, which is nice since it
// gives us a 256 bit hash. The constants are based on binary
// expansion of the square root of two, A1 being the first 128 bits
// and B1 the next 128.
//
// If we treat A1 and B1 as 128bit little endian integers, they
// have the values:
//
// A1 = 6a09e667 f3bcc908 b2fb1366 ea957d3e
// A2 = 3adec175 12775099 da2f590b 0667322a
state->a[0] = 0xea957d3eUL;
state->a[1] = 0xb2fb1366UL;
state->a[2] = 0xf3bcc908UL;
state->a[3] = 0x6a09e667UL;
state->b[0] = 0x0667322aUL;
state->b[1] = 0xda2f590bUL;
state->b[2] = 0x12775099UL;
state->b[3] = 0x3adec175UL;
memset(state->buffer, 0, sizeof(state->buffer));
state->length = 0;
state->totalbits = 0;
}
void compress_hash(struct hashstate *state) {
assert(state->length == 16);
// M_i
uint32_t message[4];
2021-04-08 21:37:10 +00:00
block2words(message, state->buffer);
2021-04-04 20:05:31 +00:00
// V_i = M_i ^ E(M_i, A_i)
// Note: In this description A_i is the *key*, not the plaintext
uint32_t v[4];
memcpy(v, message, sizeof(v));
xxtea128(state->a, v);
v[0] ^= message[0];
v[1] ^= message[1];
v[2] ^= message[2];
v[3] ^= message[3];
2021-04-04 20:05:31 +00:00
// W_i = M_i ^ E(M_i, B_i);
uint32_t w[4];
memcpy(w, message, sizeof(w));
xxtea128(state->b, w);
w[0] ^= message[0];
w[1] ^= message[1];
w[2] ^= message[2];
w[3] ^= message[3];
// A_{i+1} = Vwi^L || W_i^R
state->a[0] = v[0];
state->a[1] = v[1];
state->a[2] = w[2];
state->a[3] = w[3];
2021-04-04 20:05:31 +00:00
// B_{i+1} = W_i^L || V_i^R
2021-04-08 18:41:35 +00:00
state->b[0] = w[0];
state->b[1] = w[1];
state->b[2] = v[2];
state->b[3] = v[3];
2021-04-04 20:05:31 +00:00
// Mark that we have consumed the buffer
state->length = 0;
}
void feed_hash(struct hashstate *state, unsigned char input[], size_t length) {
// Invariant: The buffer will be filled somewhere between 0 and 15
// when we enter this loop. This is because once it reaches 16, the
// hash compression function is executed.
2021-04-04 23:15:31 +00:00
for (size_t i = 0; i < length; i++) {
2021-04-04 20:05:31 +00:00
// Must not overflow the internat counter. In practice we will not
// hit this.
assert(state->totalbits <= UINT64_MAX - 8);
state->buffer[state->length++] = input[i];
state->totalbits += 8;
if (state->length == 16) {
compress_hash(state);
}
}
}
void finalize_hash(struct hashstate *state, unsigned char hash[32]) {
// Feed the padding. It consists of one-bit, followed by zero-bits,
// followed by the number of bits in the message as big-endian
// uint64. This is the same padding as in SHA-2.
// We can assume that this works due to the invariant that buffer
// fill when entering this function is between 0 and 15
state->buffer[state->length++] = 0x80;
while(state->length != 8) {
if (state->length == 16) {
compress_hash(state);
}
state->buffer[state->length++] = 0;
}
// Add the number of bits, and do one last compression
state->buffer[8] = state->totalbits >> 56;
state->buffer[9] = state->totalbits >> 48;
state->buffer[10] = state->totalbits >> 40;
state->buffer[11] = state->totalbits >> 32;
state->buffer[12] = state->totalbits >> 24;
state->buffer[13] = state->totalbits >> 16;
state->buffer[14] = state->totalbits >> 8;
state->buffer[15] = state->totalbits;
state->length += 8;
2021-04-04 20:05:31 +00:00
compress_hash(state);
// Extract the hash state
for (size_t i = 0; i < 4; i++) {
word2bytes(&hash[i*4], state->a[i]);
}
for (size_t i = 0; i < 4; i++) {
word2bytes(&hash[i*4 + 16], state->b[i]);
}
// Clear all of the hash state, in case there was sth important
// there
explicit_bzero(state, sizeof(struct hashstate));
2021-04-04 20:05:31 +00:00
}
2021-04-08 18:23:53 +00:00
void hmac(unsigned char output[32], unsigned char key[], size_t keylen, unsigned char message[], size_t messagelen) {
// The blocksize of the underlying has function is 128 bits (16B)
// but HMAC is specified assuming that the hash function output (in
// our case 256 bits or 32B) fits in one block. As far as I can
// tell extending the key to be two blocks long is not a problem.
unsigned char padded_key[32];
if (keylen > 16) {
// We hash it even if it is shorter than our extended key
// length to avoid giving attacker any funny surfaces to
// play with at the interface of two blocks
struct hashstate state;
initialize_hash(&state);
feed_hash(&state, key, keylen);
finalize_hash(&state, padded_key);
} else {
// Copy the key and zero-pad if necessary
memset(padded_key, 0, 32);
memcpy(padded_key, key, keylen);
}
// Outer and inner key derivation
unsigned char outer_key[32], inner_key[32];
for (size_t i = 0; i < 32; i++) {
outer_key[i] = padded_key[i] ^ 0x5c;
inner_key[i] = padded_key[i] ^ 0x36;
}
// Inner hash
unsigned char inner_hash[32];
struct hashstate state;
initialize_hash(&state);
feed_hash(&state, inner_key, 32);
feed_hash(&state, message, messagelen);
finalize_hash(&state, inner_hash);
// Outer hash
initialize_hash(&state);
feed_hash(&state, outer_key, 32);
feed_hash(&state, inner_hash, 32);
finalize_hash(&state, output);
}
2021-04-08 18:41:35 +00:00
unsigned char workbuf[8 * 1024 * 1024];
#define KDF_ROUNDS (sizeof(workbuf) / 32)
2021-04-08 19:45:08 +00:00
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
// in the end hashing them together in reverse order, instead of
// just xoring together.
//
// The memory-hardness of this scheme rests of the assumption that
// it is not feasible to compute the final hash backwards, that is,
// starting with the first hash and working towards the final hash.
// While I cannot prove this to be the case, the fact that our hash
// is made out of a one-way compression function makes me
// relatively confident in it.
// Place the hash of the salt at the top of the buffer. We do not
// include the counter i from PBKDF2 since we will ever only
// produce one block of output
size_t index = KDF_ROUNDS*32 - 32;
hmac(&workbuf[index], passphrase, passphraselen, salt, 32);
2021-04-08 19:45:08 +00:00
index -= 32;
// Walk back along the buffer, at each step hashing the previous
// hashes
while (index > 0) {
hmac(&workbuf[index], passphrase, passphraselen, &workbuf[index+32], 32);
2021-04-08 19:45:08 +00:00
index -= 32;
}
hmac(workbuf, passphrase, passphraselen, &workbuf[32], 32);
2021-04-08 19:45:08 +00:00
// Perform the final hash
unsigned char final_hash[32];
hmac(final_hash, passphrase, passphraselen, workbuf, KDF_ROUNDS * 32);
2021-04-08 19:45:08 +00:00
// Use first 128 bits of final hash as the key
memcpy(key, final_hash, 16);
// Empty the buffer
explicit_bzero(workbuf, sizeof(workbuf));
2021-04-08 19:45:08 +00:00
}
2021-04-08 21:37:10 +00:00
// 16 bit authentication tag
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) {
// CCM specifies that the length field is big endian while we are
// natively little endian. Flip it.
unsigned char length_bytes[4];
length_bytes[0] = length >> 24;
length_bytes[1] = length >> 16;
length_bytes[2] = length >> 8;
length_bytes[3] = length;
uint32_t be_length = bytes2word(length_bytes);
// First block is special
uint32_t mac_words[4] = {mprime<<3 | lprime, 0, messageindex, be_length};
xxtea128(key, mac_words);
// Process all full blocks
size_t index = 0;
for (; index + 16 <= length; index += 16) {
// Xor the plaintext block and previous encrypted block
uint32_t block[4];
block2words(block, &message[index]);
mac_words[0] ^= block[0];
mac_words[1] ^= block[1];
mac_words[2] ^= block[2];
mac_words[3] ^= block[3];
// Encrypt
xxtea128(key, mac_words);
}
if (index < length) {
// Pad with zeros to block width
unsigned char fullblock[16];
memset(fullblock, 0, 16);
memcpy(fullblock, &message[index], length - index);
// Xor the plaintext block and previous encrypted block
uint32_t block[4];
block2words(block, fullblock);
mac_words[0] ^= block[0];
mac_words[1] ^= block[1];
mac_words[2] ^= block[2];
mac_words[3] ^= block[3];
// Encrypt
xxtea128(key, mac_words);
}
words2block(mac, mac_words);
}
void ccm_xor_block(unsigned char block[16], uint32_t key[4], uint32_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];
counter_bytes[0] = counter >> 24;
counter_bytes[1] = counter >> 16;
counter_bytes[2] = counter >> 8;
counter_bytes[3] = counter;
uint32_t be_counter = bytes2word(counter_bytes);
uint32_t words[4] = {lprime, 0, messageindex, be_counter};
xxtea128(key, words);
unsigned char keystream[16];
words2block(keystream, words);
for (size_t i = 0; i < 16; i++) {
block[i] ^= keystream[i];
}
}
void ccm_encrypt(unsigned char key[16], uint32_t messageindex, unsigned char message[], uint32_t length, unsigned char mac[16]) {
uint32_t key_words[4];
block2words(key_words, key);
// Authenticate
ccm_mac(mac, key_words, messageindex, message, length);
// Encrypt
// MAC is xored with first block of keystream
ccm_xor_block(mac, key_words, messageindex, 0);
// 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++);
2021-04-08 21:37:10 +00:00
}
// 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);
}
2021-04-08 21:37:10 +00:00
}
bool ccm_decrypt(unsigned char key[16], uint32_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 full blocks
size_t index = 0;
uint32_t counter = 1;
for (; index + 16 <= length; index += 16) {
2021-04-08 21:37:10 +00:00
// Message blocks are numbered from index 1 onwards
ccm_xor_block(&message[index], key_words, messageindex, counter++);
2021-04-08 21:37:10 +00:00
}
// 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);
}
2021-04-08 21:37:10 +00:00
// Compute the expected authentication tag
unsigned char computed_mac[16];
ccm_mac(computed_mac, key_words, messageindex, message, length);
// Compare the expected and actual tag in constant time
unsigned char different = 0;
for (size_t i = 0; i < 16; i++) {
different |= computed_mac[i] ^ mac[i];
}
// Do the tags match?
if (different) {
// Nope. Wipe what we decrypted and return false
explicit_bzero(message, length);
return false;
}
// They do, return true
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
int tty = open("/dev/tty", O_RDWR);
if (tty == -1) {
perror("Failed to open controlling tty");
exit(1);
}
if (write(tty, prompt, strlen(prompt)) == -1) {
perror("Failed to write to terminal");
exit(1);
}
// Turn off echo
struct termios saved;
if (tcgetattr(tty, &saved) != 0) {
perror("tcgetattr");
exit(1);
}
struct termios altered;
altered = saved;
altered.c_lflag &= ~ECHO;
if (tcsetattr(tty, TCSANOW, &altered) != 0) {
perror("tcsetattr");
exit(1);
}
// Read until newline
size_t index = 0;
for (;;) {
if (index >= size) {
fprintf(stderr, "Passphrase too long, maximum size is %zu bytes\n", size - 1);
// Clean any line buffer
char tmp;
for (;;) {
if (read(tty, &tmp, 1) <= 0) {
break;
}
if (tmp == '\n') {
break;
}
}
tcsetattr(tty, TCSANOW, &saved);
exit(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);
} else if (bytes_read == 0) {
fprintf(stderr, "Unexpected EOF\n");
tcsetattr(tty, TCSANOW, &saved);
exit(1);
}
index += bytes_read;
if (passphrase[index-1] == '\n') {
// Got end of line
break;
}
}
// Write a newline (since the user's is not visible) and restore
// terminal settings
if (write(tty, "\n", 1) == -1) {
perror("Failed to write to terminal");
exit(1);
}
tcsetattr(tty, TCSANOW, &saved);
close(tty);
return index - 1;
}
2021-04-08 18:41:35 +00:00
int main(void) {
unsigned char salt[32];
if (getentropy(salt, 32) != 0) {
perror("getentropy");
}
unsigned char passphrase[128] = {0};
size_t passphrase_len = passphrase_prompt(passphrase, sizeof(passphrase), "passphrase: ");
unsigned char key[16];
kdf(key, salt, passphrase, passphrase_len);
2021-04-08 19:45:08 +00:00
for (size_t i = 0; i < 16; i++) {
printf("%02hhx ", key[i]);
}
2021-04-08 21:37:10 +00:00
printf("\n\n");
char *message = "Syökää parsaa ja palvokaa saatanaa";
unsigned char buf[64];
memset(buf, 0xab, sizeof(buf));
2021-04-08 21:37:10 +00:00
strcpy((char*)buf, message);
for (size_t i = 0; i < sizeof(buf); i++) {
printf("%02hhx ", buf[i]);
if (i % 16 == 15) printf("\n");
}
printf("\n");
2021-04-08 21:37:10 +00:00
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");
}
2021-04-08 19:45:08 +00:00
printf("\n");
2021-04-08 21:37:10 +00:00
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");
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");
}
2021-04-08 18:41:35 +00:00
}