LoginSignup
9
2

More than 5 years have passed since last update.

bcrypt でパスワードをハッシュ化する場合はパスワードの bytes 数に気をつける

Posted at

bcrypt

bcrypt はよく使われているパスワードのハッシュ関数です。

個人的に印象的だったのは、Slack のユーザー DB がアクセス可能になってしまった障害のときにパスワードが一方向ハッシュ化されていることが excuse に使われていました。そのハッシュ関数が bcrypt です。

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 目以降が違うパスワードが同じと判断されてしまうためです。

以下のコード例で境界を確認しています。

コード

ハッシュを生成する 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)
        }
    }
}
9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2