5
\$\begingroup\$

It occurred to me that if SHA2 can be used to derive keys from passwords, then it might as well be good enough to generate random data that can be xored with a plaintext to encrypt and the other way around.

These are my assumptions:

  • SHA-512 produces a random-looking output that is impossible to guess
  • The result of SHA-512 can be fed back into it appended to a 256-bit key and produce an output with the same quality as given for a random input

If those assumptions hold, then it should provide privacy. It's much faster than AES-256-CBC.

At first I thought there must be something I'm missing, but no one has pointed out an attack that can be carried out against this construction. So what I would like to know is how secure this algorithm is and specific ways to break it.

Here's the code:

mad.h

#ifndef _MAD_H_
#define _MAD_H_

typedef struct {
  unsigned char state[64];
  unsigned char key[32];
} MadCtx;

void mad_ctx_init(MadCtx* mad, unsigned char const* key,
                  unsigned char const* iv);

void mad_encrypt(MadCtx* mad, unsigned char const* in, unsigned int in_size,
                 unsigned char* out); 

void mad_decrypt(MadCtx* mad, unsigned char const* in, unsigned int in_size,
                 unsigned char* out);

#endif

mad.c

#include "mad.h"
#include <stdint.h>
#include <assert.h>
#include <string.h>
#include <openssl/sha.h>


// Private
static void _xor64(uint64_t* dest, uint64_t const* a, uint64_t* b)
{
  for(int i = 0; i < 8; ++i)
    *dest++ = *a++ ^ *b++;
}

// Public
void mad_ctx_init(MadCtx* mad, unsigned char const* key,
                  unsigned char const* iv)
{
  memcpy(mad->state, iv, 64);
  memcpy(mad->key, key, 32);
}

void mad_encrypt(MadCtx* mad, unsigned char const* in, unsigned int in_size,
                 unsigned char* out)
{
  assert(0 == in_size % 64);

  int n = in_size >> 6; // in_size / 64
  while(n){
    uint64_t x[8];
    SHA512((unsigned char const*)mad, 96, (unsigned char*)x);
    _xor64((uint64_t*)out, (uint64_t const*)in, x);
    memcpy(mad->state, out, 64);
    in += 64;
    out += 64;
    --n;
  }
}

void mad_decrypt(MadCtx* mad, unsigned char const* in, unsigned int in_size,
                 unsigned char* out)
{
  assert(0 == in_size % 64);

  int n = in_size >> 6; // in_size / 64
  while(n){
    uint64_t x[8];
    SHA512((unsigned char const*)mad, 96, (unsigned char*)x);
    memcpy(mad->state, in, 64);
    _xor64((uint64_t*)out, (uint64_t const*)in, x);
    in += 64;
    out += 64;
    --n;
  }
}

and some test code

#include "mad.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void phex(void const* data, size_t size)
{
  char const* table = "0123456789abcdef";
  unsigned char const* in = data;
  while(size--){
    int c;
    c = table[*in >> 4];
    putchar(c);

    c = table[*in & 0xf];
    putchar(c);
    ++in;
  }
}

void read_or_die(FILE* file, void* dest, size_t size)
{
  if(size != fread(dest, 1, size, file)){
    perror("fread()");
    exit(EXIT_FAILURE);
  }
}

