This commit is contained in:
2026-06-24 16:28:42 +02:00
commit 1922f2db62
19 changed files with 571 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
export const addRoundKey = (state: number[][], roundKey: number[][]): number[][] => {
const result: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
for (let row = 0;row < 4;row++) {
for (let col = 0;col < 4;col++) {
result[row][col] = state[row][col] ^ roundKey[row][col]
}
}
return result
}
+40
View File
@@ -0,0 +1,40 @@
import { addRoundKey, expandKey, flattenBlocks, invMixColumns, invShiftRows, invSubBytes, preprocessKey } from './'
export const decrypt = (encryptedBlocks: number[][][], key: string): string => {
/**
* @param {number[][][]} encryptedBlocks - An array containing blocks of cyphertext. Each block is a 4x4 matrix.
* @param {string} key - A string of 128, 192, or 256 bits.
* @returns {string} The original plaintext.
*/
const keyBlock = preprocessKey(key)
const roundKeys = expandKey(keyBlock)
const keySize = key.length // Key size in bytes (16 for 128-bit, 24 for 192-bit, 32 for 256-bit)
const N_k = keySize / 4 // Number of 32-bit words in the key (4 for 128-bit, 6 for 192-bit, 8 for 256-bit)
const N_r = N_k + 6
const decryptedBlocks: number[][][] = []
for (let i = 0;i < encryptedBlocks.length;i++) {
let state = encryptedBlocks[i]
state = addRoundKey(state, roundKeys[N_r])
state = invShiftRows(state)
state = invSubBytes(state)
for (let j = N_r - 1;j >= 1;j--) {
state = addRoundKey(state, roundKeys[j])
state = invMixColumns(state)
state = invShiftRows(state)
state = invSubBytes(state)
}
state = addRoundKey(state, roundKeys[0])
decryptedBlocks.push(state)
}
const flatArray = flattenBlocks(decryptedBlocks)
const decoder = new TextDecoder()
return decoder.decode(new Uint8Array(flatArray))
}
+38
View File
@@ -0,0 +1,38 @@
import { preprocessPlaintext, preprocessKey, expandKey, addRoundKey, subBytes } from './'
import { mixColumns } from './mixColumns'
import { shiftRows } from './shiftRows'
export const encrypt = (plaintext: string, key: string): number[][][] => {
/**
* @param {string} plaintext - The plaintext to encrypt.
* @param {string} key - A string of 128, 192, or 256 bits.
* @returns {string} The encrypted cyphertext.
*/
const plaintextBlocks = preprocessPlaintext(plaintext)
const keyBlock = preprocessKey(key)
const roundKeys = expandKey(keyBlock)
const keySize = key.length // Key size in bytes (16 for 128-bit, 24 for 192-bit, 32 for 256-bit)
const N_k = keySize / 4 // Number of 32-bit words in the key (4 for 128-bit, 6 for 192-bit, 8 for 256-bit)
const N_r = N_k + 6
const encryptedBlocks: number[][][] = []
for (let i = 0;i < plaintextBlocks.length;i++) {
let state = plaintextBlocks[i]
state = addRoundKey(state, roundKeys[0])
for (let j = 1;j <= N_r - 1;j++) {
state = subBytes(state)
state = shiftRows(state)
state = mixColumns(state)
state = addRoundKey(state, roundKeys[j])
}
state = subBytes(state)
state = shiftRows(state)
state = addRoundKey(state, roundKeys[N_r])
encryptedBlocks.push(state)
}
return encryptedBlocks
}
+64
View File
@@ -0,0 +1,64 @@
import { RCons, SBox } from '../constants'
const subWord = (word: number[]): number[] => {
/**
* @param {number[]} word - An an array containing 4 numbers, each number is a hex value represeting one byte.
* @returns {number[]} An array containing the corresponding values in the S-box.
*/
return word.map(byte => SBox[byte])
}
const rotWord = (word: number[]): number[] => {
return word.slice(1).concat(word[0])
}
export const expandKey = (key: number[][]): number[][][] => {
/**
* @param {string} key - A string of 128, 192, or 256 bits.
* @returns {string[]} An array of round keys.
*/
const N_k = key[0].length // Number of columns in the key matrix (4 for 128-bit, 6 for 192-bit, 8 for 256-bit)
const N_r = N_k + 6 // Number of rounds (10 for 128-bit, 12 for 192-bit, 14 for 256-bit)
const totalKeys = 4 * (N_r + 1) // Total number of 4-byte words required
const roundKeys: number[][] = []
// Step 1: Copy the initial key as the first round key (N_k columns)
for (let i = 0;i < N_k;i++) {
roundKeys.push([
key[0][i],
key[1][i],
key[2][i],
key[3][i]
])
}
// Step 2: Expand the key to generate the full round keys
for (let i = N_k;i < totalKeys;i++) {
let temp = roundKeys[i - 1] // Previous word
if (i % N_k === 0) {
temp = subWord(rotWord(temp)) // RotWord + SubWord
temp = temp.map((t, index) => t ^ RCons[i / N_k][index]) // XOR with Rcon
} else if (N_k > 6 && i % N_k === 4) {
temp = subWord(temp) // Apply SubWord for AES-256
}
// XOR with the word N_k positions earlier
roundKeys.push(roundKeys[i - N_k].map((w, index) => w ^ temp[index]))
}
// Step 3: Convert flat array into 4x4 round key matrices
const expandedKeys: number[][][] = []
for (let i = 0;i < totalKeys / 4;i++) {
const matrix: number[][] = [
[roundKeys[i * 4][0], roundKeys[i * 4 + 1][0], roundKeys[i * 4 + 2][0], roundKeys[i * 4 + 3][0]],
[roundKeys[i * 4][1], roundKeys[i * 4 + 1][1], roundKeys[i * 4 + 2][1], roundKeys[i * 4 + 3][1]],
[roundKeys[i * 4][2], roundKeys[i * 4 + 1][2], roundKeys[i * 4 + 2][2], roundKeys[i * 4 + 3][2]],
[roundKeys[i * 4][3], roundKeys[i * 4 + 1][3], roundKeys[i * 4 + 2][3], roundKeys[i * 4 + 3][3]],
]
expandedKeys.push(matrix)
}
return expandedKeys
}
+22
View File
@@ -0,0 +1,22 @@
export const expandKeyFake = (key: number[][]): number[][][] => {
/**
* @param {string} key - A string of 128, 192, or 256 bits.
* @returns {string[]} An array of round keys.
*/
const N_k = key[0].length // Number of columns in the key matrix (4 for 128-bit, 6 for 192-bit, 8 for 256-bit)
const N_r = N_k + 6 // Number of rounds (10 for 128-bit, 12 for 192-bit, 14 for 256-bit)
const totalKeys = 4 * (N_r + 1) // Total number of 4-byte words required
const expandedKeys: number[][][] = []
for (let i = 0;i < totalKeys / 4;i++) {
const matrix: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
expandedKeys.push(matrix)
}
return expandedKeys
}
+13
View File
@@ -0,0 +1,13 @@
export const flattenBlocks = (blocks: number[][][]): number[] => {
const flatArray: number[] = []
for (const block of blocks) {
for (let col = 0; col < 4; col++) {
for (let row = 0; row < 4; row++) {
flatArray.push(block[row][col])
}
}
}
return flatArray
}
+14
View File
@@ -0,0 +1,14 @@
export * from './preprocessPlaintext'
export * from './preprocessKey'
export * from './expandKey'
export * from './expandKeyFake.ts'
export * from './addRoundKey'
export * from './subBytes'
export * from './shiftRows.ts'
export * from './invShiftRows'
export * from './invSubBytes'
export * from './invMixColumns'
export * from './decrypt.ts'
export * from './encrypt.ts'
export * from './flattenBlocks.ts'
export * from './preprocessPlaintextHex.ts'
+39
View File
@@ -0,0 +1,39 @@
import { galoisMultiplyBy2 } from './mixColumns'
const galoisMultiplyBy9 = (value: number): number => {
return galoisMultiplyBy2(galoisMultiplyBy2(galoisMultiplyBy2(value))) ^ value
}
const galoisMultiplyBy11 = (value: number): number => {
return galoisMultiplyBy2(galoisMultiplyBy2(galoisMultiplyBy2(value)) ^ value) ^ value
}
const galoisMultiplyBy13 = (value: number): number => {
return galoisMultiplyBy2(galoisMultiplyBy2(galoisMultiplyBy2(value) ^ value)) ^ value
}
const galoisMultiplyBy14 = (value: number): number => {
return galoisMultiplyBy2(galoisMultiplyBy2(galoisMultiplyBy2(value) ^ value) ^ value)
}
const clampToByte = (value: number): number => {
return value & 0xFF
}
export const invMixColumns = (state: number[][]): number[][] => {
const result: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
for (let col = 0; col < 4; col++) {
result[0][col] = clampToByte(galoisMultiplyBy14(state[0][col]) ^ galoisMultiplyBy11(state[1][col]) ^ galoisMultiplyBy13(state[2][col]) ^ galoisMultiplyBy9(state[3][col]))
result[1][col] = clampToByte(galoisMultiplyBy9(state[0][col]) ^ galoisMultiplyBy14(state[1][col]) ^ galoisMultiplyBy11(state[2][col]) ^ galoisMultiplyBy13(state[3][col]))
result[2][col] = clampToByte(galoisMultiplyBy13(state[0][col]) ^ galoisMultiplyBy9(state[1][col]) ^ galoisMultiplyBy14(state[2][col]) ^ galoisMultiplyBy11(state[3][col]))
result[3][col] = clampToByte(galoisMultiplyBy11(state[0][col]) ^ galoisMultiplyBy13(state[1][col]) ^ galoisMultiplyBy9(state[2][col]) ^ galoisMultiplyBy14(state[3][col]))
}
return result
}
+9
View File
@@ -0,0 +1,9 @@
export const invShiftRows = (state: number[][]): number[][] => {
const result: number[][] = []
for (let i = 0;i < state.length;i++) {
result[i] = state[i].slice(-i).concat(state[i].slice(0, -i))
}
return result
}
+15
View File
@@ -0,0 +1,15 @@
import { invSBox } from '../constants'
export const invSubBytes = (state: number[][]): number[][] => {
const result: number[][] = []
for (let i = 0;i < state.length;i++) {
result[i] = []
for (let j = 0;j < state[i].length;j++) {
const byte = state[i][j]
result[i][j] = invSBox[byte]
}
}
return result
}
+29
View File
@@ -0,0 +1,29 @@
export const galoisMultiplyBy2 = (value: number): number => {
return (value << 1) ^ ((value & 0x80) ? 0x1b : 0x00)
}
export const galoisMultiplyBy3 = (value: number): number => {
return galoisMultiplyBy2(value) ^ value
}
const clampToByte = (value: number): number => {
return value & 0xFF
}
export const mixColumns = (state: number[][]): number[][] => {
const result: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
for (let col = 0; col < 4; col++) {
result[0][col] = clampToByte(galoisMultiplyBy2(state[0][col]) ^ galoisMultiplyBy3(state[1][col]) ^ state[2][col] ^ state[3][col])
result[1][col] = clampToByte(state[0][col] ^ galoisMultiplyBy2(state[1][col]) ^ galoisMultiplyBy3(state[2][col]) ^ state[3][col])
result[2][col] = clampToByte(state[0][col] ^ state[1][col] ^ galoisMultiplyBy2(state[2][col]) ^ galoisMultiplyBy3(state[3][col]))
result[3][col] = clampToByte(galoisMultiplyBy3(state[0][col]) ^ state[1][col] ^ state[2][col] ^ galoisMultiplyBy2(state[3][col]))
}
return result
}
+34
View File
@@ -0,0 +1,34 @@
export const preprocessKey = (key: string): number[][] => {
/**
* @param {string} key - A string of 128, 192, or 256 bits.
* @returns {number[][]} A 4x4/6/8 matrix representing the key.
*/
const encoder = new TextEncoder()
const byteArray = encoder.encode(key)
const validKeyLengths = [128, 192, 256]
if (!validKeyLengths.includes(byteArray.length * 8)) {
throw new Error('Invalid Key!')
}
const N_k = (byteArray.length * 8) / 32
const matrix: number[][] = [
Array(N_k).fill(0),
Array(N_k).fill(0),
Array(N_k).fill(0),
Array(N_k).fill(0)
]
// Fill the matrix in column-major order, i.e., top-to-bottom + left-to-right
// byteArray.length is either 16, 24, or 32.
for (let i = 0;i < byteArray.length;i++) {
const row = i % 4
const col = Math.floor(i / 4)
matrix[row][col] = byteArray[i]
}
return matrix
}
+49
View File
@@ -0,0 +1,49 @@
export type PlaintextBlock = number[][]
export const preprocessPlaintext = (plaintext: string): PlaintextBlock[] => {
/**
* @param {string} plaintext - The plaintext to encrypt.
* @returns {PlaintextBlock[]} Blocks of 128 bits representing the plaintext. Each block is a 4x4 column-major matrix.
*/
const encoder = new TextEncoder()
const byteArray = encoder.encode(plaintext) // Array where each item is an 8-bit representation of a character of the string
const blockSize = 16 // 128 bits = 16 bytes
// Loop through the byteArray and split it into 128-bit blocks
const blocks: Uint8Array[] = []
for (let i = 0;i < byteArray.length;i += blockSize) {
const block = byteArray.slice(i, i + blockSize)
if (block.length < blockSize) {
// Pad the block with zeroes
const paddedBlock = new Uint8Array(blockSize)
paddedBlock.set(block)
blocks.push(paddedBlock)
} else {
blocks.push(block)
}
}
const plaintextBlocks: number[][][] = blocks.map(block => {
// Block is a 128-bit array containing 16 8-bit integers.
const matrix: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
// Fill the matrix in column-major order, i.e., top-to-bottom + left-to-right
for (let i = 0;i < blockSize;i++) {
const row = i % 4
const col = Math.floor(i / 4)
matrix[row][col] = block[i]
}
return matrix
})
return plaintextBlocks
}
+49
View File
@@ -0,0 +1,49 @@
import { PlaintextBlock} from './'
export const preprocessPlaintextHex = (hexPlaintext: string): PlaintextBlock[] => {
/**
* @param {string} hexPlaintext - The plaintext to encrypt, as a hexadecimal string.
* @returns {PlaintextBlock[]} Blocks of 128 bits representing the plaintext. Each block is a 4x4 column-major matrix.
*/
const blockSize = 16 // 128 bits = 16 bytes
// Convert the hex string into a byte array, where each pair of hex digits is a byte
const byteArray = new Uint8Array(hexPlaintext.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)))
// Loop through the byteArray and split it into 128-bit blocks
const blocks: Uint8Array[] = []
for (let i = 0;i < byteArray.length;i += blockSize) {
const block = byteArray.slice(i, i + blockSize)
if (block.length < blockSize) {
// Pad the block with zeroes
const paddedBlock = new Uint8Array(blockSize)
paddedBlock.set(block)
blocks.push(paddedBlock)
} else {
blocks.push(block)
}
}
const plaintextBlocks: number[][][] = blocks.map(block => {
// Block is a 128-bit array containing 16 8-bit integers.
const matrix: number[][] = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
]
// Fill the matrix in column-major order, i.e., top-to-bottom + left-to-right
for (let i = 0;i < blockSize;i++) {
const row = i % 4
const col = Math.floor(i / 4)
matrix[row][col] = block[i]
}
return matrix
})
return plaintextBlocks
}
+9
View File
@@ -0,0 +1,9 @@
export const shiftRows = (state: number[][]): number[][] => {
const result: number[][] = []
for (let i = 0;i < state.length;i++) {
result[i] = state[i].slice(i).concat(state[i].slice(0, i))
}
return result
}
+13
View File
@@ -0,0 +1,13 @@
import { SBox } from '../constants'
export const subBytes = (state: number[][]): number[][] => {
const result: number[][] = []
for (let i = 0;i < state.length;i++) {
result[i] = []
for (let j = 0;j < state[0].length;j++) {
const byte = state[i][j]
result[i][j] = SBox[byte]
}
}
return result
}