Experimental HMAC Cipher

By: Chris M. Thomasson

Pre-Alpha Version (0.0.0)

Prerequisites

This document assumes the reader is familar with how HMAC works.

Now, keep in mind that my algorithm relies on a quirky implementation detail that does not alter the internal state when we take a digest. In other words, my code needs the ability to take a digest without destroying its internal state: This is not a traditional HMAC!

In order to get my algorithm working, an HMAC can copy its internal state, then perform the digest operation on said copy. So, to clear up... Taking a digest should not mutate the state of the HMAC. This is how the quirky Python HMAC implementation works, and unfortunately my algorithm uses that.

1. Introduction

The cipher uses propagation of HMAC digest bytes to achieve a radical diffusion of a plaintext within its ciphertext, and vise versa. It has the following basic properties:

1: Every encryption of the same plaintext creates a radically different ciphertext.

2: If a single bit of a ciphertext is altered, it decrypts into a radically different plaintext.

2. The Secret Key

Both properties of my cipher rely on a good cryptographic hash function. HMAC is an abstract layer on top of a hash algorithm that allows for a secret key. Let us refer to this HMAC key as SK.hmac_key from now on. We also need to allow Alice and Bob to choose a hash to use with HMAC. Let us refer to this as SK.hash_algo. The size of HMAC's digest in bytes is based on the digest size of SK.hash_algo. For example, SHA-256 has 32-byte digests.

Another aspect involves prepending random bytes from a TRNG to the front of a plaintext. Let us refer to the number of these random bytes as SK.rand_n. Okay, the secret key SK used by Alice and Bob is comprised of:
_________________________________
SK.hmac_key = Key for HMAC.
SK.hash_algo = The Hash Algorithm.
SK.rand_n = The Number of TRNG bytes.
_________________________________
SK.hmac_key must be a cryptographically secure password, e.g. comprised of 1024 bytes from a TRNG.

SK.hash_algo must be a cryptographically secure hash, e.g. SHA-384.

SK.rand_n must be equal to, or ideally larger than the digest size of SK.hash_algo.

3. Pseudo-Code

Brief Description of the pseudo-code...

3.1 The CRYPT_ROUND Function

The CRYPT_ROUND function encrypts or decrypts a plaintext P. It takes two parameters P and M, where M is the mode of operation. It returns its final result as C which is either the decrypted or encrypted bytes wrt the value of M. In the pseudo-code for the CRYPT function:

P = The plaintext or ciphertext bytes.
M = ENCRYPT means encrypt P; DECRYPT means decrypt P.
C = The return value based on M.

C = CRYPT_ROUND(P, M):
_________________________________

1: Create an HMAC session H using SK.hmac_key for its key, and SK.hash_algo for its underlying hash algorithm.

2: Update H with the bytes of SK.hmac_key in reverse order.

3: Iterate through all the bytes in P:

Set I_P is used as an index for P, set it to zero.

WHILE (I_P is less than the size in bytes of P)
{

3.1: Obtain a raw hash digest D from H.

3.2: Iterate through all the bytes in D.

Set I_D is used as an index for D, set it to zero.

WHILE (I_P is less than the size in bytes of P AND I_D is less than the size in bytes of D)
{

3.2.1: Set the byte C[I_P] to P[I_P] xor'ed with D[I_D].

3.2.2: Follow the mode M, encrypt or decrypt:

IF (M is equal to ENCRYPT)
{

3.2.2.1: Update H using the byte P[I_P].

3.2.2.2: Update H using the byte C[I_P].

}

ELSE
{

3.2.2.3: Update H using the byte C[I_P].

3.2.2.4: Update H using the byte P[I_P].

}

3.2.3: Increment I_D by one.

3.2.4: Increment I_P by one.

}
}

4. Return C to the caller.

_________________________________

3.2 The CRYPT Function

The CRYPT function encrypts or decrypts a plaintext P. It takes two parameters P and M, where M is the mode of operation. It returns its final result as C which is either the decrypted or encrypted bytes wrt the value of M. In the pseudo-code for the CRYPT function:

P = The plaintext or ciphertext bytes.
M = ENCRYPT means encrypt P; DECRYPT means decrypt P.
C = The return value based on M.

C = CRYPT(P, M):
_________________________________
1: If the mode M is for encryption:

IF (M is equal to ENCRYPT)
{

1.1: Create SK.rand_n number of bytes R from a TRNG.

1.2: Prepend R to P.

}

2: Call CRYPT_ROUND(P, M) using P as the first parameter and M as the second parameter, and set its result to C.

3: Set C_1 to the bytes of C in reverse order.

4: Call CRYPT_ROUND(C_1, M) using C_1 as the first parameter and M as the second parameter, and set its result to C.

5: If the mode M is for decryption:

IF (M is equal to DECRYPT)
{
5.1: Remove SK.rand_n number of bytes from the front of C making it SK.rand_n bytes smaller.
}

6: Return C to the caller.

_________________________________

4. Description

A description of the pseudo-code in section 3 on a step-by-step basis.

4.1 The CRYPT_ROUND Function

