Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions ciphers/xtea.py
Original file line number Diff line number Diff line change
@@ -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()