I am using Windows 11 and I plan to implement the code in C++. As you might know, building C++ libraries on Windows is very complicated, so I want to make sure it uses the least amount of dependencies possible.
For more context about the larger project this will be a part of, see this.
I have decided to implement a load-balancing ShadowSocks5 proxy in C++ from scratch. This is a programming challenge, a learning project, and a practical project all in one.
I decided to start from the easiest problem, the encryption I need to use is AES-256-GCM. GCM stands for Galois/Counter_Mode, I haven't figured out how to implement it from reading the Wikipedia article. But the Wikipedia article on Advanced Encryption Standard is very helpful and is one of the primary references I used in implementing this. Another Wikipedia article I referenced is AES key schedule. I got the values for SBOX and RSBOX from this article
Now, here is the implementation, I wrote it all by myself, an effort that took two days:
import json
with open("D:/AES_256.json", "r") as f:
AES_256 = json.load(f)
MAX_256 = (1 << 256) - 1
SBOX = AES_256["SBOX"]
RCON = AES_256["RCON"]
OCTUPLE = (
(0, 4),
(4, 8),
(8, 12),
(12, 16),
(16, 20),
(20, 24),
(24, 28),
(28, 32),
)
SEXAGESY = (
(0, 1, 2, 3),
(4, 5, 6, 7),
(8, 9, 10, 11),
(12, 13, 14, 15),
(16, 17, 18, 19),
(20, 21, 22, 23),
(24, 25, 26, 27),
(28, 29, 30, 31),
(32, 33, 34, 35),
(36, 37, 38, 39),
(40, 41, 42, 43),
(44, 45, 46, 47),
(48, 49, 50, 51),
(52, 53, 54, 55),
(56, 57, 58, 59),
)
HEXMAT = (0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15)
SHIFT = (0, 1, 2, 3, 5, 6, 7, 4, 10, 11, 8, 9, 15, 12, 13, 14)
COLMAT = (
((2, 0), (1, 4), (1, 8), (3, 12)),
((2, 1), (1, 5), (1, 9), (3, 13)),
((2, 2), (1, 6), (1, 10), (3, 14)),
((2, 3), (1, 7), (1, 11), (3, 15)),
((3, 0), (2, 4), (1, 8), (1, 12)),
((3, 1), (2, 5), (1, 9), (1, 13)),
((3, 2), (2, 6), (1, 10), (1, 14)),
((3, 3), (2, 7), (1, 11), (1, 15)),
((1, 0), (3, 4), (2, 8), (1, 12)),
((1, 1), (3, 5), (2, 9), (1, 13)),
((1, 2), (3, 6), (2, 10), (1, 14)),
((1, 3), (3, 7), (2, 11), (1, 15)),
((1, 0), (1, 4), (3, 8), (2, 12)),
((1, 1), (1, 5), (3, 9), (2, 13)),
((1, 2), (1, 6), (3, 10), (2, 14)),
((1, 3), (1, 7), (3, 11), (2, 15)),
)
def add_round_key(state: list, key: list) -> list:
return [a ^ b for a, b in zip(state, key)]
def state_matrix(data: list) -> list:
return [data[i] for i in HEXMAT]
def sub_bytes(state: list) -> list:
return [SBOX[i] for i in state]
def shift_rows(state: list) -> list:
return [state[i] for i in SHIFT]
def rot8(byte: int, x: int) -> int:
x &= 7
return (byte << x | byte >> (8 - x)) & 0xFF
def quadword(quad: bytes) -> int:
a, b, c, d = quad
return (a << 24) | (b << 16) | (c << 8) | d
def rot_word(word: int) -> int:
return (word << 8 | word >> 24) & 0xFFFFFFFF
def sub_word(word: int) -> int:
return (
(SBOX[(word >> 24) & 0xFF] << 24)
| (SBOX[(word >> 16) & 0xFF] << 16)
| (SBOX[(word >> 8) & 0xFF] << 8)
| SBOX[word & 0xFF]
)
def galois_mult(x: int, y: int) -> int:
p = 0
while x and y:
if y & 1:
p ^= x
if x & 0x80:
x = (x << 1) ^ 0x11B
else:
x <<= 1
y >>= 1
return p
def mix_columns(state: list) -> list:
result = [0] * 16
for e, row in zip(state, COLMAT):
for mult, i in row:
result[i] ^= galois_mult(e, mult)
return state_matrix(result)
def key_matrix(key: list) -> list:
mat = []
for row in SEXAGESY:
line = []
for col in row:
n = key[col]
line.extend([n >> 24 & 0xFF, n >> 16 & 0xFF, n >> 8 & 0xFF, n & 0xFF])
mat.append(state_matrix(line))
return mat
def derive_key(password: bytes) -> list:
keys = [quadword(password[a:b]) for a, b in OCTUPLE]
result = keys.copy()
last = result[7]
for i in range(8, 60):
if not i & 7:
last = sub_word(rot_word(last)) ^ (RCON[i // 8] << 24)
elif i & 7 == 4:
last = sub_word(last)
key = result[i - 8] ^ last
result.append(key)
last = key
return key_matrix(result)
def aes_256_cipher(data: bytes, password: bytes) -> list:
state = add_round_key(state_matrix(data), password[0])
for i in range(1, 14):
state = add_round_key(mix_columns(shift_rows(sub_bytes(state))), password[i])
return state_matrix(add_round_key(shift_rows(sub_bytes(state)), password[14]))
def get_padded_data(data: bytes | str) -> bytes:
if isinstance(data, str):
data = data.encode("utf8")
if not isinstance(data, bytes):
raise ValueError("argument data must be bytes or str")
return data + b"\x00" * (16 - len(data) % 16)
def get_key(password: bytes | int | str) -> list:
if isinstance(password, int):
if password < 0 or password > MAX_256:
raise ValueError("argument password must be between 0 and 2^256-1")
password = password.to_bytes(32, "big")
if isinstance(password, str):
password = "".join(i for i in password if i.isalnum()).encode("utf8")
if len(password) > 32:
raise ValueError("argument password must be 32 bytes or less")
if not isinstance(password, bytes):
raise ValueError("argument password must be bytes | int | str")
return derive_key(password.rjust(32, b"\x00"))
def ecb_encrypt(data: bytes | str, password: bytes | str) -> str:
data = get_padded_data(data)
key = get_key(password)
blocks = [aes_256_cipher(data[i : i + 16], key) for i in range(0, len(data), 16)]
return "".join(f"{e:02x}" for block in blocks for e in block)
AES_256.json
{
"SBOX": [
99 , 124, 119, 123, 242, 107, 111, 197, 48 , 1 , 103, 43 , 254, 215, 171, 118,
202, 130, 201, 125, 250, 89 , 71 , 240, 173, 212, 162, 175, 156, 164, 114, 192,
183, 253, 147, 38 , 54 , 63 , 247, 204, 52 , 165, 229, 241, 113, 216, 49 , 21 ,
4 , 199, 35 , 195, 24 , 150, 5 , 154, 7 , 18 , 128, 226, 235, 39 , 178, 117,
9 , 131, 44 , 26 , 27 , 110, 90 , 160, 82 , 59 , 214, 179, 41 , 227, 47 , 132,
83 , 209, 0 , 237, 32 , 252, 177, 91 , 106, 203, 190, 57 , 74 , 76 , 88 , 207,
208, 239, 170, 251, 67 , 77 , 51 , 133, 69 , 249, 2 , 127, 80 , 60 , 159, 168,
81 , 163, 64 , 143, 146, 157, 56 , 245, 188, 182, 218, 33 , 16 , 255, 243, 210,
205, 12 , 19 , 236, 95 , 151, 68 , 23 , 196, 167, 126, 61 , 100, 93 , 25 , 115,
96 , 129, 79 , 220, 34 , 42 , 144, 136, 70 , 238, 184, 20 , 222, 94 , 11 , 219,
224, 50 , 58 , 10 , 73 , 6 , 36 , 92 , 194, 211, 172, 98 , 145, 149, 228, 121,
231, 200, 55 , 109, 141, 213, 78 , 169, 108, 86 , 244, 234, 101, 122, 174, 8 ,
186, 120, 37 , 46 , 28 , 166, 180, 198, 232, 221, 116, 31 , 75 , 189, 139, 138,
112, 62 , 181, 102, 72 , 3 , 246, 14 , 97 , 53 , 87 , 185, 134, 193, 29 , 158,
225, 248, 152, 17 , 105, 217, 142, 148, 155, 30 , 135, 233, 206, 85 , 40 , 223,
140, 161, 137, 13 , 191, 230, 66 , 104, 65 , 153, 45 , 15 , 176, 84 , 187, 22
],
"RCON": [0, 1, 2, 4, 8, 16, 32, 64, 128, 27, 54]
}
It doesn't raise exceptions, but I have absolutely no idea what I am doing.
This is an example output:
In [428]: ecb_encrypt('6a84867cd77e12ad07ea1be895c53fa3', '0'*32)
Out[428]: 'b981b1853c16fbb6adc7cf4a01c9c57b94a3e5ce608239660c324b01400ebdd5d45a5452d22fed94b7ca9d916ac47736'
I have used this website to check the correctness of the output, with the same key and plaintext, AES-256-ECB mode, the cipher text in hex is:
e07beff38697f04e7adbc971adc2a9135f60746178fcd0f1b3040e4d15c920ad0318e084e1666e699891c78f8aa98960
It isn't what my code outputs as you can see.
Why isn't my code working properly?