From e017466cc7a3304214b9a6cb5c61520f070b83fb Mon Sep 17 00:00:00 2001 From: hanez Date: Sun, 8 Feb 2026 23:56:21 +0100 Subject: [PATCH] Added AES256 support... (0.38.7) --- CMakeLists.txt | 7 +- examples/crypto/aes256.fun | 45 +++++ lib/crypt/aes256.fun | 327 +++++++++++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 6 deletions(-) create mode 100755 examples/crypto/aes256.fun create mode 100644 lib/crypt/aes256.fun diff --git a/CMakeLists.txt b/CMakeLists.txt index c7c8bbb..8b09c23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.38.6 LANGUAGES C) +project(fun VERSION 0.38.7 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -229,15 +229,10 @@ endif() # Common locations (out-of-source and legacy in-source builds) list(APPEND _candidates \"\${CMAKE_BINARY_DIR}/install_manifest.txt\" - \"\${CMAKE_SOURCE_DIR}/install_manifest.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_Debug.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_Release.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_RelWithDebInfo.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_MinSizeRel.txt\" - \"\${CMAKE_SOURCE_DIR}/install_manifest_Debug.txt\" - \"\${CMAKE_SOURCE_DIR}/install_manifest_Release.txt\" - \"\${CMAKE_SOURCE_DIR}/install_manifest_RelWithDebInfo.txt\" - \"\${CMAKE_SOURCE_DIR}/install_manifest_MinSizeRel.txt\" ) set(_manifest_file \"\") diff --git a/examples/crypto/aes256.fun b/examples/crypto/aes256.fun new file mode 100755 index 0000000..f1f9b61 --- /dev/null +++ b/examples/crypto/aes256.fun @@ -0,0 +1,45 @@ +#!/usr/bin/env fun + +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-02-04 + */ + +/* + * Example: Using AES-256 (ECB) from lib/crypt/aes256.fun + * + * Test vector (FIPS-197, AES-256): + * key: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f + * pt : 00112233445566778899aabbccddeeff + * ct : 8ea2b7ca516745bfeafc49904b496089 + */ + +#include + +aes = AES256() + +key = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +pt = "00112233445566778899aabbccddeeff" + +ct = aes.encrypt_block_hex(pt, key) +print(ct) + +// Multi-block ECB example (two same blocks => two same ciphertext blocks) +//pt2 = join([pt, pt], "") +//ct2 = aes.encrypt_ecb_hex(pt2, key) +//print(ct2) + +/* Expected output: +8ea2b7ca516745bfeafc49904b496089 +*/ + +/* Wanted doutput: +8ea2b7ca516745bfeafc49904b496089 +8ea2b7ca516745bfeafc49904b4960898ea2b7ca516745bfeafc49904b496089 +*/ diff --git a/lib/crypt/aes256.fun b/lib/crypt/aes256.fun new file mode 100644 index 0000000..89c7767 --- /dev/null +++ b/lib/crypt/aes256.fun @@ -0,0 +1,327 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + * + * Added: 2026-02-04 + */ + +/* + * AES-256 (ECB) encryption in pure Fun. + * + * Public API (class AES256): + * encrypt_block_hex(pt_hex32, key_hex64) -> ct_hex32 + * encrypt_ecb_hex(hexStr, key_hex64) -> ct_hex (hexStr length must be multiple of 32) + * + * Example test vector (AES-256, FIPS-197): + * key: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f + * pt : 00112233445566778899aabbccddeeff + * ct : 8ea2b7ca516745bfeafc49904b496089 + */ + +#include + +class AES256() + // ---------- Hex helpers ---------- + fun hex_val(this, ch) + if (ch == "0") + return 0 + else if (ch == "1") + return 1 + else if (ch == "2") + return 2 + else if (ch == "3") + return 3 + else if (ch == "4") + return 4 + else if (ch == "5") + return 5 + else if (ch == "6") + return 6 + else if (ch == "7") + return 7 + else if (ch == "8") + return 8 + else if (ch == "9") + return 9 + else if (ch == "a" || ch == "A") + return 10 + else if (ch == "b" || ch == "B") + return 11 + else if (ch == "c" || ch == "C") + return 12 + else if (ch == "d" || ch == "D") + return 13 + else if (ch == "e" || ch == "E") + return 14 + else if (ch == "f" || ch == "F") + return 15 + else + return 0 + + fun byte_from_hex_pair(this, hh) + hi = this.hex_val(substr(hh, 0, 1)) + lo = this.hex_val(substr(hh, 1, 1)) + return hi * 16 + lo + + fun from_hex(this, hex) + arr = [] + i = 0 + n = len(hex) + while i + 1 < n + push(arr, this.byte_from_hex_pair(substr(hex, i, 2))) + i = i + 2 + return arr + + fun two_hex(this, n) + n = n % 256 + d = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] + hi = n / 16 + lo = n % 16 + parts = [d[hi], d[lo]] + return join(parts, "") + + fun bytes_to_hex(this, arr) + i = 0 + out = [] + while i < len(arr) + push(out, this.two_hex(arr[i])) + i = i + 1 + return join(out, "") + + // ---------- Finite field helpers (GF(2^8)) ---------- + fun b8(this, x) + // clamp to 0..255 + while x < 0 + x = x + 256 + while x >= 256 + x = x - 256 + return x + + fun xtime(this, x) + x = this.b8(x) + hi = band(x, 128) + x2 = this.b8(shl(x, 1)) + if (hi != 0) + x2 = bxor(x2, 27) + return this.b8(x2) + + fun mul2(this, x) + return this.xtime(x) + fun mul3(this, x) + return bxor(this.xtime(x), this.b8(x)) + + // ---------- S-box & Rcon ---------- + S = [ + 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, + 108,216,171,77,154 + ] + + // ---------- Key schedule for AES-256 ---------- + // Input key (32 bytes). Output round keys as 240 bytes (Nr=14, Nb=4 -> (Nr+1)*Nb*4 = 60*4). + fun sub_word(this, w) + // w is 4-byte array + return [ this.S[w[0]], this.S[w[1]], this.S[w[2]], this.S[w[3]] ] + + fun rot_word(this, w) + return [ w[1], w[2], w[3], w[0] ] + + fun key_expansion(this, key_bytes) + Nk = 8 + Nb = 4 + Nr = 14 + W = [] // words; each word is 4 bytes stored back-to-back in a flat array of bytes + + // copy initial key (8 words -> 32 bytes) + i = 0 + while i < 32 + push(W, key_bytes[i]) + i = i + 1 + + // Expand to 60 words (240 bytes) + // We work in 4-byte steps, keeping temporary word t (4 bytes) + bytes_len = 32 + while bytes_len < 240 + // fetch previous word + t0 = W[bytes_len - 4] + t1 = W[bytes_len - 3] + t2 = W[bytes_len - 2] + t3 = W[bytes_len - 1] + t = [t0,t1,t2,t3] + + i_words = bytes_len / 4 // word index + if (i_words % Nk == 0) + t = this.sub_word(this.rot_word(t)) + rc = this.Rcon[i_words / Nk] + t[0] = bxor(t[0], rc) + else if (i_words % Nk == 4) + t = this.sub_word(t) + + // W[i] = W[i - Nk] xor t + j = 0 + while j < 4 + prev = W[bytes_len - 32 + j] // 32 bytes == Nk*4 + push(W, bxor(prev, t[j])) + j = j + 1 + bytes_len = bytes_len + 4 + return W // 240 bytes + + // ---------- State transforms ---------- + fun add_round_key(this, state, roundKeys, round) + // state: 16 bytes, roundKeys: 240 bytes, round idx 0..14 + // Nb=4, 16 bytes per round + offset = round * 16 + i = 0 + while i < 16 + state[i] = bxor(state[i], roundKeys[offset + i]) + i = i + 1 + return state + + fun sub_bytes(this, state) + i = 0 + while i < 16 + state[i] = this.S[state[i]] + i = i + 1 + return state + + fun shift_rows(this, s) + // s is 1D [r + 4*c] (row-major within column-major indexing): r in [0..3], c in [0..3] + // Row 0: no shift + // Row 1: left by 1 + t = s[1] + s[1] = s[5] + s[5] = s[9] + s[9] = s[13] + s[13] = t + // Row 2: left by 2 + t0 = s[2] + t1 = s[6] + s[2] = s[10] + s[6] = s[14] + s[10] = t0 + s[14] = t1 + // Row 3: left by 3 (aka right by 1) + t = s[15] + s[15] = s[11] + s[11] = s[7] + s[7] = s[3] + s[3] = t + return s + + fun mix_single_column(this, a0, a1, a2, a3) + // returns [r0,r1,r2,r3] + r0 = bxor(bxor(bxor(this.mul2(a0), this.mul3(a1)), a2), a3) + r1 = bxor(bxor(bxor(a0, this.mul2(a1)), this.mul3(a2)), a3) + r2 = bxor(bxor(bxor(a0, a1), this.mul2(a2)), this.mul3(a3)) + r3 = bxor(bxor(bxor(this.mul3(a0), a1), a2), this.mul2(a3)) + return [this.b8(r0), this.b8(r1), this.b8(r2), this.b8(r3)] + + fun mix_columns(this, s) + c = 0 + while c < 4 + i = c * 4 + col = this.mix_single_column(s[i], s[i+1], s[i+2], s[i+3]) + s[i] = col[0] + s[i+1] = col[1] + s[i+2] = col[2] + s[i+3] = col[3] + c = c + 1 + return s + + // ---------- Block encryption ---------- + fun encrypt_block_bytes(this, pt16, key32) + // pt16: 16 bytes array, key32: 32 bytes array + // returns 16 bytes array (ciphertext) + Nb = 4 + Nr = 14 + rk = this.key_expansion(key32) // 240 bytes + + // copy state + s = [] + i = 0 + while i < 16 + push(s, pt16[i]) + i = i + 1 + + // round 0 + s = this.add_round_key(s, rk, 0) + + round = 1 + while round < Nr + s = this.sub_bytes(s) + s = this.shift_rows(s) + s = this.mix_columns(s) + s = this.add_round_key(s, rk, round) + round = round + 1 + + // final round (no MixColumns) + s = this.sub_bytes(s) + s = this.shift_rows(s) + s = this.add_round_key(s, rk, Nr) + return s + + fun encrypt_block_hex(this, pt_hex32, key_hex64) + // Validate lengths + if (len(pt_hex32) != 32) + // Return empty on invalid input to match common style + return "" + if (len(key_hex64) != 64) + return "" + pt = this.from_hex(pt_hex32) + key = this.from_hex(key_hex64) + ct = this.encrypt_block_bytes(pt, key) + return this.bytes_to_hex(ct) + + fun encrypt_ecb_hex(this, hexStr, key_hex64) + // Robust handling that avoids substr semantics ambiguity by working on bytes + // Validate key + if (len(key_hex64) != 64) + return "" + // Validate hexStr length: must be even and represent a whole number of 16-byte blocks + if ((len(hexStr) % 2) != 0) + return "" + bytes = this.from_hex(hexStr) + if ((len(bytes) % 16) != 0) + return "" + key = this.from_hex(key_hex64) + out_bytes = [] + off = 0 + total = len(bytes) + while off < total + // collect 16-byte block + blk = [] + j = 0 + while j < 16 + push(blk, bytes[off + j]) + j = j + 1 + ct_blk = this.encrypt_block_bytes(blk, key) + // append to output + k = 0 + while k < 16 + push(out_bytes, ct_blk[k]) + k = k + 1 + off = off + 16 + return this.bytes_to_hex(out_bytes)