From 641436091f173fe42c04c9107ccdd8a2d6326ff5 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:29:20 -0400 Subject: [PATCH 01/11] Add initial files for AES encrypted messages --- builtin/client/aes.lua | 280 ++++++++++++++++++++++++++++++ builtin/client/definition.lua | 111 ++++++++++++ builtin/client/encrypted_chat.lua | 60 +++++++ 3 files changed, 451 insertions(+) create mode 100644 builtin/client/aes.lua create mode 100644 builtin/client/definition.lua create mode 100644 builtin/client/encrypted_chat.lua diff --git a/builtin/client/aes.lua b/builtin/client/aes.lua new file mode 100644 index 000000000..33ad272ae --- /dev/null +++ b/builtin/client/aes.lua @@ -0,0 +1,280 @@ +--[[ +AES-128-CBC Implementation with a Two-Layer API (Readable Version) + +This file provides a clear, commented, and maintainable implementation of AES. + +- It provides two APIs: + 1. `core`: Low-level functions that require manual IV management. + 2. `packaged`: High-level, easy-to-use functions that handle random IV + generation and Base64 packaging automatically. +]] + +-- ################################################################# +-- # 0. SETUP & DEPENDENCIES # +-- ################################################################# + +local AES_CORE = {} +local AES_PACKAGED = {} + +-- Load the Base64 library, which is a dependency for the 'packaged' API. +local base64 +pcall(function() base64 = dofile(minetest.get_modpath("ttd_aes") .. "/b64.lua") end) +if not base64 then pcall(function() base64 = dofile("b64.lua") end) end +if not base64 then error("CSM.TTD.AES: LOADER.ERROR: Could not load the required base64 library.") end + +-- Seed the pseudorandom number generator with high-precision time. +if minetest and minetest.get_us_time then + math.randomseed(minetest.get_us_time()) +else + math.randomseed(os.time()) +end +-- "Burn" the first few values to improve initial randomness +math.random(); math.random(); math.random() + +-- ################################################################# +-- # 1. INTERNAL HELPERS & ALGORITHMS # +-- ################################################################# + +-- Bitwise XOR for Lua 5.1 +local function bxor(a, b) + local p, c = 1, 0 + while a > 0 or b > 0 do + local ra, rb = a % 2, b % 2 + if ra ~= rb then c = c + p end + a = math.floor(a / 2) + b = math.floor(b / 2) + p = p * 2 + end + return c +end + +-- Bitwise Left Shift for Lua 5.1 +local function blshift(x, bits) + return x * (2^bits) +end + +-- String to byte-value table conversion +local function string_to_bytes(str) + if not str then return {} end + local bytes = {} + for i = 1, #str do + bytes[i] = string.byte(str, i) + end + return bytes +end + +-- Byte-value table to string conversion +local function bytes_to_string(bytes) + return string.char(unpack(bytes)) +end + +-- XOR two tables of bytes +local function xor_byte_tables(t1, t2) + local result = {} + for i = 1, #t1 do + result[i] = bxor(t1[i], t2[i]) + end + return result +end + +-- AES constants +local sbox={0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16} +local inv_sbox={0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d} +local Rcon={0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36} +local Nb,Nk,Nr=4,4,10 + +-- Core AES transformation functions +local sub_bytes, shift_rows, inv_shift_rows, mix_columns, inv_mix_columns, add_round_key +local key_expansion, cipher, inv_cipher + +do -- Scoping block for the low-level AES functions + sub_bytes = function(state, use_inv_sbox) + local box = use_inv_sbox and inv_sbox or sbox + for i = 1, 16 do state[i] = box[state[i] + 1] end + end + + shift_rows = function(state) + local t = state[2]; state[2]=state[6]; state[6]=state[10]; state[10]=state[14]; state[14]=t + local t1,t2=state[3],state[7]; state[3]=state[11]; state[7]=state[15]; state[11]=t1; state[15]=t2 + t = state[16]; state[16]=state[12]; state[12]=state[8]; state[8]=state[4]; state[4]=t + end + + inv_shift_rows = function(state) + local t = state[14]; state[14]=state[10]; state[10]=state[6]; state[6]=state[2]; state[2]=t + local t1,t2=state[11],state[15]; state[11]=state[3]; state[15]=state[7]; state[3]=t1; state[7]=t2 + t = state[4]; state[4]=state[8]; state[8]=state[12]; state[12]=state[16]; state[16]=t + end + + local function gmul(a, b) + local p = 0 + for _ = 1, 8 do + if (b % 2) == 1 then p = bxor(p, a) end + local hi_bit_set = (a >= 0x80) + a = blshift(a, 1) % 256 + if hi_bit_set then a = bxor(a, 0x1b) end -- XOR with irreducible polynomial + b = math.floor(b / 2) + end + return p + end + + mix_columns = function(state) + local temp_state = {} + for i=1,4 do + local c = (i-1)*4 + temp_state[c+1] = bxor(bxor(gmul(state[c+1], 2), gmul(state[c+2], 3)), bxor(state[c+3], state[c+4])) + temp_state[c+2] = bxor(bxor(state[c+1], gmul(state[c+2], 2)), bxor(gmul(state[c+3], 3), state[c+4])) + temp_state[c+3] = bxor(bxor(state[c+1], state[c+2]), bxor(gmul(state[c+3], 2), gmul(state[c+4], 3))) + temp_state[c+4] = bxor(bxor(gmul(state[c+1], 3), state[c+2]), bxor(state[c+3], gmul(state[c+4], 2))) + end + for i=1,16 do state[i] = temp_state[i] end + end + + inv_mix_columns = function(state) + local temp_state = {} + for i=1,4 do + local c = (i-1)*4 + temp_state[c+1] = bxor(bxor(gmul(state[c+1], 14), gmul(state[c+2], 11)), bxor(gmul(state[c+3], 13), gmul(state[c+4], 9))) + temp_state[c+2] = bxor(bxor(gmul(state[c+1], 9), gmul(state[c+2], 14)), bxor(gmul(state[c+3], 11), gmul(state[c+4], 13))) + temp_state[c+3] = bxor(bxor(gmul(state[c+1], 13), gmul(state[c+2], 9)), bxor(gmul(state[c+3], 14), gmul(state[c+4], 11))) + temp_state[c+4] = bxor(bxor(gmul(state[c+1], 11), gmul(state[c+2], 13)), bxor(gmul(state[c+3], 9), gmul(state[c+4], 14))) + end + for i=1,16 do state[i] = temp_state[i] end + end + + add_round_key = function(state, round_key) + for i = 1, 16 do state[i] = bxor(state[i], round_key[i]) end + end + + key_expansion = function(key) + local w, temp_word = {}, {} -- 'w' is standard AES term for the key schedule + for i=1,Nk do w[i]={key[4*i-3],key[4*i-2],key[4*i-1],key[4*i]} end + for i=Nk+1,Nb*(Nr+1) do + temp_word = {w[i-1][1], w[i-1][2], w[i-1][3], w[i-1][4]} + if (i-1)%Nk == 0 then + local t = temp_word[1]; temp_word[1],temp_word[2],temp_word[3],temp_word[4] = temp_word[2],temp_word[3],temp_word[4],t -- RotWord + for j=1,4 do temp_word[j] = sbox[temp_word[j]+1] end -- SubWord + temp_word[1] = bxor(temp_word[1], Rcon[math.floor((i-1)/Nk)]) -- XOR with Rcon + end + w[i] = {} + local prev_word = w[i-Nk] + for j=1,4 do w[i][j] = bxor(prev_word[j], temp_word[j]) end + end + return w + end + + cipher = function(input_bytes, w) + local state = {unpack(input_bytes)}; local round_key = {} + for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[j][k] end end; add_round_key(state,round_key) + for round=1,Nr-1 do sub_bytes(state); shift_rows(state); mix_columns(state); for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[round*Nb+j][k] end end; add_round_key(state,round_key) end + sub_bytes(state); shift_rows(state); for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[Nr*Nb+j][k] end end; add_round_key(state,round_key) + return state + end + + inv_cipher = function(input_bytes, w) + local state = {unpack(input_bytes)}; local round_key = {} + for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[Nr*Nb+j][k] end end; add_round_key(state,round_key) + for round=Nr-1,1,-1 do inv_shift_rows(state); sub_bytes(state,true); for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[round*Nb+j][k] end end; add_round_key(state,round_key); inv_mix_columns(state) end + inv_shift_rows(state); sub_bytes(state,true); for j=1,4 do for k=1,4 do round_key[(j-1)*4+k]=w[j][k] end end; add_round_key(state,round_key) + return state + end +end + +-- PKCS#7 Padding and Unpadding +local function pkcs7_pad(data_bytes) + local block_size = 16 + local padding_len = block_size - (#data_bytes % block_size) + if padding_len == 0 then padding_len = block_size end + for i = 1, padding_len do table.insert(data_bytes, padding_len) end + return data_bytes +end + +local function pkcs7_unpad(data_bytes) + if #data_bytes == 0 then return data_bytes, "Input is empty" end + local padding_len = data_bytes[#data_bytes] + if padding_len > #data_bytes or padding_len > 16 or padding_len < 1 then return nil, "Invalid padding length" end + for i = #data_bytes - padding_len + 1, #data_bytes do + if data_bytes[i] ~= padding_len then return nil, "Corrupt padding value" end + end + return {unpack(data_bytes, 1, #data_bytes - padding_len)} +end + +-- Generates a 16-byte random IV as a raw string. +local function generate_iv_string() + local iv_bytes = {} + for i = 1, 16 do iv_bytes[i] = math.random(0, 255) end + return bytes_to_string(iv_bytes) +end + +-- ################################################################# +-- # 2. LOW-LEVEL "CORE" API # +-- ################################################################# +-- This API requires you to provide the IV. It works with raw byte strings. + +--- [CORE] Encrypts raw data with a given key and IV. +function AES_CORE.encrypt(plaintext, key_str, iv_str) + if #key_str ~= 16 or #iv_str ~= 16 then return nil, "Key and IV must be 16 bytes." end + local expanded_key = key_expansion(string_to_bytes(key_str)) + local data_bytes = pkcs7_pad(string_to_bytes(plaintext)) + local ciphertext_bytes = {} + local prev_cipher_block = string_to_bytes(iv_str) + for i = 1, #data_bytes, 16 do + local block = {unpack(data_bytes, i, i + 15)} + local block_to_encrypt = xor_byte_tables(block, prev_cipher_block) + local encrypted_block = cipher(block_to_encrypt, expanded_key) + for j = 1, 16 do table.insert(ciphertext_bytes, encrypted_block[j]) end + prev_cipher_block = encrypted_block + end + return bytes_to_string(ciphertext_bytes) +end + +--- [CORE] Decrypts raw ciphertext with a given key and IV. +function AES_CORE.decrypt(ciphertext, key_str, iv_str) + if #key_str ~= 16 or #iv_str ~= 16 then return nil, "Key and IV must be 16 bytes." end + if #ciphertext % 16 ~= 0 then return nil, "Ciphertext length must be a multiple of 16." end + local expanded_key = key_expansion(string_to_bytes(key_str)) + local data_bytes = string_to_bytes(ciphertext) + local plaintext_bytes = {} + local prev_cipher_block = string_to_bytes(iv_str) + for i = 1, #data_bytes, 16 do + local block = {unpack(data_bytes, i, i + 15)} + local decrypted_block = inv_cipher(block, expanded_key) + local plain_block = xor_byte_tables(decrypted_block, prev_cipher_block) + for j = 1, 16 do table.insert(plaintext_bytes, plain_block[j]) end + prev_cipher_block = block + end + local unpadded_bytes, err = pkcs7_unpad(plaintext_bytes) + if not unpadded_bytes then return nil, err end + return bytes_to_string(unpadded_bytes) +end + +-- ################################################################# +-- # 3. HIGH-LEVEL "PACKAGED" API # +-- ################################################################# +-- This is the recommended, easy-to-use API. It handles everything. + +--- [PACKAGED] Encrypts plaintext, returning a self-contained Base64 package. +function AES_PACKAGED.encrypt(plaintext, key_str) + if type(plaintext) ~= "string" or type(key_str) ~= "string" then return nil, "Plaintext and key must be strings." end + local iv_str = generate_iv_string() + local raw_ciphertext, err = AES_CORE.encrypt(plaintext, key_str, iv_str) + if not raw_ciphertext then return nil, err end + local raw_package = iv_str .. raw_ciphertext + return base64.encode(raw_package) +end + +--- [PACKAGED] Decrypts a self-contained Base64 package. +function AES_PACKAGED.decrypt(base64_package, key_str) + if type(base64_package) ~= "string" or type(key_str) ~= "string" then return nil, "Package and key must be strings." end + local raw_package = base64.decode(base64_package) + if not raw_package then return nil, "Invalid Base64." end + if #raw_package < 16 then return nil, "Package too short." end + local iv_str = raw_package:sub(1, 16) + local ciphertext = raw_package:sub(17) + return AES_CORE.decrypt(ciphertext, key_str, iv_str) +end + +-- Return both APIs so the loader can choose which one to use. +return { + core = AES_CORE, + packaged = AES_PACKAGED +} \ No newline at end of file diff --git a/builtin/client/definition.lua b/builtin/client/definition.lua new file mode 100644 index 000000000..bc44b958a --- /dev/null +++ b/builtin/client/definition.lua @@ -0,0 +1,111 @@ +core.display_chat_message("loading commands...") + +-- secret messages +-- (using formerly b64 but now using A E S) +local MOD_PATH = core.get_modpath("ttd_aes") +local HEADER = [[LSswNDJbIiI6IX0rLz48KyEtMXw=]] -- header to detect +local FOOTER = [[MV8pISh8Kw==]] + +local aes = dofile(MOD_PATH.."aes.lua").packaged + +local function hex_to_string(hex_str) + -- Make sure the hex string has an even number of characters + if #hex_str % 2 ~= 0 then + hex_str = "0" .. hex_str + end + return (hex_str:gsub('..', function (cc) + return string.char(tonumber(cc, 16)) + end)) +end + +local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") + +-- safe guard to not send to the wrong people +TRUSTED = { + -- Ionic + AtomicCheese = "AtomicCheese", + IonicCheese = "IonicCheese", + Sapphire = "Sapphire", + -- nub + nub = "nub", + noob = "noob", + slimy_bannana_peel = "slimy_bannana_peel", + -- Crazylad + Crazylad = "Crazylad", + someLuaGuy = "someLuaGuy", +} + +local function encrypt_aes(message, name) + if not message or not name then + return false, "Invalid Usage, Rejecting..." + end + + local encoded, err = aes.encrypt(message, key) + + if not encoded then + return false, core.colorize("red", "[-!-] CSM.TTD.AES: ENCRYPT.ERROR: ")..err + end + local str = HEADER .. encoded .. FOOTER + + core.send_chat_message("/msg "..name.. " " .. str) + local decoded, err = aes.decrypt(encoded, key) + if decoded then + return true + end + return false, err +end + +core.override_chatcommand("b", { + description = "Send a AES encrypted message (with safeguards on who you send it to)", + param = " ", + func = function(text) + local player, message = text:match("^(%S+)%s(.+)$") + + if not player or not message then + return false, "-!- Invalid Usage, Command Usage: .b " + end + + if TRUSTED[player] then + return encrypt_aes(message, player) + else + return false, core.colorize("red", "CSM.TTD.AES: SAFEGUARD: Attempted to send to untrusted player ("..player..")") + end + end +}) + +core.override_chatcommand("fb", { + description = "Force send a AES encrypted message (USE WITH CAUTION)", + param = " ", + func = function(text) + local player, message = text:match("^(%S+)%s(.+)$") + + if not player or not message then + return false, "-!- Invalid Usage, Command Usage: .b " + end + + return encrypt_aes(message, player) + end +}) +-- +core.register_on_receiving_chat_message(function(message) + local name, head_start = message:find(HEADER,nil,true) + local footer_start, _ = message:find(FOOTER, nil, true) + + if head_start and footer_start then + --if message:find("DM sent to") or + -- message:find("DM to") or + -- message:find("Message sent to") or + -- message:find("PM sent to") then return true end + + local mtd = message:sub(head_start + 1, footer_start - 1) + local decrypted, err = aes.decrypt(mtd, key) + + if decrypted then + core.display_chat_message(core.colorize("red","CSM.TTD.AES: ")..message:sub(1, name - 1)..decrypted) + return true + else + core.display_chat_message(core.colorize("red","[-!-] CSM.TTD.AES: DECRYPT.ERROR: ").."failed to decrypt, `"..mtd .. "`, "..err) + core.log("warning", "[CSM.TTD.AES]: DECRYPT.ERROR: failed to decrypt: `"..mtd .. "`, "..err) + end + end +end) \ No newline at end of file diff --git a/builtin/client/encrypted_chat.lua b/builtin/client/encrypted_chat.lua new file mode 100644 index 000000000..9b6890e91 --- /dev/null +++ b/builtin/client/encrypted_chat.lua @@ -0,0 +1,60 @@ +-- secret messages +-- (using formerly b64 but now using A E S) +local MOD_PATH = core.get_modpath("ttd_aes") + +local aes = dofile(MOD_PATH.."aes.lua").packaged + +local function hex_to_string(hex_str) + -- Make sure the hex string has an even number of characters + if #hex_str % 2 ~= 0 then + hex_str = "0" .. hex_str + end + return (hex_str:gsub('..', function (cc) + return string.char(tonumber(cc, 16)) + end)) +end + +local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") + +local sanity_sucess = false + +-- you should hopefully never see these errors +local function sanity_check() + local message = "TTD.AES.SANITY_CHECK: Message for AES sanity check, !(%)#@)%^}|{/,.}°*~`|><,+™." + local encoded, err = aes.encrypt(message, key) + local encoded2, err = aes.encrypt(message, key) + + if not encoded or not encoded2 then + return false, core.colorize("red", "[-!-] CSM.TTD.AES: SANITY.CHECK: ENCRYPT.ERROR: ") .."Failed to encrypt, "..err + end + + local decoded, err = aes.decrypt(encoded, key) + + if not decoded then + return false, core.colorize("red", "[-!-] CSM.TTD.AES: SANITY.CHECK: DECRYPT.ERROR: ") .."Failed to decrypt, "..err + end + + if encoded == encoded2 then + sanity_sucess = true + return false, core.colorize("orange", "[-!-] CSM.TTD.AES: SANITY.CHECK: SECURITY.WARNING: ") .."Failed to generate second IV, TTD.AES will still run but wont be as secure" + end + + if decoded == message then + sanity_sucess = true + return true, core.colorize("lime", "[-!-] CSM.TTD.AES: SANITY.CHECK: ") .. "Sanity Check complete, TTD.AES is functioning correctly" + end +end + +core.register_chatcommand("b", {}) +core.register_chatcommand("fb", {}) + +core.after(0.5, function() + local success, result = sanity_check() + core.display_chat_message(result) + + if not success then + core.log("error", result) + end + + dofile(MOD_PATH .. "definition.lua") +end) \ No newline at end of file From 8927d45cbc2cade6acbbfc8615230a2ccbb52e6d Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:31:42 -0400 Subject: [PATCH 02/11] Enable the encrypted chat --- builtin/client/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/client/init.lua b/builtin/client/init.lua index 769fbe56c..55c3903cf 100644 --- a/builtin/client/init.lua +++ b/builtin/client/init.lua @@ -11,6 +11,7 @@ dofile(commonpath .. "mod_storage.lua") dofile(commonpath .. "chatcommands.lua") dofile(commonpath .. "information_formspecs.lua") dofile(clientpath .. "chatcommands.lua") +dofile(clientpath .. "encrypted_chat.lua") dofile(clientpath .. "misc.lua") assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions From 3088d0428ad2f6049ebe716a48c73948020451cf Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:39:59 -0400 Subject: [PATCH 03/11] Convert from CSM to builtin --- builtin/client/encrypted_chat.lua | 49 +++---------------------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/builtin/client/encrypted_chat.lua b/builtin/client/encrypted_chat.lua index 9b6890e91..3481af4ca 100644 --- a/builtin/client/encrypted_chat.lua +++ b/builtin/client/encrypted_chat.lua @@ -1,6 +1,6 @@ -- secret messages --- (using formerly b64 but now using A E S) -local MOD_PATH = core.get_modpath("ttd_aes") +-- (using formerly base64 but now using A E S) +local MOD_PATH = core.get_builtin_path() .. "client" local aes = dofile(MOD_PATH.."aes.lua").packaged @@ -14,47 +14,6 @@ local function hex_to_string(hex_str) end)) end -local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") +local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") -- this will do for now... -local sanity_sucess = false - --- you should hopefully never see these errors -local function sanity_check() - local message = "TTD.AES.SANITY_CHECK: Message for AES sanity check, !(%)#@)%^}|{/,.}°*~`|><,+™." - local encoded, err = aes.encrypt(message, key) - local encoded2, err = aes.encrypt(message, key) - - if not encoded or not encoded2 then - return false, core.colorize("red", "[-!-] CSM.TTD.AES: SANITY.CHECK: ENCRYPT.ERROR: ") .."Failed to encrypt, "..err - end - - local decoded, err = aes.decrypt(encoded, key) - - if not decoded then - return false, core.colorize("red", "[-!-] CSM.TTD.AES: SANITY.CHECK: DECRYPT.ERROR: ") .."Failed to decrypt, "..err - end - - if encoded == encoded2 then - sanity_sucess = true - return false, core.colorize("orange", "[-!-] CSM.TTD.AES: SANITY.CHECK: SECURITY.WARNING: ") .."Failed to generate second IV, TTD.AES will still run but wont be as secure" - end - - if decoded == message then - sanity_sucess = true - return true, core.colorize("lime", "[-!-] CSM.TTD.AES: SANITY.CHECK: ") .. "Sanity Check complete, TTD.AES is functioning correctly" - end -end - -core.register_chatcommand("b", {}) -core.register_chatcommand("fb", {}) - -core.after(0.5, function() - local success, result = sanity_check() - core.display_chat_message(result) - - if not success then - core.log("error", result) - end - - dofile(MOD_PATH .. "definition.lua") -end) \ No newline at end of file +core.register_chatcommand("aes", {}) -- overridden in definition.lua From 573e06dbd981301e67fb0240e854206112ec0d54 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:40:14 -0400 Subject: [PATCH 04/11] Convert from CSM to builtin --- builtin/client/definition.lua | 60 ++++++----------------------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/builtin/client/definition.lua b/builtin/client/definition.lua index bc44b958a..2238e61ba 100644 --- a/builtin/client/definition.lua +++ b/builtin/client/definition.lua @@ -1,8 +1,8 @@ core.display_chat_message("loading commands...") -- secret messages --- (using formerly b64 but now using A E S) -local MOD_PATH = core.get_modpath("ttd_aes") +-- (using formerly base64 but now using A E S) +local MOD_PATH = core.get_builtin_path() .. "client" local HEADER = [[LSswNDJbIiI6IX0rLz48KyEtMXw=]] -- header to detect local FOOTER = [[MV8pISh8Kw==]] @@ -18,22 +18,7 @@ local function hex_to_string(hex_str) end)) end -local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") - --- safe guard to not send to the wrong people -TRUSTED = { - -- Ionic - AtomicCheese = "AtomicCheese", - IonicCheese = "IonicCheese", - Sapphire = "Sapphire", - -- nub - nub = "nub", - noob = "noob", - slimy_bannana_peel = "slimy_bannana_peel", - -- Crazylad - Crazylad = "Crazylad", - someLuaGuy = "someLuaGuy", -} +local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") -- this will do for now... local function encrypt_aes(message, name) if not message or not name then @@ -43,7 +28,7 @@ local function encrypt_aes(message, name) local encoded, err = aes.encrypt(message, key) if not encoded then - return false, core.colorize("red", "[-!-] CSM.TTD.AES: ENCRYPT.ERROR: ")..err + return false, core.colorize("red", "-!- ENCRYPT ERROR: ")..err end local str = HEADER .. encoded .. FOOTER @@ -55,48 +40,23 @@ local function encrypt_aes(message, name) return false, err end -core.override_chatcommand("b", { - description = "Send a AES encrypted message (with safeguards on who you send it to)", +core.override_chatcommand("aes", { + description = "Send an AES encrypted message", param = " ", func = function(text) local player, message = text:match("^(%S+)%s(.+)$") if not player or not message then - return false, "-!- Invalid Usage, Command Usage: .b " - end - - if TRUSTED[player] then - return encrypt_aes(message, player) - else - return false, core.colorize("red", "CSM.TTD.AES: SAFEGUARD: Attempted to send to untrusted player ("..player..")") + return false, "-!- Invalid usage, command usage: .aes " end end }) -core.override_chatcommand("fb", { - description = "Force send a AES encrypted message (USE WITH CAUTION)", - param = " ", - func = function(text) - local player, message = text:match("^(%S+)%s(.+)$") - - if not player or not message then - return false, "-!- Invalid Usage, Command Usage: .b " - end - - return encrypt_aes(message, player) - end -}) --- core.register_on_receiving_chat_message(function(message) local name, head_start = message:find(HEADER,nil,true) local footer_start, _ = message:find(FOOTER, nil, true) if head_start and footer_start then - --if message:find("DM sent to") or - -- message:find("DM to") or - -- message:find("Message sent to") or - -- message:find("PM sent to") then return true end - local mtd = message:sub(head_start + 1, footer_start - 1) local decrypted, err = aes.decrypt(mtd, key) @@ -104,8 +64,8 @@ core.register_on_receiving_chat_message(function(message) core.display_chat_message(core.colorize("red","CSM.TTD.AES: ")..message:sub(1, name - 1)..decrypted) return true else - core.display_chat_message(core.colorize("red","[-!-] CSM.TTD.AES: DECRYPT.ERROR: ").."failed to decrypt, `"..mtd .. "`, "..err) - core.log("warning", "[CSM.TTD.AES]: DECRYPT.ERROR: failed to decrypt: `"..mtd .. "`, "..err) + core.display_chat_message(core.colorize("red","-!- DECRYPT ERROR: ").."failed to decrypt, `"..mtd .. "`, "..err) + core.log("warning", "DECRYPT ERROR: failed to decrypt: `"..mtd .. "`, "..err) end end -end) \ No newline at end of file +end) From b60fa1763822fc16e9661117f95ad13d50407e63 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:46:25 -0400 Subject: [PATCH 05/11] Fix a bug --- builtin/client/definition.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/client/definition.lua b/builtin/client/definition.lua index 2238e61ba..b5fd35d29 100644 --- a/builtin/client/definition.lua +++ b/builtin/client/definition.lua @@ -2,7 +2,7 @@ core.display_chat_message("loading commands...") -- secret messages -- (using formerly base64 but now using A E S) -local MOD_PATH = core.get_builtin_path() .. "client" +local MOD_PATH = core.get_builtin_path() .. "client/" local HEADER = [[LSswNDJbIiI6IX0rLz48KyEtMXw=]] -- header to detect local FOOTER = [[MV8pISh8Kw==]] @@ -61,7 +61,7 @@ core.register_on_receiving_chat_message(function(message) local decrypted, err = aes.decrypt(mtd, key) if decrypted then - core.display_chat_message(core.colorize("red","CSM.TTD.AES: ")..message:sub(1, name - 1)..decrypted) + core.display_chat_message(core.colorize("red","Received encrypted message: ")..message:sub(1, name - 1)..decrypted) return true else core.display_chat_message(core.colorize("red","-!- DECRYPT ERROR: ").."failed to decrypt, `"..mtd .. "`, "..err) From 79e86f96ac6c772acb6bfd161293fcf89d920e48 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:46:46 -0400 Subject: [PATCH 06/11] Fix a bug --- builtin/client/encrypted_chat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/client/encrypted_chat.lua b/builtin/client/encrypted_chat.lua index 3481af4ca..d2e5c45f6 100644 --- a/builtin/client/encrypted_chat.lua +++ b/builtin/client/encrypted_chat.lua @@ -1,6 +1,6 @@ -- secret messages -- (using formerly base64 but now using A E S) -local MOD_PATH = core.get_builtin_path() .. "client" +local MOD_PATH = core.get_builtin_path() .. "client/" local aes = dofile(MOD_PATH.."aes.lua").packaged From d4eb220d95461737b3c86bded5409111c7e924bd Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:49:26 -0400 Subject: [PATCH 07/11] Fix a bug --- builtin/client/aes.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/client/aes.lua b/builtin/client/aes.lua index 33ad272ae..e549527af 100644 --- a/builtin/client/aes.lua +++ b/builtin/client/aes.lua @@ -18,9 +18,9 @@ local AES_PACKAGED = {} -- Load the Base64 library, which is a dependency for the 'packaged' API. local base64 -pcall(function() base64 = dofile(minetest.get_modpath("ttd_aes") .. "/b64.lua") end) +pcall(function() base64 = dofile(core.get_builtin_path() .. "client") .. "/b64.lua") end) if not base64 then pcall(function() base64 = dofile("b64.lua") end) end -if not base64 then error("CSM.TTD.AES: LOADER.ERROR: Could not load the required base64 library.") end +if not base64 then error("LOADER ERROR: Could not load the required base64 library.") end -- Seed the pseudorandom number generator with high-precision time. if minetest and minetest.get_us_time then @@ -277,4 +277,4 @@ end return { core = AES_CORE, packaged = AES_PACKAGED -} \ No newline at end of file +} From 860c5d0c9e94940f93ffeb5a1ba76c18e8f8795c Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:50:13 -0400 Subject: [PATCH 08/11] Add missing base64 library --- builtin/client/b64.lua | 162 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 builtin/client/b64.lua diff --git a/builtin/client/b64.lua b/builtin/client/b64.lua new file mode 100644 index 000000000..2e12ece03 --- /dev/null +++ b/builtin/client/b64.lua @@ -0,0 +1,162 @@ +-- A self-contained Base64 encoding and decoding module for Lua. +-- This version is compatible with Lua 5.1 and 5.2, as it does not +-- use the native bitwise operators introduced in Lua 5.3. + +local base64 = {} + +-- The 64 characters used in Base64 encoding, plus the padding character '=' +local b64_chars = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1029384756<>" +local pad_char = "=" + +-- Create a reverse lookup table for fast decoding +local b64_lookup = {} +for i = 1, #b64_chars do + local char = b64_chars:sub(i, i) + b64_lookup[char] = i - 1 -- Store as 0-63 index +end + + +-- =================================================================== +-- Bitwise Operation Polyfills for Lua 5.1/5.2 +-- =================================================================== + +-- Simulates bitwise left shift (<<) +local function bit_lshift(x, bits) + return x * (2^bits) +end + +-- Simulates bitwise right shift (>>) +local function bit_rshift(x, bits) + return math.floor(x / (2^bits)) +end + +-- Simulates bitwise AND (&) +local function bit_and(a, b) + local result = 0 + local power_of_2 = 1 + while a > 0 and b > 0 do + if a % 2 == 1 and b % 2 == 1 then + result = result + power_of_2 + end + a = math.floor(a / 2) + b = math.floor(b / 2) + power_of_2 = power_of_2 * 2 + end + return result +end + +-- Simulates bitwise OR (|) +local function bit_or(a, b) + local result = 0 + local power_of_2 = 1 + while a > 0 or b > 0 do + if a % 2 == 1 or b % 2 == 1 then + result = result + power_of_2 + end + a = math.floor(a / 2) + b = math.floor(b / 2) + power_of_2 = power_of_2 * 2 + end + return result +end + + +-- =================================================================== +-- Core Functions +-- =================================================================== + +--- Encodes a string into Base64 format. +-- @param data The raw string data to encode. +-- @return The Base64 encoded string. +function base64.encode(data) + if not data or data == "" then + return "" + end + + local encoded_parts = {} + local data_len = #data + + -- Process the data in chunks of 3 bytes + for i = 1, data_len, 3 do + local b1 = data:byte(i) + local b2 = data:byte(i + 1) + local b3 = data:byte(i + 2) + + -- Combine the 3 bytes (24 bits) and extract four 6-bit chunks + local enc1 = bit_rshift(b1, 2) + local enc2 = bit_or(bit_lshift(bit_and(b1, 3), 4), (b2 and bit_rshift(b2, 4) or 0)) + local enc3 = b2 and bit_or(bit_lshift(bit_and(b2, 15), 2), (b3 and bit_rshift(b3, 6) or 0)) + local enc4 = b3 and bit_and(b3, 63) + + -- Convert 6-bit chunk values to Base64 characters + local str = b64_chars:sub(enc1 + 1, enc1 + 1) .. + b64_chars:sub(enc2 + 1, enc2 + 1) + + if enc3 then + str = str .. b64_chars:sub(enc3 + 1, enc3 + 1) + else + str = str .. pad_char + end + + if enc4 then + str = str .. b64_chars:sub(enc4 + 1, enc4 + 1) + else + str = str .. pad_char + end + + table.insert(encoded_parts, str) + end + + return table.concat(encoded_parts) +end + +--- Decodes a Base64 string. +-- @param data The Base64 encoded string. +-- @return The decoded raw string, or nil if the input is invalid. +function base64.decode(data) + if not data or data == "" then + return "" + end + + -- THE FIX IS HERE: We escape the '+' so it's treated as a literal character. + local pattern = '[^' .. b64_chars:gsub('+', '%%+') .. '=]' + data = data:gsub(pattern, '') + + -- Now this check will pass, because '+' symbols are no longer removed. + if #data % 4 ~= 0 then + return nil + end + + local decoded_parts = {} + + for i = 1, #data, 4 do + local c1, c2, c3, c4 = data:sub(i, i), data:sub(i + 1, i + 1), data:sub(i + 2, i + 2), data:sub(i + 3, i + 3) + local v1, v2 = b64_lookup[c1], b64_lookup[c2] + + if not v1 or not v2 then + return nil + end + + local b1 = bit_or(bit_lshift(v1, 2), bit_rshift(v2, 4)) + table.insert(decoded_parts, string.char(b1)) + + if c3 ~= pad_char then + local v3 = b64_lookup[c3] + if not v3 then return nil end + local b2 = bit_or(bit_lshift(bit_and(v2, 15), 4), bit_rshift(v3, 2)) + table.insert(decoded_parts, string.char(b2)) + end + + if c4 ~= pad_char then + local v3 = b64_lookup[c3] + local v4 = b64_lookup[c4] + if not v4 then return nil end + local b3 = bit_or(bit_lshift(bit_and(v3, 3), 6), v4) + table.insert(decoded_parts, string.char(b3)) + end + end + + return table.concat(decoded_parts) +end + +return base64 \ No newline at end of file From 7b5f22550a360bb68799ac818fd58777a36c3128 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:52:17 -0400 Subject: [PATCH 09/11] Fix a bug --- builtin/client/aes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/client/aes.lua b/builtin/client/aes.lua index e549527af..90c055f90 100644 --- a/builtin/client/aes.lua +++ b/builtin/client/aes.lua @@ -18,7 +18,7 @@ local AES_PACKAGED = {} -- Load the Base64 library, which is a dependency for the 'packaged' API. local base64 -pcall(function() base64 = dofile(core.get_builtin_path() .. "client") .. "/b64.lua") end) +pcall(function() base64 = dofile(core.get_builtin_path() .. "client/b64.lua") end) if not base64 then pcall(function() base64 = dofile("b64.lua") end) end if not base64 then error("LOADER ERROR: Could not load the required base64 library.") end From df836a313ac32d8fbc64c9662b28f912d96c3691 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:55:39 -0400 Subject: [PATCH 10/11] Move the chatcommand registraton to another file --- builtin/client/definition.lua | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/builtin/client/definition.lua b/builtin/client/definition.lua index b5fd35d29..884ea4ae4 100644 --- a/builtin/client/definition.lua +++ b/builtin/client/definition.lua @@ -40,18 +40,6 @@ local function encrypt_aes(message, name) return false, err end -core.override_chatcommand("aes", { - description = "Send an AES encrypted message", - param = " ", - func = function(text) - local player, message = text:match("^(%S+)%s(.+)$") - - if not player or not message then - return false, "-!- Invalid usage, command usage: .aes " - end - end -}) - core.register_on_receiving_chat_message(function(message) local name, head_start = message:find(HEADER,nil,true) local footer_start, _ = message:find(FOOTER, nil, true) From fec3756447a5ffe68960af29235f65f758b1acd7 Mon Sep 17 00:00:00 2001 From: a-bad-dev Date: Tue, 2 Dec 2025 03:55:58 -0400 Subject: [PATCH 11/11] Move the chatcommand registration to this file --- builtin/client/encrypted_chat.lua | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/builtin/client/encrypted_chat.lua b/builtin/client/encrypted_chat.lua index d2e5c45f6..6742baaa1 100644 --- a/builtin/client/encrypted_chat.lua +++ b/builtin/client/encrypted_chat.lua @@ -16,4 +16,14 @@ end local key = hex_to_string("2a2305e78c34997168bed08c6434ffd") -- this will do for now... -core.register_chatcommand("aes", {}) -- overridden in definition.lua +core.register_chatcommand("aes", { + description = "Send an AES encrypted message", + param = " ", + func = function(text) + local player, message = text:match("^(%S+)%s(.+)$") + + if not player or not message then + return false, "-!- Invalid usage, command usage: .aes " + end + end +})