Steps CRYPT_ROUND.(1 and 2) Create a normal HMAC session called H using SK.hmac_key as its secret key _AND_ make it use SK.hash_algo as the underlying HMAC hash algorithm. Update H with the bytes of SK.hmac_key in reverse order. SK.hmac_key should be a crypto secure key, preferably larger than 1024 bytes of data generated from a TRNG. SK.hash_algo needs to be a crypto secure hash algorithm, perhaps SHA-384 with its 48-byte digests. Keep in mind that SK.rand_n needs to be at least as large as 48 bytes in this case of the digest size reaped from SHA-384. SK.rand_n needs to be equal to, or preferably larger than the digest size of SK.hash_algo. Therefore, I recommend using a larger SK.rand_n, perhaps something like 79 in this case. These two steps create our HMAC "starting place" for either encryption or decryption depending on the mode M.

4.2 The CRYPT Function

Pending...

5. Appendix

This will show actual real implementations and various test vectors.

Python Implementation

Here is some example Python source code of the pseudo-code in section 3.


# Chris M. Thomasson Copyright 2018 (c)
# Experimental HMAC Cipher
#____________________________________________________________


# Our external libs
#____________________________________________________________
import random;
import hashlib;
import hmac;


# Some Utilities
#____________________________________________________________
def ct_bytes_to_hex(origin, offset):
    hex = "";
    n = len(origin);
    t = "0123456789ABCDEF";
    for i in range(offset, n):
        c = ord(origin[i]);
        nibl = c & 0x0F;
        nibh = (c & 0xF0) >> 4;
        hex = hex + t[nibh];
        hex = hex + t[nibl];
        hex = hex + " ";
        if (not ((i + 1) % 16) and i != n - 1): 
            hex = hex + "\r\n";
    return hex;


# Generate n random bytes
# These need should ideally be from a truly random, non-repeatable 
# source. TRNG!
def ct_rand_bytes(n):
    rb = "";
    for i in range(n):
        rb = rb + chr(random.randint(0, 255));
    return rb;


# The Secret Key
# Contains all the parts of the secret key
#____________________________________________________________
class ct_secret_key:
    def __init__(self, hmac_key, hash_algo, rand_n):
        self.hmac_key = hmac_key;
        self.hash_algo = hash_algo;
        self.rand_n = rand_n;
 
    def __repr__(self):
        return "hmac_key:%s\nhash_algo:%s\nrand_n:%s" % (ct_bytes_to_hex(self.hmac_key, 0), self.hash_algo, self.rand_n);
 
    def __str__(self): return self.__repr__();


# The Ciphertext or Plaintext
# It holds the bytes of a ciphertext or a plaintext
#____________________________________________________________
class ct_bin:
    def __init__(self, ctxt):
        self.bytes = ctxt;
    def __repr__(self):
        return "%s" % (ct_bytes_to_hex(self.bytes, 0));
 
    def __str__(self): return self.__repr__();


# The Crypt Round Function
#____________________________________________________________
def ct_crypt_round(SK, P, M):
    H = hmac.new(SK.hmac_key.encode(), None, SK.hash_algo);
    H.update(SK.hmac_key[::-1].encode());
    C = "";
    I_P = 0;
    I_P_N = len(P.bytes);
    while (I_P < I_P_N):
        D = H.digest();
        I_D = 0;
        I_D_N = len(D);
        while (I_P < I_P_N and I_D < I_D_N):
            C_I_P = ord(P.bytes[I_P]) ^ D[I_D];
            C = C + chr(C_I_P);
            if (M == False):
                H.update(P.bytes[I_P].encode());
                H.update(chr(C_I_P).encode());
            else:
                H.update(chr(C_I_P).encode());
                H.update(P.bytes[I_P].encode());
            I_P = I_P + 1;
            I_D = I_D + 1;
    return ct_bin(C);


# The Crypt Function
#____________________________________________________________
def ct_crypt(SK, P, M):
    if (M == False):
        R = ct_rand_bytes(SK.rand_n);
        P.bytes = R + P.bytes;
    C = ct_crypt_round(SK, P, M);
    C_1 = ct_bin(C.bytes[::-1]);
    C = ct_crypt_round(SK, C_1, M);
    if (M == True):
        size = len(C.bytes) - SK.rand_n;
        C.bytes = C.bytes[SK.rand_n : SK.rand_n + size];
    return C;


# The Main Program
#____________________________________________________________

# Alice and Bob's Secret Key
#____________________
SK = ct_secret_key(
    "This is the HMAC Key. It should be a crypto secure key! Damn it.",
    hashlib.sha384, # The hash function. It should be a crypto secure hash.
    73 # The number of bytes. The should be generated by a TRNG
);
print("%s" % (SK));

# Alice's Plaintext
#____________________
Original_Plaintext = "ABCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDE";
A_P = ct_bin(Original_Plaintext);
print(
    "\n\nAlice's Plaintext Bytes:"
    "\n____________________\n%s\n" % (A_P)
);

# Encrypt
#____________________
C = ct_crypt(SK, A_P, False);
print(
    "\n\nCiphertext Bytes:"
    "\n____________________\n%s\n" % (C)
);

# Decrypt
#____________________
B_P = ct_crypt(SK, C, True);
print(
    "\n\nBob's Ciphertext Bytes:"
    "\n____________________\n%s\n" % (B_P)
);


if (B_P.bytes != Original_Plaintext):
  print("DATA CORRUPTED!");