Golangのセキュリティ
Goでより安全なコードを書くには?
今回は、hash
関数と暗号化/復号化
関数を見ていくよ。主な違いに加えて、言語の擬似ランダム関数もちょっと見てみよう。
イントロ
まず最初に、_stdlib_のCryptoパッケージがほとんどの必要なツールを提供してくれるよ。だから、実装の疑問を解消するためや、細かい点を理解するためには、その公式ドキュメントを調べておこう。
次に、重要なことだけど、この操作は"一方通行"だから、結果からは元の入力値を取り戻せないんだよ。
SHA512でハッシュ化
SHAって頭文字、ウェブや講座で見かけたことあるかもね。Secure Hash Algorithmの略で、ずっと前から使われているよ。2001年にアメリカのNSAで開発され、その年に使用が承認されたんだ。SHAにはいくつかのバージョンがあって、SHA-0から始まり、その後SHA-1, SHA-2, SHA-3が現在のもの。そして512というのは、出力結果、つまり_digest_のビットサイズを意味しているんだ。
基本的な動作はこれだけだよ。どんなサイズの入力(バイトの配列)でも受け取り、常に同じ長さの単語(この場合は512ビット)を返す。入力が本当に大きな値でも大丈夫で、公式データによると (2^128-1)まで扱えるんだ。
さて、ここからコードへと進むよ。
// hashShortVersion はパッケージの関数をシンプルに使用、バイトのスライスを返し、シンプルで効果的。
func hashShortVersion(s string) string {
hasher := sha512.Sum512([]byte(s))
return fmt.Sprintf("%x", hasher)
}
// hasherVersion では hash.Hashインターフェイスを作成し、sha512の実装を割り当てて、インターフェイスのsumメソッドを使用。
// コンポジションやポリモーフィズムに役立つかもしれない。結果は前述のものとは異なる。
func hasherVersion(s string) string {
var hasher hash.Hash
hasher = sha512.New()
b := hasher.Sum([]byte(s))
res := fmt.Sprintf("%x", b)
return res
}
// binaryVersion ではデータをバイナリ形式に変換。
func binaryVersion(s string) string {
var hasher hash.Hash
hasher = sha512.New()
hasher.Write([]byte(s))
m, _ := hasher.(encoding.BinaryMarshaler)
_, _ = m.MarshalBinary()
return string(hasher.Sum(nil))
}
コードとコメントにあるように、異なるメソッド/関数を使用しているため、結果は同じではないよ。MarshallBinary()
は関係データベースやドキュメントデータベース、ファイルなどの「標準的な」ストレージにデータを保存するのにはおすすめできない。なぜならそのフォーマットはかなり異なり、人間には読めないからだよ。また"=="
演算子で比較することもできない。
ハッシュの一般的な用途
ハッシュ関数は任意の入力を「消化」して、常に同じサイズの結果を返すことを目的としているよ。1文字を入力しても、『ドン・キホーテ』全作を入力しても、Linuxカーネルのソースコードを丸ごと入力しても、出力は常に同じ長さなんだ。また、純粋な関数なので、同じ入力には常に同じ出力が得られる。だから一番一般的な用途はソフトウェアの真正性を保証すること。コンパイルされたファイルや実行ファイルのSHAを渡すことで、ダウンロードした人が同じプロセスを実行して、公式の文書にあるものと同じものをダウンロードしたかを確認できるんだ。
一方で、よくある使い方だけどおすすめはしないのがパスワード。なぜなら多くのデータを含むRainbow tablesというものがあり、パスワードのクラッキングに使われるからだよ。
ハッシュについてはここまで。次は暗号化についていくよ。
暗号化と復号化
ハッシュとの主な違いは、元の入力を取り戻すことができること。そして第二に、同じ入力に対して同じ出力が得られないこと(衝突が少ないほど良い)だよ。それはsalt
(英語ではsalt
)と呼ばれる複雑な数学関数によるもので、それは入力値が同じでも結果が変わるように設計された無作為なビットのセットだよ。
コード例を見てみよう。
func Encrypt(s string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(s), bcrypt.MinCost) // bcrypt.DefaultCostも使える
}
func Decrypt(encryptedPassword, plainPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(plainPassword))
}
これだけ見ても分かると思うけど、使い方がとても簡単で明快だよね。
同じ入力で何度か試してみれば、異なる結果が得られるのを見ることができるよ。
暗号化の一般的な用途
もちろん、一番一般的な使用方法(長年にわたって)は、メッセージを隠して、受信者がそれを読み解けるようにすること。ITでは、パスワード、トークン、ユーザーが送信する機密情報に広く使われているよ。
乱数
始める前に、実際には乱数ではなく擬似乱数ということをおさえておこう。その理論的な定義はここで見ることができるよ。
このコードでは、2つの非常に一般的な使用例を見ることができるよ。最初の関数は、与えられたアルファベットでランダムな文字列を取得するもの。入力パラメータは1つで、生成された文字列がどれくらい長くなるべきかを示しているよ。簡単でシンプルだね。
2つ目の関数も似たようなもので、今度は数字だけを生成するよ。入力パラメータは上限値で、その値は含まれない。つまり、1000
を入力すると0
から999
の間で生成されるよ。
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, 0, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret = append(ret, letters[num.Int64()])
}
return string(ret), nil
}
func GenerateRandNum(n int) int64 {
b, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
if err != nil {
return 0
}
return b.Int64()
}
これら2つのスニペットで、かなり多くのケースをカバーできるよ。
まとめ
ハッシュと暗号化の主な違いを学んだり復習したりしたね。それは概念的なものだけでなく、Golangでの実装や業界での一般的な使い方についてだよ。また、crypto.Randパッケージが何であり、どのように使うかについても簡単に紹介したし、少しコードも加えたよ。
スポンサーシップを検討しているなら、ここでできるよ!
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/tomaslingotti/seguridad-en-golang-4f29