This idea is inspired by a recent question about sending 32 byte messages, encrypted, without a MAC but still achieving the equivalent protection of a MAC just by using a block cipher, extra invocations of this block cipher and extra bandwidth (for example, since message is small and sending any number of bytes up to a single IP packet is "free").
Professional cryptographers, given AES and wanting AE (or AEAD), would create CBC-MAC, OMAC, CMAC, CCM, EAX, etc.
But is there a simpler way that would work, for certain values of "work"?
I present an impractical block cipher mode.
There is no IV or nonce.
Start with an AES key and a 64bit counter initiated at 0.
Assume cleartext message is evenly divisible by 8.
Break message into 8 byte pieces.
For each 8 byte piece of the cleartext input message:
- Create a block from 8 bytes of the input and 8 bytes of counter.
- Encrypt the block using AES.
- Increment the counter.
- Send the encrypted block.
- Repeat for the next 8 bytes of input.
Obviously this scheme needs rekeying after 2^64 blocks or 2^67 bytes sent.
Obviously padding can be added for messages that are not evenly divisible by 8.
Obviously if message size is not a constant known in advance, length of the message has to be sent too.
But just for sending 32 byte messages, and no more than 2^67 bytes with a single key, is this secure?
I thought of three things:
Does it encrypt same cleartext to same ciphertext like ECB? No.
Can the attacker remove, duplicate or reorder blocks without the decrypter noticing? No.
Can the attacker flip bits in the ciphertext without the decrypter noticing? I think no.
What am I missing? Is this "secure" but silly when CCM and EAX (and GCM, and Poly1305, and HMAC) exist?
Meta question: is there a tag for "recreational cryptography", i.e. something no one will ever use? Or is that against the rules?
Implementation in python3:
import binascii import struct from hmac import compare_digest from Crypto.Cipher import AES class Encrypter: def __init__(self, key): assert type(key) == bytes assert len(key) == 16 self.cipher = AES.new(key, AES.MODE_ECB) self.counter = 0 def encrypt(self, cleartext): assert type(cleartext) == bytes len_cleartext = len(cleartext) assert (len_cleartext % 8) == 0 ciphertext = b'' while len(cleartext) > 0: piece_8_bytes = cleartext[:8] cleartext = cleartext[8:] counter_8_bytes = struct.pack('<Q', self.counter) cleartext_block = piece_8_bytes + counter_8_bytes self.counter += 1 ciphertext_block = self.cipher.encrypt(cleartext_block) ciphertext += ciphertext_block assert len(ciphertext) == len_cleartext * 2 return ciphertext class Decrypter: def __init__(self, key): assert type(key) == bytes assert len(key) == 16 self.cipher = AES.new(key, AES.MODE_ECB) self.counter = 0 def decrypt(self, ciphertext): assert type(ciphertext) == bytes len_ciphertext = len(ciphertext) assert (len_ciphertext % 16) == 0 cleartext = b'' all_good = True while len(ciphertext) > 0: ciphertext_block = ciphertext[:16] ciphertext = ciphertext[16:] cleartext_block = self.cipher.decrypt(ciphertext_block) piece_8_bytes = cleartext_block[:8] expected_counter_bytes = struct.pack('<Q', self.counter) self.counter += 1 actual_counter_bytes = cleartext_block[8:] all_good &= compare_digest(expected_counter_bytes, actual_counter_bytes) cleartext += piece_8_bytes assert len(cleartext) * 2 == len_ciphertext if all_good is True: return cleartext else: cleartext = None raise ValueError("bad ciphertext") key = b'YELLOW SUBMARINE' e = Encrypter(key) message1 = b'Four score and seven years ago o' message2 = b'ur fathers brought forth on this' ciphertext1 = e.encrypt(message1) ciphertext2 = e.encrypt(message2) d = Decrypter(key) message1_back = d.decrypt(ciphertext1) message2_back = d.decrypt(ciphertext2) print(message1_back, message2_back)