Ran tests (everything worked)

Renamed a variable
Implemented a custom AES function as "Rijndael128"
  - Built in key derivation at the moment (PBKDF2-HMAC-SHA1)
  - Added built-in string encryption/decryption
  - All encryption functions compute the results using the underlying mathematical models instead of using tables, simply to demonstrate the inner workings of AES
Simplified some methods for readability and convenience
Removed unnecessary use of BigInteger
Replaced manually (and terribly) calculated Galois field arithmetic with Galois2 methods to make the code simpler and more understandable
This commit is contained in:
Gabriel Tofvesson 2018-02-21 22:40:17 +01:00
parent e6926b9fff
commit 22df2e7fcb
4 changed files with 241 additions and 53 deletions

View File

@ -9,7 +9,12 @@ namespace Client
{
static void Main(string[] args)
{
byte[] test = SHA.SHA1(Encoding.UTF8.GetBytes("Hello there!"));
Rijndael128 symcrypt = new Rijndael128("Eyy");
byte[] testMSG = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
string cryptres = symcrypt.DecryptString(symcrypt.EncryptString("Hello!"), 6);
RandomProvider provider = new CryptoRandomProvider();
byte[] test = KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes("Hello there!"), new byte[] { 1, 2, 3, 4 }, 4096, 128);
Console.WriteLine(Support.ToHexString(test));
Galois2 gal = Galois2.FromValue(33);
Console.WriteLine(gal.ToString());

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
@ -131,42 +130,195 @@ namespace Tofvesson.Crypto
}
}
public static class AESFunctions
public class Rijndael128
{
// Substitution box generated for all 256 possible input bytes from a part of a state
// Generated by getting the multiplicative inverse over GF(2^8) (i.e. the "prime polynomial" x^8 + x^4 + x^3 + x + 1) and applying the affine transformation
// Used to increase diffusion during encryption, but affine is also used to increase confusion by preventing mathematically aimed attacks
private static readonly byte[] rijndael_sbox = new byte[]
{
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
};
protected readonly byte[] roundKeys;
protected readonly byte[] key;
protected readonly byte[] iv;
// MixColumns matrix basis. Used for multiplication over GF(2^8) i.e. mod P(x) where P(x) = x^8 + x^4 + x^3 + x + 1
public Rijndael128(string key)
{
// Derive a proper key
var t = DeriveKey(key);
this.key = t.Item1;
this.iv = t.Item2;
// Expand the derived key
roundKeys = KeySchedule(this.key, BitMode.Bit128);
}
protected Rijndael128(byte[] key, byte[] iv)
{
this.key = key;
this.iv = iv;
// Expand the derived key
roundKeys = KeySchedule(this.key, BitMode.Bit128);
}
public byte[] EncryptString(string message) => Encrypt(Encoding.UTF8.GetBytes(message));
public string DecryptString(byte[] message, int length) => new string(Encoding.UTF8.GetChars(Decrypt(message, length, false))).Substring(0, length);
public byte[] Encrypt(byte[] message)
{
byte[] result = new byte[message.Length + ((16 - (message.Length % 16))%16)];
Array.Copy(message, result, message.Length);
for(int i = 0; i<result.Length/16; ++i)
Array.Copy(AES128_Encrypt(result.SubArray(i * 16, i * 16 + 16)), 0, result, i * 16, 16);
return result;
}
public byte[] Decrypt(byte[] message, int messageLength) => Decrypt(message, messageLength, true);
protected byte[] Decrypt(byte[] message, int messageLength, bool doTruncate)
{
if (message.Length % 16 != 0) throw new SystemException("Invalid encrypted message length!");
byte[] result = new byte[message.Length];
Array.Copy(message, result, message.Length);
for (int i = 0; i < result.Length / 16; ++i)
Array.Copy(AES128_Decrypt(result.SubArray(i * 16, i * 16 + 16)), 0, result, i * 16, 16);
return doTruncate ? result.SubArray(0, messageLength) : result;
}
protected virtual byte[] AES128_Encrypt(byte[] state)
{
// Initial round
state = AddRoundKey(state, roundKeys, 0);
// Rounds 1 - 9
for (int rounds = 1; rounds < 10; ++rounds)
{
state = ShiftRows(SubBytes(state, false));
if (rounds != 9) state = MixColumns(state, true);
state = AddRoundKey(state, roundKeys, rounds * 16);
}
return state;
}
protected virtual byte[] AES128_Decrypt(byte[] state)
{
for (int rounds = 9; rounds > 0; --rounds)
{
state = AddRoundKey(state, roundKeys, rounds * 16);
if (rounds != 9) state = MixColumns(state, false);
state = SubBytes(UnShiftRows(state), true);
}
return AddRoundKey(state, roundKeys, 0);
}
public void Save(string baseName, bool force = false)
{
if (force || !File.Exists(baseName + ".key")) File.WriteAllBytes(baseName + ".key", key);
if (force || !File.Exists(baseName + ".iv")) File.WriteAllBytes(baseName + ".iv", iv);
}
public byte[] Serialize() => Support.SerializeBytes(new byte[][] { key, iv });
public static Rijndael128 Deserialize(byte[] message, out int read)
{
byte[][] output = Support.DeserializeBytes(message, 2);
read = output[0].Length + output[1].Length + 8;
return new Rijndael128(output[0], output[1]);
}
public static Rijndael128 Load(string baseName)
{
if (!File.Exists(baseName + ".iv") || !File.Exists(baseName + ".key")) throw new SystemException("Required files could not be located");
return new Rijndael128(File.ReadAllBytes(baseName + ".key"), File.ReadAllBytes(baseName + ".iv"));
}
// Internal methods for encryption :)
private static uint KSchedCore(uint input, int iteration)
{
input = Rotate(input);
byte[] bytes = Support.WriteToArray(new byte[4], input, 0);
for (int i = 0; i < bytes.Length; ++i) bytes[i] = SBox(bytes[i]);
bytes[bytes.Length - 1] ^= RCON(iteration);
return (uint)Support.ReadInt(bytes, 0);
}
public enum BitMode { Bit128, Bit192, Bit256 }
private static byte[] KeySchedule(byte[] key, BitMode mode)
{
int n = mode == BitMode.Bit128 ? 16 : mode == BitMode.Bit192 ? 24 : 32;
int b = mode == BitMode.Bit128 ? 176 : mode == BitMode.Bit192 ? 208 : 240;
byte[] output = new byte[b];
Array.Copy(key, output, n);
int rcon_iter = 1;
int accruedBytes = n;
while (accruedBytes < b)
{
// Generate 4 new bytes of extended key
byte[] t = Support.WriteToArray(new byte[4], KSchedCore((uint)Support.ReadInt(output, accruedBytes - 4), rcon_iter), 0);
++rcon_iter;
for (int i = 0; i < 4; ++i) t[i] ^= output[accruedBytes - n + i];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
// Generate 12 new bytes of extended key
for (int i = 0; i < 3; ++i)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] ^= output[accruedBytes - n + j];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
// Special processing for 256-bit key schedule
if (mode == BitMode.Bit256)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] = (byte)(SBox(t[j]) ^ output[accruedBytes - n + j]);
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
// Special processing for 192-bit key schedule
if (mode != BitMode.Bit128)
for (int i = mode == BitMode.Bit192 ? 1 : 2; i >= 0; --i)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] ^= output[accruedBytes - n + j];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
}
Console.WriteLine(Support.ArrayToString(output));
return output;
}
// MixColumns matrix basis. Used for multiplication over the rijndael field
private static readonly byte[] mix_matrix = new byte[] { 2, 3, 1, 1 };
private static readonly byte[] unmix_matrix = new byte[] { 14, 11, 13, 9 };
/// <summary>
/// Rijndael substitution step in the encryption (first thing that happens). This supplied confusion for the algorithm
/// Rijndael substitution step in the encryption (first thing that happens). This supplies confusion for the algorithm
/// </summary>
/// <param name="state">The AES state</param>
/// <returns>The substituted bytes for the given state</returns>
public static byte[] SBox(byte[] state)
/// <param name="b">The value (most likely from the AES state) that should be substituted</param>
/// <returns>The substituted byte</returns>
private static byte SBox(byte b) => Affine(new Galois2(new byte[] { b }).InvMul().ToByteArray()[0]);
// Inverse SBox-function
private static byte ISBox(byte b) => new Galois2(new byte[] { Rffine(b) }).InvMul().ToByteArray()[0];
// Replaces GF(2^8) matrix multiplication for the affine and reverse affine functions
private static byte Affine(byte value) => (byte)(value ^ Rot(value, 1) ^ Rot(value, 2) ^ Rot(value, 3) ^ Rot(value, 4) ^ 0b0110_0011);
private static byte Rffine(byte value) => (byte)(Rot(value, 1) ^ Rot(value, 3) ^ Rot(value, 6) ^ 0b0000_0101);
// Rotate bitss
private static byte Rot(byte value, int by) => (byte)((value << by) | (value >> (8 - by)));
private delegate byte SBOXFunc(byte b);
private static byte[] SubBytes(byte[] state, bool reverse)
{
for (int i = 0; i < state.Length; ++i) state[i] = rijndael_sbox[state[i]];
SBOXFunc v;
if (reverse) v = ISBox;
else v = SBox;
for (int i = 0; i < state.Length; ++i) state[i] = v(state[i]);
return state;
}
@ -189,35 +341,59 @@ namespace Tofvesson.Crypto
/// <returns>The shifted matrix</returns>
public static byte[] ShiftRows(byte[] state)
{
for(int i = 1; i<4; ++i)
for (int i = 1; i < 4; ++i)
{
byte tmp = state[i];
for(int j = 0; j<3; ++j) state[i + j*4] = state[i + ((j + i)%4)*4];
state[i + 12] = tmp;
uint value = GetRow(state, i);
for (int j = 0; j < i; ++j) value = Rotate(value);
WriteToRow(value, state, i);
}
return state;
}
private static byte[] UnShiftRows(byte[] state)
{
for (int i = 1; i < 4; ++i)
{
uint value = GetRow(state, i);
for (int j = 3; j >= i; --j) value = Rotate(value);
WriteToRow(value, state, i);
}
return state;
}
private static void WriteToRow(uint value, byte[] to, int row)
{
to[row] = (byte)(value & 255);
to[row + 4] = (byte)((value >> 8) & 255);
to[row + 8] = (byte)((value >> 16) & 255);
to[row + 12] = (byte)((value >> 24) & 255);
}
private static uint GetRow(byte[] from, int row) => (uint)(from[row] | (from[row + 4] << 8) | (from[row + 8] << 16) | (from[row + 12] << 24));
/// <summary>
/// MixColumns adds diffusion to the algorithm. Performs matrix multiplication under GF(2^8) with the irreducible prime 0x11B (x^8 + x^4 + x^3 + x + 1)
/// </summary>
/// <param name="state"></param>
/// <returns>A matrix-multiplied and limited state (mixed)</returns>
public static byte[] MixColumns(byte[] state)
private static byte[] MixColumns(byte[] state, bool mix)
{
byte[] res = new byte[16];
byte[] rowGenerator = mix ? mix_matrix : unmix_matrix;
// Simplified matrix multiplication under GF(2^8)
for(int i = 0; i<4; ++i)
for (int i = 0; i < 4; ++i)
{
for(int j = 0; j<4; ++j)
for (int j = 0; j < 4; ++j)
{
for (int k = 0; k < 4; ++k)
{
int idx = 4 - j;
int r = ((state[k + i * 4] * (mix_matrix[(k + idx) % 4] & 1)) ^ ((state[k + i * 4] << 1) * ((mix_matrix[(k + idx) % 4]>>1)&1)));
if (r > 0b100011011) r ^= 0b100011011;
res[j + i * 4] ^= (byte) r;
Galois2 g = Galois2.FromValue(state[k + i * 4]);
res[j + i * 4] ^= g.Multiply(Galois2.FromValue(rowGenerator[(k + idx) % 4])).ToByteArray()[0];
//int r = ((state[k + i * 4] * (mix_matrix[(k + idx) % 4] & 1)) ^ ((state[k + i * 4] << 1) * ((mix_matrix[(k + idx) % 4]>>1)&1)));
//if (r > 0b100011011) r ^= 0b100011011;
//res[j + i * 4] ^= (byte) r;
}
}
}
@ -230,9 +406,9 @@ namespace Tofvesson.Crypto
/// <param name="state">The state to introduce the roundkey to</param>
/// <param name="subkey">The subkey</param>
/// <returns>The state where the roundkey has been added</returns>
public static byte[] AddRoundKey(byte[] state, byte[] subkey)
private static byte[] AddRoundKey(byte[] state, byte[] subkey, int offset)
{
for (int i = 0; i < state.Length; ++i) state[i] ^= subkey[i];
for (int i = 0; i < state.Length; ++i) state[i] ^= subkey[i + offset];
return state;
}
@ -241,22 +417,22 @@ namespace Tofvesson.Crypto
/// </summary>
/// <param name="i"></param>
/// <returns>Rotated value</returns>
public static int Rotate(int i) => ((i >> 24) & 255) & ((i << 8) & ~255);
private static uint Rotate(uint i) => (uint)(((i >> 24) & 255) | ((i << 8) & ~255));
/// <summary>
/// KDF for a given input string.
/// </summary>
/// <param name="message">Input string to derive key from</param>
/// <returns>A key and an IV</returns>
public static Tuple<byte[], byte[]> DeriveKey(string message) =>
new Tuple<byte[], byte[]>(
new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(message)).ToLength(128),
new RegularRandomProvider(new Random((int)(new BigInteger(Encoding.UTF8.GetBytes(message)) % int.MaxValue))).GetBytes(new byte[128])
);
private static byte RCON(int i) => i<=0?(byte)0x8d:new Galois2(i-1, Galois2.RijndaelIP).ToByteArray()[0];
}
private static Tuple<byte[], byte[]> DeriveKey(string message)
{
byte[] salt = new CryptoRandomProvider().GetBytes(16); // Get a random 16-byte salt
byte[] key = KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(message), salt, 4096, 16); // Generate a 16-byte (128-bit) key from salt over 4096 iterations of HMAC-SHA1
return new Tuple<byte[], byte[]>(key, salt);
}
private static byte RCON(int i) => i <= 0 ? (byte)0x8d : new Galois2(i - 1).ToByteArray()[0];
}
/// <summary>
/// Object representation of a Galois Field with characteristic 2

View File

@ -46,7 +46,7 @@ namespace Tofvesson.Crypto
for(int i = 1; i<iterations; ++i)
{
u = function(password, u);
for (int i = 0; i < u.Length; ++i) ures[i] ^= u[i];
for (int j = 0; j < u.Length; ++j) ures[j] ^= u[j];
}
dk = Support.Concatenate(dk, ures);

View File

@ -328,6 +328,13 @@ namespace Tofvesson.Crypto
return output;
}
public static T[] SubArray<T>(this T[] array, int start, int end)
{
T[] res = new T[end-start];
for (int i = start; i < end; ++i) res[i - start] = array[i];
return res;
}
// -- Misc --
// Allows deconstruction when iterating over a collection of Tuples