Leo
2024-02-01 21:58:23 UTC
Hey sci.crypt,
I wanted an encrypted and authenticated "secret box" like thing. Instead
of the usual IV + encrypted blob + MAC combination, I wanted to explore
the problem space and have a little fun.
I used an unbalanced Feistel network, with one part being a single byte.
For this, I needed a round function that can take the rest of the block
along with a key, and reduce it into a single byte. I go through the
entire block and perform N rounds of Feistel for an N-byte block.
A standard Merkle-Damgard hash works well for this, but there are a ton of
options that can work for this purpose. Perhaps something to explore in
the future.
"Regular" solutions are really malleable. Depending on the block cipher
mode, it is quite easy to modify the ciphertext to affect the plaintext in
predictable ways. This goes from flipping bits while only breaking a
single block all the way to being able to make arbitrary modifications as
long as you know the plaintext. This causes MACs to have a massive
importance.
With this mode, any change to the ciphertext affects the entire plaintext
in a random way. It sort of An ASCII message ceases to be meaningful text,
a JSON message ceases to be valid JSON etc. Depending on the security
target, a MAC can be completely left out. Or a fixed string somewhere can
be used in place of a MAC, like every valid block starting/ending with
0xCAFEBABE to have the same effect as a 32-bit MAC.
Have any of you used a similar construction anywhere? I'd be curious to
know what you think, I had a lot of fun playing around with this, and it
even ended up having some convenient properties.
I'm putting a small example in Python with SHA-256 as the round function.
import hashlib
def sha256(x: bytes) -> int: return hashlib.sha256(x).digest()[0]
def encrypt(buf: bytes, key: bytes) -> bytes:
key = len(key).to_bytes(8, 'little') + key
for _ in range(len(buf)):
left, right = buf[0], buf[1:]
new_right = sha256(key + right) ^ left
buf = right + bytes([new_right])
return buf
def decrypt(buf: bytes, key: bytes) -> bytes:
key = len(key).to_bytes(8, 'little') + key
for _ in range(len(buf)):
left, right = buf[:-1], buf[-1]
new_left = sha256(key + left) ^ right
buf = bytes([new_left]) + left
return buf
while True:
action = input("'encrypt' or 'decrypt'? ")
if action not in ('encrypt', 'decrypt'): continue
key = input("key: ").encode("utf-8")
buf = input("data: ")
if action == 'encrypt':
buf = buf.encode("utf-8")
print(encrypt(buf, key).hex())
elif action == 'decrypt':
buf = bytes.fromhex(buf)
print(decrypt(buf, key))
I wanted an encrypted and authenticated "secret box" like thing. Instead
of the usual IV + encrypted blob + MAC combination, I wanted to explore
the problem space and have a little fun.
I used an unbalanced Feistel network, with one part being a single byte.
For this, I needed a round function that can take the rest of the block
along with a key, and reduce it into a single byte. I go through the
entire block and perform N rounds of Feistel for an N-byte block.
A standard Merkle-Damgard hash works well for this, but there are a ton of
options that can work for this purpose. Perhaps something to explore in
the future.
"Regular" solutions are really malleable. Depending on the block cipher
mode, it is quite easy to modify the ciphertext to affect the plaintext in
predictable ways. This goes from flipping bits while only breaking a
single block all the way to being able to make arbitrary modifications as
long as you know the plaintext. This causes MACs to have a massive
importance.
With this mode, any change to the ciphertext affects the entire plaintext
in a random way. It sort of An ASCII message ceases to be meaningful text,
a JSON message ceases to be valid JSON etc. Depending on the security
target, a MAC can be completely left out. Or a fixed string somewhere can
be used in place of a MAC, like every valid block starting/ending with
0xCAFEBABE to have the same effect as a 32-bit MAC.
Have any of you used a similar construction anywhere? I'd be curious to
know what you think, I had a lot of fun playing around with this, and it
even ended up having some convenient properties.
I'm putting a small example in Python with SHA-256 as the round function.
import hashlib
def sha256(x: bytes) -> int: return hashlib.sha256(x).digest()[0]
def encrypt(buf: bytes, key: bytes) -> bytes:
key = len(key).to_bytes(8, 'little') + key
for _ in range(len(buf)):
left, right = buf[0], buf[1:]
new_right = sha256(key + right) ^ left
buf = right + bytes([new_right])
return buf
def decrypt(buf: bytes, key: bytes) -> bytes:
key = len(key).to_bytes(8, 'little') + key
for _ in range(len(buf)):
left, right = buf[:-1], buf[-1]
new_left = sha256(key + left) ^ right
buf = bytes([new_left]) + left
return buf
while True:
action = input("'encrypt' or 'decrypt'? ")
if action not in ('encrypt', 'decrypt'): continue
key = input("key: ").encode("utf-8")
buf = input("data: ")
if action == 'encrypt':
buf = buf.encode("utf-8")
print(encrypt(buf, key).hex())
elif action == 'decrypt':
buf = bytes.fromhex(buf)
print(decrypt(buf, key))
--
Leo
Leo