暗号で使われる標数2の体の実装
初めに
今回は標数2の体の基本的な実装方法をいくつか紹介します。標数2の体自体の話は暗号で使われる標数2の体を参照ください。
サンプルコードはgf256にあります。
多項式のビット表現
AES本体で使われる8次拡大体
たとえば
ビット位置 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | |
多項式 | 0 | 0 | 0 | 1 |
多項式の加算
2個の多項式
各係数は
typedef uint8_t K;
K add(K a, K b)
{
return a ^ b;
}
K sub(K a, K b)
{
return add(a, b);
}
多項式の乗算
まずはナイーブな実装から考えましょう。
多項式
ここの足し算や掛け算も
まず 体
typdef bool F2;
typedef uint8_t K;
typedef uint16_t K2;
template<class T>
F2 get(T a, int i) { return (a >> i) & 1; }
template<class T>
void set(T& a, int i, F2 b)
{
a &= ~(1ull<<i);
if (b) a |= 1ull<<i;
}
これらを用いてmulPolyを実装します。
K2 mulPoly(K a, K b)
{
const int n = 8;
K2 ret = 0;
for (int i = 0; i < n; i++) {
F2 r = 0;
for (int j = 0; j <= i; j++) r ^= get(a, j) & get(b, i-j);
set(ret, i, r);
}
for (int i = n; i < 2*n-1; i++) {
F2 r = 0;
for (int j = i-n+1; j < n; j++) r ^= get(a, j) & get(b, i-j);
set(ret, i, r);
}
return ret;
}
前半のループが
modPolyにより
割り算の筆算と同じく、多項式の割り算は上の桁(上の次数)から計算します。
たとえば
つまり
次数 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
元の多項式 | 1 | . | . | . | . | . | . | . | . | . | . | . | . | . |
mod後の多項式 | 0 | . | . | . | +1 | +1 | . | +1 | +1 | . | . | . | . | . |
この操作を次数
K modPoly(K2 c)
{
const int n = 8;
for (int i = 2*n-2; i >= n; i--) {
if (get(c, i)) {
c ^= 0x1b << (i-n);
}
}
return c;
}
速度改善
前節は多項式を掛けてからmodしました。そうではなく、掛け算しながらmodすることを考えます。
そのためにまず多項式
掛ける前が7次なら
K mulX(K a)
{
if (a & 0x80) {
// 7次なら0x1bを足す
a = (a << 1) ^ 0x1b;
}else{
// 7次未満なら単にXを掛けるだけ
a <<= 1;
}
return a;
}
多項式
K mul(K a, K b)
{
K ret = 0;
while (b) {
if (b & 1) ret ^= a; // 最下位ビットがあればret += a;
a = mulX(a); // a ← aX
b >>= 1; // 次のビット
}
return ret;
}
最初の多項式を掛けてからmodする場合の演算量はざっくり
テーブル引き(その1)
今回入力が8ビット整数なので
256×256×1バイト=65KiBと少々大きいですが、コードは簡単になります。
static uint8_t g_mulTbl[256 * 256];
// 一度だけテーブル生成
for (uint32_t x = 0; x < 256; x++) {
for (uint32_t y = 0; y < 256; y++) {
g_mulTbl[x + y * 256] = gf256_mul(x, y);
}
}
uint8_t mulTbl(uint8_t x, uint8_t y) // テーブル引きによる乗算
{
return g_mulTbl[x + y * 256];
}
ベンチマークをとるとコードの単純さの割りに10.3clkと前節の2倍少々しか速くなりませんでした。やはりメモリアクセスは負荷が多いようです。
64KiBはテーブルとして大きすぎる場面もあるでしょう。そこでテーブルサイズを512バイトと128分の1にする方法を紹介します。
テーブル引き(その2)
天下りですが、多項式
べき乗を次々と計算すると
0は何乗しても0で変わらないので除外すると、
これを
すると
と変形できます。
実際には次のように書けます。0を除外するので指数の範囲は0以上255未満であることに注意してください。
uint8_t gf256_mul(uint8_t x, uint8_t y)
{
if (x == 0 || y == 0) return 0;
// x y = exp(logX) exp(logY) = exp(logX + logY)
uint8_t logX = logTbl[x - 1];
uint8_t logY = logTbl[y - 1];
uint32_t z = logX + logY;
// mod 255
if (z >= 255) z -= 255;
assert(z < 255);
return expTbl[z];
}
指数と対数のテーブルはそれぞれ256個(厳密には255個)なので、合計512バイトです。ベンチマークを取ると全てテーブル引きした場合の1.6~1.8倍の遅さできた。
テーブルサイズは100分の1になりましたが3回テーブル引きしているので遅くなりました。
除算は、
しかし、指数と対数のテーブルを用意しておけば、
まとめ
アーキテクチャによってどれがよいかは変わるでしょう。次回はx64における専用命令を使った実装を紹介します。
Discussion