diff --git a/ciphers/xtea.py b/ciphers/xtea.py new file mode 100644 index 000000000000..4d2aedde6c31 --- /dev/null +++ b/ciphers/xtea.py @@ -0,0 +1,118 @@ +""" +XTEA (eXtended Tiny Encryption Algorithm) is a block cipher designed to +correct weaknesses in TEA. It was published by David Wheeler and Roger +Needham in 1997. XTEA operates on 64-bit blocks with a 128-bit key and +uses a Feistel network with a recommended 64 rounds. + +It's still found in embedded systems and game networking protocols due +to its simplicity and small code footprint. + +Reference: https://en.wikipedia.org/wiki/XTEA +""" + +import struct + +DELTA = 0x9E3779B9 +MASK = 0xFFFFFFFF + + +def xtea_encrypt(block: bytes, key: bytes, num_rounds: int = 64) -> bytes: + """ + Encrypt a single 64-bit block using XTEA. + + :param block: 8 bytes of plaintext + :param key: 16 bytes (128-bit key) + :param num_rounds: number of Feistel rounds (default 64) + :return: 8 bytes of ciphertext + + >>> key = b'\\x00' * 16 + >>> plaintext = b'\\x00' * 8 + >>> ciphertext = xtea_encrypt(plaintext, key) + >>> ciphertext.hex() + 'fc924d124ad0ed50' + + >>> xtea_encrypt(b'hello!!!', b'sixteenbyteskey!') + b'u\\x8d\\x00\\x17c\\xb8\\xf0*' + + >>> xtea_encrypt(b'short', key) + Traceback (most recent call last): + ... + ValueError: block must be 8 bytes + + >>> xtea_encrypt(plaintext, b'short') + Traceback (most recent call last): + ... + ValueError: key must be 16 bytes + """ + if len(block) != 8: + raise ValueError("block must be 8 bytes") + if len(key) != 16: + raise ValueError("key must be 16 bytes") + + v0, v1 = struct.unpack("!II", block) + k = struct.unpack("!4I", key) + + total = 0 + for _ in range(num_rounds): + v0 = (v0 + ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (total + k[total & 3]))) & MASK + total = (total + DELTA) & MASK + v1 = ( + v1 + ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (total + k[(total >> 11) & 3])) + ) & MASK + + return struct.pack("!II", v0, v1) + + +def xtea_decrypt(block: bytes, key: bytes, num_rounds: int = 64) -> bytes: + """ + Decrypt a single 64-bit block using XTEA. + + :param block: 8 bytes of ciphertext + :param key: 16 bytes (128-bit key) + :param num_rounds: number of Feistel rounds (default 64) + :return: 8 bytes of plaintext + + Roundtrip test -- encrypt then decrypt returns original plaintext: + >>> key = b'\\x00' * 16 + >>> plaintext = b'\\x00' * 8 + >>> xtea_decrypt(xtea_encrypt(plaintext, key), key) == plaintext + True + + >>> msg = b'hello!!!' + >>> k = b'sixteenbyteskey!' + >>> xtea_decrypt(xtea_encrypt(msg, k), k) == msg + True + + >>> xtea_decrypt(b'short', key) + Traceback (most recent call last): + ... + ValueError: block must be 8 bytes + + >>> xtea_decrypt(b'\\x00' * 8, b'short') + Traceback (most recent call last): + ... + ValueError: key must be 16 bytes + """ + if len(block) != 8: + raise ValueError("block must be 8 bytes") + if len(key) != 16: + raise ValueError("key must be 16 bytes") + + v0, v1 = struct.unpack("!II", block) + k = struct.unpack("!4I", key) + + total = (DELTA * num_rounds) & MASK + for _ in range(num_rounds): + v1 = ( + v1 - ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ (total + k[(total >> 11) & 3])) + ) & MASK + total = (total - DELTA) & MASK + v0 = (v0 - ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ (total + k[total & 3]))) & MASK + + return struct.pack("!II", v0, v1) + + +if __name__ == "__main__": + import doctest + + doctest.testmod()