Python implementation of Chris M. Thomasson's HMAC cipher

22 views
Skip to first unread message

Leo

unread,
Nov 21, 2021, 3:08:57 PM11/21/21
to
Hi sci.crypt,

After seeing it in the newsgroup so many times, I wanted to write my
own implementation of this cipher. I haven't looked into the cipher in
too much detail, only enough to make our outputs match. Therefore I am
not endorsing its use nor am I denouncing it in any way.

The reference implementation that I tried to replicate the behavior of
is the JS version here [1].

From my tests, it is able to decrypt messages created by the JS
version, and encrypt messages that the JS version can then decrypt.

[1]: https://fractallife247.com/test/hmac_cipher/ver_0_0_0_1/

I'll be pasting the code below.

#!/usr/bin/env python3
import hmac
import os
import sys

# ct_hmac_cipher.py
# Python implementation of Chris M. Thomasson's HMAC cipher
# Copyright (C) Gokberk Yaltirakli, 2021

# The reference implementation can be found at
# https://fractallife247.com/test/hmac_cipher/ver_0_0_0_1/.

# This is an implementation of Chris M. Thosmasson's HMAC cipher. The
# Chris' JS implementation and this Python implementation can encrypt
# and decrypt the same data. Their inputs and outputs are compatible
# as of 2021-11-21.

# These are the algorithm parameters. They can be changed by
# command-line arguments. The reference implenmentation in JS only
# supports two rounds.

CONFIG = {}
CONFIG["algo"] = "sha256"
CONFIG["hex_input"] = "false"
CONFIG["hex_output"] = "false"
CONFIG["key"] = "Password"
CONFIG["rand_n"] = "64"
CONFIG["rounds"] = "2"
CONFIG["verbose"] = "false"


def log(*a):
if CONFIG["verbose"] != "true":
# Do not write logs if we are not in verbose mode
return
sys.stdout.buffer.write("".join(map(str, a)).encode("utf-8"))
sys.stdout.buffer.write(b"\n")


def get_hmac_function(algo):
functions = {}
functions["sha256"] = lambda x, y: hmac.digest(x, y, "sha256")
functions["sha512"] = lambda x, y: hmac.digest(x, y, "sha512")

return functions[algo]


def ct_hmac_round(buf, decrypt):
hm = get_hmac_function(CONFIG["algo"])
key = CONFIG["key"].encode("utf-8")
hmac_buf = bytes(key[::-1])

res = b""

p_i = 0
while p_i < len(buf):
digest = hm(key, hmac_buf)
log(" Digest: ", digest.hex())
for x in digest:
cip = buf[p_i] ^ x

res += bytes([cip])

if decrypt:
hmac_buf += bytes([cip, buf[p_i]])
else:
hmac_buf += bytes([buf[p_i], cip])

p_i += 1
if p_i == len(buf):
break
return res


def ct_hmac_cipher(buf, decrypt):
if not decrypt:
# We are encrypting a plaintext. Prepend a random buffer in
# front of the plaintext.
rand = os.urandom(int(CONFIG["rand_n"]))
buf = rand + buf

rounds = int(CONFIG["rounds"])
for rnd in range(rounds):
log(f"Round {rnd}")
log(" Before: ", buf.hex())
buf = ct_hmac_round(buf, decrypt)
if rnd != rounds - 1:
buf = buf[::-1] # Reverse the buffer
log(" After: ", buf.hex())

if decrypt:
buf = buf[int(CONFIG["rand_n"]) :]

return buf


def run_cipher(decrypt):
input_buf = sys.stdin.buffer.read()
if CONFIG["hex_input"] == "true":
hex_input = input_buf.replace(b"\n", b"")
hex_input = hex_input.decode("ascii")
input_buf = bytes.fromhex(hex_input)

log(f"Input buffer is {len(input_buf)} bytes")
out_buf = ct_hmac_cipher(input_buf, decrypt)
log(f"Output buffer is {len(out_buf)} bytes")

if CONFIG["hex_output"] == "true":
for i, c in enumerate(out_buf.hex()):
sys.stdout.buffer.write(c.encode("ascii"))

if i % 70 == 69:
sys.stdout.buffer.write(b"\n")
sys.stdout.buffer.write(b"\n")
else:
sys.stdout.buffer.write(out_buf)


def action_help():
print("Usage\n")

print(f"{NAME} encrypt key <key>")
print(" Encrypt stdin with the key <key>.")
print()

