前回(S-Boxとガロア体について)
前々回(AES暗号各種用語)
この記事では暗号化のラウンド鍵の生成処理についてまとめます。
鍵スケジュール、 鍵の拡張 などとも呼ばれますが、各ラウンドで使用するラウンド鍵を秘密鍵を元に生成します。
SubWord() は4バイトのそれぞれに S-Box の変換を行います。
uint SubWord(uint x)
{
byte[] sBox = SBoxTable.SBox;
return sBox[x & 255]
| ((uint) sBox[(x >> 8) & 255] << 8)
| ((uint) sBox[(x >> 16) & 255] << 16)
| ((uint) sBox[(x >> 24) & 255] << 24);
}
RotWord() は4バイトのデータを左方向に循環シフトさせます。
uint RotWord(uint r)
{
return (r << 8) | ((r >> 24) & 0xFF);
}
ラウンド毎に RCon という定数を加算します。
ラウンド定数は以下のように定義されています。
public static uint[] RCon { get; } =
{
0x00000000, // [00000000000000000000000000000000]
0x01000000, // [00000001000000000000000000000000]
0x02000000, // [00000010000000000000000000000000]
0x04000000, // [00000100000000000000000000000000]
0x08000000, // [00001000000000000000000000000000]
0x10000000, // [00010000000000000000000000000000]
0x20000000, // [00100000000000000000000000000000]
0x40000000, // [01000000000000000000000000000000]
0x80000000, // [10000000000000000000000000000000]
0x1b000000, // [00011011000000000000000000000000]
0x36000000, // [00110110000000000000000000000000]
0x6c000000, // [01101100000000000000000000000000]
0xd8000000, // [11011000000000000000000000000000]
0xab000000, // [10101011000000000000000000000000]
0x4d000000, // [01001101000000000000000000000000]
0x9a000000, // [10011010000000000000000000000000]
0x2f000000, // [00101111000000000000000000000000]
0x5e000000, // [01011110000000000000000000000000]
0xbc000000, // [10111100000000000000000000000000]
0x63000000, // [01100011000000000000000000000000]
0xc6000000, // [11000110000000000000000000000000]
0x97000000, // [10010111000000000000000000000000]
0x35000000, // [00110101000000000000000000000000]
0x6a000000, // [01101010000000000000000000000000]
0xd4000000, // [11010100000000000000000000000000]
0xb3000000, // [10110011000000000000000000000000]
0x7d000000, // [01111101000000000000000000000000]
0xfa000000, // [11111010000000000000000000000000]
0xef000000, // [11101111000000000000000000000000]
0xc5000000, // [11000101000000000000000000000000]
};
ラウンド鍵生成(128bit)
流れとしては以下のような感じで鍵長が128bitであれば4Word周期でRotWord + SubWord + Rconによる鍵生成を行い、その演算結果と直前のブロックのXORした値を次の鍵とします。

void InitRoundKeys128(byte[] key, out NativeArray<uint> oRoundKeys, out int oRounds)
{
const int rounds = 10;
oRounds = rounds;
oRoundKeys = new NativeArray<uint>((rounds + 1) * NUM_KEY, Allocator.Persistent);
uint* w = GetRoundKeyPtr(oRoundKeys, 0);
uint t0 = ToUInt32(key.AsSpan(0));
uint t1 = ToUInt32(key.AsSpan(4));
uint t2 = ToUInt32(key.AsSpan(8));
uint t3 = ToUInt32(key.AsSpan(12));
// 初期ラウンド
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
for (int i = 1; i <= rounds; ++i)
{
uint u = SubWord(RotWord(t3)) ^ RCon[i];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
w = GetRoundKeyPtr(oRoundKeys, i);
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
}
}
ラウンド鍵生成(192bit)
192bit鍵の場合は6Word周期で鍵生成が行われます。
少し鍵の生成タイミングが複雑でわかりづらいので私は鍵長別に関数を分けて実装しています。
(BouncyCastleの実装がそんな感じだったのでそれを参考にしています)