int main(int argc, char* argv[])
{
  FILE* urandom = fopen("/dev/urandom", "r");
  unsigned char iv[64];
  unsigned char key[32];
  unsigned char plain[128];
  unsigned char cipher[128];
  unsigned char decrypted[128];

  read_or_die(urandom, iv, 64);
  read_or_die(urandom, key, 32);
  memset(plain, 0xdd, 128);

  puts("plain text is:");
  for(int i = 0; i != 128; i += 32){
    phex(plain + i, 32);
    putchar('\n');
  }
  putchar('\n');

  MadCtx ctx;
  mad_ctx_init(&ctx, key, iv);
  mad_encrypt(&ctx, plain, 128, cipher);

  puts("cipher text is:");
  for(int i = 0; i != 128; i += 32){
    phex(cipher + i, 32);
    putchar('\n');
  }
  putchar('\n');

  assert(0 != memcmp(plain, cipher, 128));

  mad_ctx_init(&ctx, key, iv);
  mad_decrypt(&ctx, cipher, 128, decrypted);

  puts("decrypted text is:");
  for(int i = 0; i != 128; i += 32){
    phex(decrypted + i, 32);
    putchar('\n');
  }
  putchar('\n');

  assert(0 == memcmp(plain, decrypted, 128));
}
\$\endgroup\$
  • \$\begingroup\$ Does the decrypter know the 256 bit key and the 512 bit initial value already? \$\endgroup\$ – JS1 Sep 24 '16 at 21:55
  • \$\begingroup\$ @JS1 Yes, it's symmetric and requires the key and IV to be known when decrypting it. But the IV is usually stored with the file. So you normally only know the key and read the IV. \$\endgroup\$ – Douglas Sep 24 '16 at 23:32
  • 1
    \$\begingroup\$ It is possible to use SHA512 as block cipher for encryption in OFB mode. I don't see how it could be done in CBC mode. And I don't see what your SHA512 function is doing. Either way, AES is faster than SHA512, or even SHA256. AES also has its own instruction set on newer chips, this makes AES even faster. AES can also be used in different modes like CTR mode, this is often time required in real applications. I would suggest using SHA-2 for what SHA-2 is intended for, and using AES for what AES is intended for. \$\endgroup\$ – Barmak Shemirani Sep 26 '16 at 1:56
  • \$\begingroup\$ @BarmakShemirani It's feeding back the output as state to compute the next value that will be xored with the plaintext. I wrote it for fun and learning, but it's much faster than AES on my i7-2600. I get 160MB/s vs at most 121MB/s for aes-128 cbc. Link: imgur.com/UO3pQPB \$\endgroup\$ – Douglas Sep 26 '16 at 14:24
  • \$\begingroup\$ A high performance implementation of AES-128-CTR using the AES-NI instructions will run at about 1.33 cpb, or 2.5 GB/s. Without AES-NI it should cost about 11.5 cpb. SHA-512 hashes at about 11 cpb, but you need to double that for encryption, so you're at 22 cpb or 155 MB/s. (Single threaded, Sandy Bridge at 3.4 GHz, Turbo Boost disabled, costs taken from the hydra7 system on eBACS) So AES is almost twice as fast without using AES-NI and about 15x faster using AES-NI. \$\endgroup\$ – CodesInChaos Oct 26 '16 at 10:32
3
\$\begingroup\$

The above encrypt/decrypt functions can be summarized as follows:

void mad_encrypt(...)
{
  while(n)
  {
    SHA512(mad, 96, x);
    XOR(out, in, x);
    memcpy(mad->state, out, 64);
    ...
  }
}

void mad_decrypt(...)
{
  while(n)
  {
    SHA512(mad, 96, x);
    XOR(out, in, x);
    memcpy(mad->state, in, 64);
    ...
  }
}

This is basically the CFB mode. Note that the two functions are almost identical, except a small difference in memcpy.

AES in CFB mode is done in much the same way, except of course it uses AES block cipher instead of SHA512(...) function above. Also AES uses blocks of 16 bytes, so AES has to run 4 rounds to catch up with a single SHA512 round. Overall, AES is faster.

To compare the performance you can simply compare 4 rounds of AES to 1 round of SHA512. Watch out for compiler optimizations which may skew the result. Many tests have already been done for this, it shows AES is faster.

AES also uses key expansion which makes it more secure. Key expansion is relatively slow but it's done only once per file/data. This may skew the performance test depending on how the test is done.

typedef struct {
  unsigned char state[64];
  unsigned char key[32];
} MadCtx;