print(f"{NAME} decrypt key <key>")
print(" Decrypt stdin with the key <key>.")
print()

print("This is all the help you get, read the code for more")


def action_encrypt():
log("Running cipher in encryption mode")
run_cipher(False)


def action_decrypt():
log("Running cipher in decryption mode")
run_cipher(True)


def main():
globals()["NAME"] = sys.argv.pop(0)

try:
action = sys.argv.pop(0)
except:
action = "help"

while sys.argv:
name = sys.argv.pop(0)
val = sys.argv.pop(0)
CONFIG[name] = val

fname = f"action_{action}"
if fname in globals():
fn = globals()[f"action_{action}"]
fn()
else:
print(f"Unknown command: {action}")


if __name__ == "__main__":
main()

--
Leo

Leo

unread,
Nov 21, 2021, 3:27:21 PM11/21/21
to
Ah, it looks like there was already an implementation in Python, but this
one
might be easier to drop in shell scripts. And the more implementations,
the
merrier.

--
Leo

Chris M. Thomasson

unread,
Nov 22, 2021, 2:25:37 AM11/22/21
to
On 11/21/2021 12:08 PM, Leo wrote:
> Hi sci.crypt,
>
> After seeing it in the newsgroup so many times, I wanted to write my
> own implementation of this cipher. I haven't looked into the cipher in
> too much detail, only enough to make our outputs match. Therefore I am
> not endorsing its use nor am I denouncing it in any way.
>
> The reference implementation that I tried to replicate the behavior of
> is the JS version here [1].
>
> From my tests, it is able to decrypt messages created by the JS
> version, and encrypt messages that the JS version can then decrypt.
>
> [1]: https://fractallife247.com/test/hmac_cipher/ver_0_0_0_1/
>
> I'll be pasting the code below.
>
> #!/usr/bin/env python3
[snip nice code]

The payload of this message was generated by your implementation of my
experimental HMAC cipher using the default key:

http://fractallife247.com/test/hmac_cipher/ver_0_0_0_1?ct_hmac_cipher=ee0dff078cbf51796dc8e651f65922f1d501e521141fe69647cc50b2f1c410ddd6d945207d7122631ca5ddef59865096c8848806712d19f90206646763c411eff5880966aac5bf7c7c613ec4fd1414274ea2a54a306279e8eceafec2e25620bb11c5e99c4494bbb2795493fd85f5abb04dd1bbd42566b62658b88c32f20139d5f5b505362d1b6bc5484de7ff4a129954fe1c394007519446a66c5ec10cbc4da770ca1846269a405dc3cd437a24ca347659dc9cc4d78f8c2fa41c80d4bddee5460cb6dc6b21cbcc1ac8f82b7f6b57061d3d27b9e6d121890ac896f1260bffb3e29ffb6944ab28f9181285ee954e7670ed02cddea27a1f6e3633271255ea7a228c0faea2386a088d2ed62ab832c7bab4ee5d7dea4d22d7bf336ec598a614b838858bb2b7a026a97da13e8a95fe4ccc552013ce2f39ede92740793f0f984ebb0798f76645d3a7499beb6e22e239316f9d5a9a47c0dccbabeccd37af084fd95f6a994c4461be8e33df88db630d13185189cb76ce11f14fc9509db39369852d05c2d3ea591c5dd98c7408fa3702bc0ecbb6edee7f63e9474da7750513b9147d6b7a6e7cb211fd79000c3059bf33e613a6c4decb7cde486eaa1f6e6bc7d886cd10a6520562b64cc89021a33ce55022eb6efc07997b9cdea6618f8f98831e2bc9bd133084c7b9d551ab




Chris M. Thomasson

unread,
Nov 22, 2021, 2:32:54 AM11/22/21
to

Chris M. Thomasson

unread,
Nov 22, 2021, 2:39:21 AM11/22/21
to

Leo

unread,
Nov 22, 2021, 3:16:38 AM11/22/21
to
On Sun, 21 Nov 2021 23:39:14 -0800, Chris M. Thomasson wrote:

> On 11/21/2021 11:25 PM, Chris M. Thomasson wrote:
>> The payload of this message was generated by your implementation of my
>> experimental HMAC cipher using the default key:
>>
>> [snip link]
>>
> Using algo:sha512 alone:
>
> [snip link]
>

Glad it's working for you so far. Just curious, are you changing the
parameters in the code or passing them in the command-line arguments?

Also I had to guess how you would handle multiple rounds (I thought
you would reverse every round except the last). If that's not what you
had in mind I can change that part so nothing breaks in the future if
you increase the JS version's round count in the future.

--
Leo

Chris M. Thomasson

unread,
Nov 22, 2021, 3:38:46 AM11/22/21
to
On 11/22/2021 12:16 AM, Leo wrote:
> On Sun, 21 Nov 2021 23:39:14 -0800, Chris M. Thomasson wrote:
>
>> On 11/21/2021 11:25 PM, Chris M. Thomasson wrote:
>>> The payload of this message was generated by your implementation of my
>>> experimental HMAC cipher using the default key:
>>>
>>> [snip link]
>>>
>> Using algo:sha512 alone:
>>
>> [snip link]
>>
>
> Glad it's working for you so far. Just curious, are you changing the
> parameters in the code or passing them in the command-line arguments?

Passing them in at the command line. For instance here is some output.
On the windozer 11 right now. YIKES! anyway:
________________________________________________
python ct_hmac_python.py encrypt key CTHMACCpiher algo sha512 verbose
true < plaintext.txt
Running cipher in encryption mode
Input buffer is 25 bytes
Round 0
Before:
120a2ead867fd2e383e547bb4a1f0029fcbc5b82e02ea8d13c32409823d21e477bbe4d1452a5de1f4fc062b2cb38a6cad0a4ee64ecf083bbfa45ddd02ecfe4dd43616e20796f75207265616420746869733f0d0a0d0a3b5e29
Digest:
0d6090ea8f0d2ce921156c2e8f24657aa676989ba4a724ea68e51952ee49a17da23053acebe3f6a6051beecff3ebd64eed2fb3c650a01bbdc768dceb06260b7d
Digest:
d452dbd4decf44a010a0174dc1afc50cd571ce79a4a4cfdb4a201d53823ec96f274cd4b9b8f3d159890542aa49abb26b59bd8d1ffc53b55816034ab6b712816c
After:
6385f4aea973c34ea665addbe12976c5628031a0a7f4b53397a0efe9283b012d3d069850bca25d8b3d8470d3387d8cdb4ab92846b9b81e8ed93abf9bcdca59d7543b8c894419c3ca5a53653bc5952bf0a20afe720947be6a1f
Round 1
Before:
6385f4aea973c34ea665addbe12976c5628031a0a7f4b53397a0efe9283b012d3d069850bca25d8b3d8470d3387d8cdb4ab92846b9b81e8ed93abf9bcdca59d7543b8c894419c3ca5a53653bc5952bf0a20afe720947be6a1f
Digest:
0d6090ea8f0d2ce921156c2e8f24657aa676989ba4a724ea68e51952ee49a17da23053acebe3f6a6051beecff3ebd64eed2fb3c650a01bbdc768dceb06260b7d
Digest:
1ef94b5b596db7ba4804eed3ac12b839e11fad579e36f78306bfd59a6e2168b5d9f70988164d632cf3ba932ee95f6f5f9196161c9f09c7960dfed251e24b8ca7
After:
6ee56444267eefa78770c1f56e0d13bfc4f6a93b035391d9ff45f6bbc672a0509f36cbfc5741ab2d389f9e1ccb965a95a7969b80e91805331e526370cbec52aa4ac2c7d21d74747012578be8698793c943155325977149e919
Output buffer is 89 bytes
‼����;♥S���E���r�P�6��WA�-8��∟˖Z������↑♣3▲Rcp��R�J���↔ttp↕W��i���C§S%�qI�↓
________________________________________________

Your impl works great:

http://fractallife247.com/test/hmac_cipher/ver_0_0_0_1?ct_hmac_cipher=6ee56444267eefa78770c1f56e0d13bfc4f6a93b035391d9ff45f6bbc672a0509f36cbfc5741ab2d389f9e1ccb965a95a7969b80e91805331e526370cbec52aa4ac2c7d21d74747012578be8698793c943155325977149e919

I accidentally misspelled Cipher in the key:CTHMACCpiher. The password
is the misspelled one. Sorry. ;^)

Can you decrypt it? The key is in the cmd line.


> Also I had to guess how you would handle multiple rounds (I thought
> you would reverse every round except the last). If that's not what you
> had in mind I can change that part so nothing breaks in the future if
> you increase the JS version's round count in the future.

