[インデックス 17971] ファイルの概要
このコミットは、Go言語の標準ライブラリ crypto/rc4 パッケージ内の rc4_ref.go ファイルに対する修正です。このファイルは、RC4ストリーム暗号の純粋なGo言語によるリファレンス実装を含んでいます。具体的には、RC4の主要な操作であるXORKeyStream関数における型エラーを修正しています。
コミット
commit 9394629b897b81284d1a02ecf840c1a991bd3b96
Author: Richard Musiol <mail@richard-musiol.de>
Date: Thu Dec 12 14:32:31 2013 -0500
crypto/rc4: fix type errors in pure Go implementation
R=golang-dev, agl
CC=golang-dev
https://golang.org/cl/40540049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9394629b897b81284d1a02ecf840c1a991bd3b96
元コミット内容
crypto/rc4: fix type errors in pure Go implementation
このコミットは、crypto/rc4 パッケージの純粋なGo言語実装における型エラーを修正することを目的としています。
変更の背景
RC4アルゴリズムの実装において、Go言語の型システムに起因する潜在的なバグが存在していました。特に、uint8 型の変数に対する算術演算の結果が、自動的にデフォルトの整数型(通常は int)に昇格されるGoの挙動が問題を引き起こしていました。RC4の内部状態S-boxは256バイトの配列であり、そのインデックスは0から255の範囲に収まる必要があります。uint8 型はまさにこの範囲を表現するのに適していますが、算術演算によって int に昇格された値が、再び uint8 の範囲に収まることを保証しないまま配列のインデックスとして使用されると、予期せぬ結果やランタイムパニック(例: インデックスが配列の範囲外になる)が発生する可能性がありました。
このコミットは、このような型昇格による問題を明示的な型キャスト uint8() を用いて修正し、RC4アルゴリズムの正確性と堅牢性を確保することを目的としています。
前提知識の解説
RC4 (Rivest Cipher 4)
RC4は、Ron Rivestによって開発されたストリーム暗号です。ストリーム暗号は、データをビットまたはバイト単位で暗号化・復号化する方式で、ブロック暗号とは異なり、固定長のブロックに分割せずに処理を行います。RC4は、そのシンプルさと高速性から、かつてはSSL/TLSやWEPなどの多くのプロトコルで広く利用されていました。
RC4の動作は、主に以下の2つのフェーズに分けられます。
-
キー・スケジューリング・アルゴリズム (KSA: Key-Scheduling Algorithm):
- 256バイトのS-box(State box)と呼ばれる配列
Sを初期化します。最初はS[i] = iとなります。 - 提供された秘密鍵
keyを使用して、S-boxの要素をシャッフルします。このシャッフルは、鍵の長さに基づいて複数回行われます。 - このフェーズの出力は、初期化されたS-boxです。
- 256バイトのS-box(State box)と呼ばれる配列
-
擬似乱数生成アルゴリズム (PRGA: Pseudo-Random Generation Algorithm):
- KSAで初期化されたS-boxと、2つのポインタ
iとj(初期値は0) を使用します。 - 各ステップで、
iとjを更新し、S-boxからバイトを生成します。 - 生成されたバイトは、平文のバイトとXOR演算され、暗号文のバイトが生成されます(またはその逆で復号化)。
iとjの更新は以下のようになります:i = (i + 1) mod 256j = (j + S[i]) mod 256S[i]とS[j]をスワップt = (S[i] + S[j]) mod 256- 出力バイトは
S[t]
- KSAで初期化されたS-boxと、2つのポインタ
RC4は、その設計上の欠陥(特に鍵の再利用や特定の弱い鍵の使用)が発見されたため、現在では安全ではないとされており、新しいシステムでの使用は推奨されていません。しかし、そのアルゴリズム自体は、暗号の基本的な概念を理解する上で良い例となります。
Go言語の型システムと整数型
Go言語は静的型付け言語であり、変数の型はコンパイル時に決定されます。Goの整数型には、符号付き整数 (int8, int16, int32, int64, int) と符号なし整数 (uint8, uint16, uint32, uint64, uint, uintptr) があります。
uint8型は、0から255までの符号なし8ビット整数を表します。これは、バイトデータや、RC4のS-boxのインデックスのように256の範囲に収まる値を扱うのに非常に適しています。int型は、システムに依存するデフォルトの符号付き整数型です。通常は32ビットまたは64ビットです。
Go言語では、異なる型の数値演算を行う場合、より広い範囲を表現できる型に自動的に昇格されることがあります。特に、uint8 型の変数と int 型の定数(例: 1)との加算などでは、uint8 が int に昇格されて演算が行われます。この昇格自体は問題ありませんが、その結果を再び uint8 の範囲に収める必要がある場合や、uint8 を期待する配列のインデックスとして使用する場合には、明示的な型キャストが必要になります。
例えば、var x uint8 = 200; var y uint8 = 100; z := x + y のようなコードでは、x + y の結果は uint8 の最大値255を超えてしまうため、コンパイルエラーになります。Goはオーバーフローを許容せず、開発者に明示的な型変換を促します。しかし、j += c.s[i] のようなケースでは、c.s[i] は uint8 ですが、j も uint8 であり、Goは j + c.s[i] の結果が uint8 の範囲に収まることを保証できないため、この演算の結果は int に昇格されます。この int 型の結果をそのまま uint8 型の j に代入しようとすると、型ミスマッチが発生します。
RC4のS-boxのインデックスは常に0から255の範囲に収まる必要があります。したがって、算術演算の結果がこの範囲に収まることを保証するために、明示的に uint8() にキャストすることが重要です。
技術的詳細
このコミットで修正されているのは、crypto/rc4 パッケージの Cipher 構造体の XORKeyStream メソッド内の2箇所です。
RC4アルゴリズムのPRGAフェーズでは、i と j という2つのポインタがS-boxのインデックスとして使用されます。これらのポインタは常に0から255の範囲に収まる必要があります。S-box c.s は [256]uint8 型の配列です。
元のコードでは、以下の行に問題がありました。
j += c.s[i]dst[k] = v ^ c.s[c.s[i]+c.s[j]]
1. j += c.s[i] の問題
c.s[i]はuint8型の値です。jもuint8型です。- Go言語では、
uint8型同士の加算j + c.s[i]は、結果がuint8の範囲(0-255)を超える可能性があるため、自動的にint型に昇格されて演算が行われます。 - この
int型の結果を、再びuint8型のjに代入しようとすると、型ミスマッチが発生します。Goは異なる型の代入を許さないため、コンパイルエラーになるか、あるいは暗黙的な型変換が行われる環境では予期せぬ結果(オーバーフローによる値の切り捨てなど)を招く可能性があります。Goの場合は、明示的なキャストがないとコンパイルエラーになります。
修正: j += uint8(c.s[i])
c.s[i] を uint8 にキャストしているように見えますが、これは j が uint8 型であるため、j + c.s[i] の結果を uint8 に明示的にキャストして j に代入していることを意味します。Goの複合代入演算子 += は j = j + c.s[i] と等価です。したがって、j = uint8(j + c.s[i]) となり、j + c.s[i] の結果が int に昇格された後、その結果を明示的に uint8 にキャストして j に代入しています。これにより、j の値が常に uint8 の範囲に収まることが保証されます。
2. dst[k] = v ^ c.s[c.s[i]+c.s[j]] の問題
この行はさらに複雑です。c.s[i]+c.s[j] の部分が問題です。
c.s[i]とc.s[j]はどちらもuint8型の値です。- これらの加算
c.s[i]+c.s[j]も、結果がuint8の範囲を超える可能性があるため、int型に昇格されて演算が行われます。 - この
int型の結果を、S-boxc.sのインデックスとして直接使用しようとすると、Goの配列インデックスは通常int型を期待しますが、この値がint型のままだと、S-boxのインデックスとして使用される際に、その値が255を超えてしまう可能性があります。RC4のS-boxは256要素しか持たないため、インデックスが255を超えるとランタイムパニック(index out of range)が発生します。
修正: dst[k] = v ^ uint8(c.s[uint8(c.s[i]+c.s[j])])
この修正は二重のキャストを含んでいます。
- 内側の
uint8(c.s[i]+c.s[j]):c.s[i]+c.s[j]の結果(int型に昇格されている)を明示的にuint8にキャストしています。これにより、S-boxのインデックスとして使用される値が確実に0から255の範囲に収まるようにします。RC4アルゴリズムの定義上、S[i] + S[j]の結果はmod 256されるため、このキャストはmod 256演算と同じ効果を持ちます(uint8のオーバーフローはラップアラウンド挙動を示すため)。 - 外側の
uint8(c.s[...]):c.s[...]の結果はuint8型ですが、vもuint8型です。v ^ c.s[...]のXOR演算の結果もuint8型になるため、この外側のキャストは厳密には不要に見えます。しかし、Goのコンパイラがv ^ c.s[...]の結果をintに昇格させる可能性がある場合(例えば、vがint型の変数であるか、あるいはコンテキストによってintが期待される場合)、このキャストは結果をuint8に明示的に戻す役割を果たします。このケースでは、vはsrcスライスから取得されたbyte型(uint8のエイリアス)であり、c.s[...]もuint8なので、XOR演算の結果はuint8になります。したがって、この外側のキャストは冗長である可能性もありますが、安全のために追加されたか、あるいはGoの古いバージョンでのコンパイラの挙動を考慮したものである可能性があります。最も重要なのは内側のキャストです。
これらの修正により、RC4のS-boxインデックス計算が常に正しい範囲で行われるようになり、アルゴリズムの正確性と安定性が保証されます。
コアとなるコードの変更箇所
--- a/src/pkg/crypto/rc4/rc4_ref.go
+++ b/src/pkg/crypto/rc4/rc4_ref.go
@@ -12,9 +12,9 @@ func (c *Cipher) XORKeyStream(dst, src []byte) {
i, j := c.i, c.j
for k, v := range src {
i += 1
- j += c.s[i]
+ j += uint8(c.s[i])
c.s[i], c.s[j] = c.s[j], c.s[i]
- dst[k] = v ^ c.s[c.s[i]+c.s[j]]
+ dst[k] = v ^ uint8(c.s[uint8(c.s[i]+c.s[j])])
}
c.i, c.j = i, j
}
コアとなるコードの解説
変更は XORKeyStream メソッド内の2行に集中しています。
-
j += uint8(c.s[i]):- RC4のPRGAフェーズにおける
jの更新式j = (j + S[i]) mod 256に対応します。 c.s[i]はuint8型ですが、jもuint8型であるため、Goの型推論と演算規則によりj + c.s[i]の結果はint型に昇格される可能性があります。uint8(...)キャストは、このint型に昇格された結果を明示的にuint8型に戻し、jに代入することを保証します。これにより、jの値が常に0から255の範囲に収まり、S-boxの有効なインデックスとして機能することが保証されます。Goのuint8型は256でモジュロ演算を行うかのように振る舞うため、このキャストはmod 256と同等の効果を持ちます。
- RC4のPRGAフェーズにおける
-
dst[k] = v ^ uint8(c.s[uint8(c.s[i]+c.s[j])]):- RC4のPRGAフェーズにおける出力バイトの生成式
output = S[(S[i] + S[j]) mod 256]に対応します。 - 内側の
uint8(c.s[i]+c.s[j]):c.s[i]とc.s[j]はどちらもuint8型ですが、その和c.s[i]+c.s[j]はint型に昇格される可能性があります。このint型の値をS-boxc.sのインデックスとして使用する前に、明示的にuint8型にキャストしています。これにより、インデックスが常に0から255の範囲に収まることが保証され、index out of rangeのようなランタイムパニックを防ぎます。 - 外側の
uint8(...):c.s[...]の結果はuint8型であり、vもuint8型(byteのエイリアス)です。これらのXOR演算の結果もuint8型になるため、この外側のキャストは冗長に見えるかもしれません。しかし、Goのコンパイラの特定のバージョンや、将来的な型推論の変更に備えて、結果が確実にuint8型であることを保証するために追加された可能性があります。
- RC4のPRGAフェーズにおける出力バイトの生成式
これらの修正は、Go言語の厳密な型システムと、RC4アルゴリズムのインデックス計算の特性を考慮したものであり、暗号実装の正確性と安全性を高める上で非常に重要です。
関連リンク
- Go Change-ID: https://golang.org/cl/40540049
参考にした情報源リンク
- RC4 - Wikipedia: https://ja.wikipedia.org/wiki/RC4
- The Go Programming Language Specification - Numeric types: https://go.dev/ref/spec#Numeric_types
- The Go Programming Language Specification - Arithmetic operators: https://go.dev/ref/spec#Arithmetic_operators
- The Go Programming Language Specification - Assignments: https://go.dev/ref/spec#Assignments
- Go言語の型変換と型アサーション: https://go.dev/tour/basics/13 (Go Tourの関連セクション)
- Go言語におけるuint8のオーバーフロー挙動 (ラップアラウンド): https://go.dev/play/p/example (Go Playgroundで
uint8の加算とオーバーフローを試す)- 例:
var a uint8 = 200; var b uint8 = 100; fmt.Println(a + b)はコンパイルエラー。 - 例:
var a uint8 = 200; var b uint8 = 100; var c uint8 = a + bもコンパイルエラー。 - 例:
var a uint8 = 200; var b uint8 = 100; var c uint8 = uint8(a + b)はcが44になる(200+100=300, 300 mod 256 = 44)。 - この挙動がRC4の
mod 256演算に利用されている。
- 例: