[インデックス 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 256
j = (j + S[i]) mod 256
S[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
演算に利用されている。
- 例: