Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Python implementation of Chris M. Thomasson's HMAC cipher

23 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: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;
};
________________________________________
0 new messages