bcrypt
bcrypt はよく使われているパスワードのハッシュ関数です。
個人的に印象的だったのは、Slack のユーザー DB がアクセス可能になってしまった障害のときにパスワードが一方向ハッシュ化されていることが excuse に使われていました。そのハッシュ関数が bcrypt です。
- Slack’s hashing function is bcrypt with a randomly generated salt per-password which makes it computationally infeasible that your password could be recreated from the hashed form.
bcrypt が生成するハッシュは $2a$10$OTKlbteacFY8DOeZY5imi.wvNLmJ1WDenLlDSzXfFxizVX.D1BNfu
形式の 60 文字です。
Go では golang.org/x/crypto/bcrypt が使えます。
パスワードの bytes 数
少なくとも golang.org/x/crypto/bcrypt では 72 bytes 以内に制限する必要があります。
先頭の 72 bytes が同じで 73 bytes 目以降が違うパスワードが同じと判断されてしまうためです。
- when using bcrypt you should be aware that it limits your maximum password length to 50-72 bytes. The exact length depends on the bcrypt implementation you are using
以下のコード例で境界を確認しています。
コード
ハッシュを生成する Generate 関数と、そのハッシュとハッシュ化前の値が一致しているかを確認する Compare 関数を実装します。
package password
import (
"testing"
"golang.org/x/crypto/bcrypt"
"regexp"
"strings"
)
// Generate は平文のパスワードから一方向ハッシュを生成する
func Generate(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// Compare は一方向ハッシュと平文のパスワードを比較して、一致しているかどうかとエラーを返す
func Compare(hash, password string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
// MEMO: err を wrap して詳細を伝えると良い
return false, err
}
return false, err
}
return true, nil
}
func TestGenerate(t *testing.T) {
tests := []struct {
name string
inputPassword string
}{
{
name: "ハッシュを生成できる",
inputPassword: strings.Repeat("w", 30),
},
}
for _, test := range tests {
hash, err := Generate(test.inputPassword)
if err != nil {
t.Errorf("[%s] got err %v, but want nil", test.name, err)
continue
}
if !regexp.MustCompile(`\$2a\$10\$.{53}`).Match([]byte(hash)) {
t.Errorf("got %s, but want passwordhash should be a bcrypto format", hash)
}
}
}
func TestCompare(t *testing.T) {
tests := []struct {
name string
inputHash string
inputPassword string
wantMatched bool
}{
{
name: "一致することを確認する",
inputHash: "$2a$10$jRtzjPGiLZdUMErjW0M8oeG/JQHbMHBIqIjAFI73X28cD0wqwAT56",
inputPassword: "a5020327-3086-4cbd-ae9d-bd480a2a08c8",
wantMatched: true,
},
{
name: "一致しないことを確認する",
inputHash: "$2a$10$iwzHZAQ8erjNMDzqhlA3i.YY2Tp.ge0wDxZDznU5J3jZaaDXR32YK",
inputPassword: "a5020327-3086-4cbd-ae9d-bd480a2a08c8",
},
}
for _, test := range tests {
matched, err := Compare(test.inputHash, test.inputPassword)
if matched != test.wantMatched {
t.Errorf("[%s] got %v but want %v (error %v)", test.name, matched, test.wantMatched, err)
}
}
}
func TestGenerateAndCompare(t *testing.T) {
tests := []struct {
name string
inputGeneratePassword string
inputComparePassword string
wantMatched bool
}{
{
name: "生成したハッシュと一致しないことを確認する",
inputGeneratePassword: strings.Repeat("w", 50),
inputComparePassword: strings.Repeat("k", 50),
},
{
name: "生成したハッシュと一致することを確認する",
inputGeneratePassword: strings.Repeat("w", 50),
inputComparePassword: strings.Repeat("w", 50),
wantMatched: true,
},
{
name: "73 bytes 目以降が一致しない文字列同士が一致となってしまうことを確認する",
inputGeneratePassword: strings.Repeat("w", 72) + "a",
inputComparePassword: strings.Repeat("w", 72) + "d",
wantMatched: true,
},
{
name: "72 bytes 目が一致しない文字列同士は不一致になることを確認する",
inputGeneratePassword: strings.Repeat("w", 71) + "a",
inputComparePassword: strings.Repeat("w", 71) + "d",
},
}
for _, test := range tests {
hash, err := Generate(test.inputGeneratePassword)
if err != nil {
t.Errorf("[%s] got err %v, but want nil", test.name, err)
continue
}
matched, err := Compare(hash, test.inputComparePassword)
if matched != test.wantMatched {
t.Errorf("[%s] got %v but want %v (error %v)", test.name, matched, test.wantMatched, err)
}
}
}