For improvement, don't let the key linger on in MadCtx::key. For example, you can use SHA512 to combine the key with IV (or state as you call it) so it becomes hidden during the operation, then you can drop key out of the structure.

Example:

unsigned char mad_IV[64];

void mad_init(const unsigned char *key, const unsigned char* iv)
{
    unsigned char buf[96];
    memcpy(buf, iv, 64);
    memcpy(buf + 64, key, 32);
    SHA512(buf, 96, mad_IV);
}

void mad_crypt(char const* in, int in_size, char* out, int encrypt)
{
    assert(0 == in_size % 64);

    int n = in_size >> 6; // in_size / 64
    while (n)
    {
        SHA512(mad_IV, 64, mad_IV);
        _xor64((uint64_t*)out, (uint64_t*)in, (uint64_t*)mad_IV);

        if (encrypt)
            memcpy(mad_IV, out, 64);
        else
            memcpy(mad_IV, in, 64);

        in += 64;
        out += 64;
        --n;
    }
}

int main()
{   
    unsigned char iv[64] = { 0 };
    unsigned char key[32] = { 0 };
    memcpy(iv, "iv", 2);
    memcpy(key, "key", 3);

    int size = 640;
    char *plaintext = malloc(size);
    char *decrypted = malloc(size);
    char *encrypted = malloc(size);

    memset(plaintext, 0, size);
    memset(encrypted, 0, size);
    memset(decrypted, 0, size);

    strcpy_s(plaintext, size, "plainxxxxxx.");

    mad_init(key, iv);
    mad_crypt(plaintext, size, encrypted, 1);

    mad_init(key, iv);
    mad_crypt(encrypted, size, decrypted, 0);

    phex(encrypted, 64);
    printf("\n\n");

    printf("plaintext: %s\n", plaintext);
    printf("decrypted: %s\n", decrypted);

    putchar('\n');

    return 0;
}
\$\endgroup\$
  • \$\begingroup\$ I used OpenSSL to measure performance: imgur.com/UO3pQPB and it very clearly shows that AES is slower. Since everyone keeps saying that SHA512 is slower, maybe there's something about the version I'm using or AES became much faster on newer computers? I don't have access to another machine now, maybe you would be kind enough to run on your PC and let me know the results? \$\endgroup\$ – Douglas Sep 27 '16 at 22:39
  • \$\begingroup\$ Note that one of the purposes of SHA2 is to calculate the file's hash. For this purpose SHA2 won't do a full round from SHA2_init to SHA2_update to SHA2_final. Therefore SHA2 can hash a file faster than AES can encrypt a file. I am not familiar with OpenSSL, it looks like you are running some automated test which measures hash versus encryption, apples and oranges... I tested SHA512(in, in_size, out) versus 4 * AES_encrypt(in, out, key), AES was about 3 times faster without any major optimization. \$\endgroup\$ – Barmak Shemirani Sep 28 '16 at 0:39
  • \$\begingroup\$ I benchmarked on a server and AES was indeed faster for blocksizes >= 1024 (imgur.com/cY9vAgR), so it's probably related to my system/hardware. What I was most interested in knowing though is if there are known ways of breaking the code. \$\endgroup\$ – Douglas Sep 28 '16 at 14:13
  • \$\begingroup\$ The mathematics of this is over my head. The obvious advantage is AES is faster, it has key expansion, it supports symmetric mode, it was designed for encryption and has been well studied in that regard. Perhaps you can ask crypto.stackexchange.com if there are some math related reasons that SHA-2 should not be used. \$\endgroup\$ – Barmak Shemirani Sep 28 '16 at 21:20
  • \$\begingroup\$ By the way, there are other issues such as known-plaintext attack, chosen-plaintext attack, etc. AES is designed to deal with these attacks. Using SHA2 for encryption may work for a simple case, but may fail in other cases. \$\endgroup\$ – Barmak Shemirani Sep 29 '16 at 0:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.