Its been a while since I experimented with multiple rounds. Iirc, I was
reversing on every round. I need at least two because it allows me to
gain full blown bit sensitivity across an entire plaintext. If one bit
of the plaintext is altered, one will get a radically different
ciphertext. Visa versa wrt the ciphertext. I need 2 rounds here to do
that. The reverse in between them is ESSENTIAL to this effect!

Iirc, the "pattern" I used for n rounds was:

round_0, reverse, round_1
round_1, reverse, round_2
round_3, reverse, round_4
round_n, reverse, round_n+1

...

Chris M. Thomasson

unread,
Nov 22, 2021, 3:43:27 AM11/22/21
to
On 11/22/2021 12:38 AM, Chris M. Thomasson wrote:
> On 11/22/2021 12:16 AM, Leo wrote:
>> On Sun, 21 Nov 2021 23:39:14 -0800, Chris M. Thomasson wrote:
>>
>>> On 11/21/2021 11:25 PM, Chris M. Thomasson wrote:
>>>> The payload of this message was generated by your implementation of my
>>>> experimental HMAC cipher using the default key:
>>>>
>>>> [snip link]
>>>>
>>> Using algo:sha512 alone:
>>>
>>> [snip link]
>>>
>>
>> Glad it's working for you so far. Just curious, are you changing the
>> parameters in the code or passing them in the command-line arguments?
>
> Passing them in at the command line. For instance here is some output.
> On the windozer 11 right now. YIKES! anyway:
> ________________________________________________
> python ct_hmac_python.py encrypt key CTHMACCpiher algo sha512 verbose
[...]
> Its been a while since I experimented with multiple rounds. Iirc, I was
> reversing on every round. I need at least two because it allows me to
> gain full blown bit sensitivity across an entire plaintext. If one bit
> of the plaintext is altered, one will get a radically different
> ciphertext. Visa versa wrt the ciphertext. I need 2 rounds here to do
> that. The reverse in between them is ESSENTIAL to this effect!
>
> Iirc, the "pattern" I used for n rounds was:
>
> round_0, reverse, round_1
> round_1, reverse, round_2
> round_3, reverse, round_4
> round_n, reverse, round_n+1
>
> ...

Actually, now that I think about it... Humm... Perhaps it should be:

round_0: crypt() reverse() crypt()
round_1: crypt() reverse() crypt()
round_2: crypt() reverse() crypt()
round_3: crypt() reverse() crypt()
round_n: crypt() reverse() crypt()
...


Humm.... Thanks for the thoughts Leo!

:^)

Chris M. Thomasson

unread,
Nov 22, 2021, 4:01:22 AM11/22/21
to
On 11/22/2021 12:43 AM, Chris M. Thomasson wrote:
> On 11/22/2021 12:38 AM, Chris M. Thomasson wrote:
>> On 11/22/2021 12:16 AM, Leo wrote:
>>> On Sun, 21 Nov 2021 23:39:14 -0800, Chris M. Thomasson wrote:
[...]

Sorry for the msg flood, but IIRC here is how I was doing it. This is a
crudely mutated crypt function that runs two extra rounds:
________________________________________
ct_hmac_cipher.prototype.crypt = function (data, M) {
var P = ct_string_to_codes(data);

if (M == false) {
var R = ct_rand_bytes(this.m_skey.m_rand_n);
P = R.concat(P);
}

// 0
ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 0...\n");
var C = this.crypt_round(P, M);
C.reverse();
ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 1...\n");
C = this.crypt_round(C, M);

// 1
ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 2...\n");
var C_1 = this.crypt_round(C, M);
C_1.reverse();
ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 3...\n");
C = this.crypt_round(C_1, M);

if (M == true) {
C.splice(0, this.m_skey.m_rand_n);
}

return C;
};
________________________________________


Here is the original function on the site:
________________________________________
ct_hmac_cipher.prototype.crypt = function (data, M) {
var P = ct_string_to_codes(data);

if (M == false) {
var R = ct_rand_bytes(this.m_skey.m_rand_n);
P = R.concat(P);
}

ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 0...\n");
var C = this.crypt_round(P, M);
C.reverse();
ct_gui_parse_append_element_to_string("g_ct_html_input_digests",
"Round 1...\n");
C = this.crypt_round(C, M);

if (M == true) {
C.splice(0, this.m_skey.m_rand_n);
}

return C;
};
________________________________________
Reply all
Reply to author
Forward
0 new messages