static void InitRoundKeys192(byte[] key, out NativeArray<uint> oRoundKeys, out int oRounds)
{
const int rounds = 12;
oRounds = rounds;
oRoundKeys = new NativeArray<uint>((rounds + 1) * NUM_KEY, Allocator.Persistent);
uint* w = GetRoundKeyPtr(oRoundKeys, 0);
uint t0 = ToUInt32(key.AsSpan(0));
uint t1 = ToUInt32(key.AsSpan(4));
uint t2 = ToUInt32(key.AsSpan(8));
uint t3 = ToUInt32(key.AsSpan(12));
uint t4 = ToUInt32(key.AsSpan(16));
uint t5 = ToUInt32(key.AsSpan(20));
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
w = GetRoundKeyPtr(oRoundKeys, 1);
w[0] = t4;
w[1] = t5;
int rconIndex = 1;
uint u = SubWord(RotWord(t5)) ^ RCon[rconIndex++];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
t4 ^= t3;
t5 ^= t4;
w[2] = t0;
w[3] = t1;
w = GetRoundKeyPtr(oRoundKeys, 2);
w[0] = t2;
w[1] = t3;
w[2] = t4;
w[3] = t5;
for (int i = 3; i < rounds; i += 3)
{
u = SubWord(RotWord(t5)) ^ RCon[rconIndex++];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
t4 ^= t3;
t5 ^= t4;
w = GetRoundKeyPtr(oRoundKeys, i);
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
w = GetRoundKeyPtr(oRoundKeys, i + 1);
w[0] = t4;
w[1] = t5;
u = SubWord(RotWord(t5)) ^ RCon[rconIndex++];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
t4 ^= t3;
t5 ^= t4;
w[2] = t0;
w[3] = t1;
w = GetRoundKeyPtr(oRoundKeys, i + 2);
w[0] = t2;
w[1] = t3;
w[2] = t4;
w[3] = t5;
}
u = SubWord(RotWord(t5)) ^ RCon[rconIndex];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
w = GetRoundKeyPtr(oRoundKeys, 12);
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
}
ラウンド鍵生成(256bit)
256bit鍵の場合は8Word周期で鍵生成が行われますが、128bit、192bitとは異なり
鍵を4Word生成した後にもう一度直前のWordを使ってSubWordのみが行われるタイミングがあります。

void InitRoundKeys256(byte[] key, out NativeArray<uint> oRoundKeys, out int oRounds)
{
const int rounds = 14;
oRounds = rounds;
oRoundKeys = new NativeArray<uint>((rounds + 1) * NUM_KEY, Allocator.Persistent);
uint* w = GetRoundKeyPtr(oRoundKeys, 0);
uint t0 = ToUInt32(key.AsSpan(0));
uint t1 = ToUInt32(key.AsSpan(4));
uint t2 = ToUInt32(key.AsSpan(8));
uint t3 = ToUInt32(key.AsSpan(12));
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
w = GetRoundKeyPtr(oRoundKeys, 1);
uint t4 = ToUInt32(key.AsSpan(16));
uint t5 = ToUInt32(key.AsSpan(20));
uint t6 = ToUInt32(key.AsSpan(24));
uint t7 = ToUInt32(key.AsSpan(28));
w[0] = t4;
w[1] = t5;
w[2] = t6;
w[3] = t7;
uint u;
int roundConstantIndex = 1;
for (int i = 2; i < rounds; i += 2)
{
u = SubWord(RotWord(t7)) ^ RCon[roundConstantIndex++];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
w = GetRoundKeyPtr(oRoundKeys, i);
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
u = SubWord(t3);
t4 ^= u;
t5 ^= t4;
t6 ^= t5;
t7 ^= t6;
w = GetRoundKeyPtr(oRoundKeys, i + 1);
w[0] = t4;
w[1] = t5;
w[2] = t6;
w[3] = t7;
}
w = GetRoundKeyPtr(oRoundKeys, 14);
u = SubWord(RotWord(t7)) ^ RCon[roundConstantIndex];
t0 ^= u;
t1 ^= t0;
t2 ^= t1;
t3 ^= t2;
w[0] = t0;
w[1] = t1;
w[2] = t2;
w[3] = t3;
}