技術向上

プログラミングの学び、気になるテクノロジーやビジネストレンドを発信

bcryptでパスワードをhash化【Go】

サードパーティのbcryptを使用します。
bcrypt - GoDoc

シンプルな記述で暗号化でき、暗号の強度も数字や定数を指定するだけです。


暗号化

暗号化の対象(byte配列)とコスト(暗号の強度)を引数にとり、byte配列とエラーを返します。

bs, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)


コストには数字とあらかじめ用意された定数を指定することができます。
定数は次の通りです。

const (
    MinCost     int = 4
    MaxCost     int = 31
    DefaultCost int = 10
)

GenerateFromPassword()の第2引数に、MinCostよりも小さい数字(3以下)を指定すると、DefaultCostが使用されます。
MaxCostを指定したハッシュ化を試しましたが、処理にかなりの時間を要しました。
コストの指定にあたっては、セキュリティとパフォーマンスを天秤にかけ、場合に応じて選択すべきでしょう。

また、ハッシュ化の対象には注意すべき点があり、72byteよりも大きいのサイズのデータを渡してはいけません。 パスワード入力に文字数制限をかけて対応すべきでしょう。


sign-upの例(Session【Go】 - 技術向上)では平文のパスワードでしたが、
このbcryptを用いた暗号化を施すには、次のようにします。(修正箇所を中心に記載)

type user struct {
    UserName, First, Last string
    Password              []byte    // dbに格納するパスワードはハッシュ化するため、byte配列
}

...

func signup(w http.ResponseWriter, req *http.Request) {
    ...
    if req.Method == http.MethodPost {
        ...
        bs, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost)    // formのパスワードのinput要素(p)をbyte配列にキャストし、暗号化の対象とする
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        u = user{
            UserName: un,
            Password: bs,    // formのinput要素の内容をハッシュ化したbsを指定
            First:    f,
            Last:     l,
        }
        ...
    }
}


解読

ハッシュ化された対象を解読して、あるbyte配列と照合する関数が用意されています。

err := bcrypt.CompareHashAndPassword(u.Password, []byte(p))

第1引数にハッシュ化済みの対象を、第2引数に照合したい対象を指定します。
エラーのみを返し、エラーがnilでない場合には照合に失敗したことを意味します。


上記関数をログインに使用すると次のようになります。
暗号化と同じく、sign-upの例(Session【Go】 - 技術向上)からの変更点を中心に記載します。
ただし、signupの後は自動でログインされてしまい、かつログアウト機能もまだないため、このままではログイン単体での処理を試すことができません。そのため、あらかじめログイン試行用のユーザーをinit()で追加しています。
また、usernameとpasswordのform input要素を持つ、login.gohtmlを用意します。

func init() {
    tpl = template.Must(template.ParseGlob("templates/*"))
    bs, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.MinCost)    // sign-up処理の後は自動ログインされ、ログイン単体での処理を試すことができないため、あらかじめ用意
    dbUsers["test@test.com"] = user{UserName: "go@go.com", Password: bs, First: "Go", Last: "Lang"}    // sign-up処理の後は自動ログインされ、ログイン単体での処理を試すことができないため、あらかじめ用意
}

func main() {
    ...
    http.HandleFunc("/login", login)
    ...
    http.ListenAndServe(":8080", nil)
}

func login(w http.ResponseWriter, req *http.Request) {
    if alreadyLoggedIn(req) {
        http.Redirect(w, req, "/", http.StatusSeeOther)
        return
    }

    if req.Method == http.MethodPost {
        un := req.FormValue("username")
        p := req.FormValue("password")

        u, ok := dbUsers[un]
        if !ok {    // usernameが登録されていない場合
            http.Error(w, "Username and/or Password do not match.", http.StatusForbidden)
            return
        }

        err := bcrypt.CompareHashAndPassword(u.Password, []byte(p))
        if err != nil {    // パスワードの照合に失敗した場合
            http.Error(w, "Username or/and Password do not match.", http.StatusForbidden)
            return
        }

        // sessionを生成
        sID, _ := uuid.NewV4()
        c := &http.Cookie{
            Name:  "session",
            Value: sID.String(),
        }
        http.SetCookie(w, c)
        dbSessions[c.Value] = un
        http.Redirect(w, req, "/", http.StatusSeeOther)
        return
    }
    tpl.ExecuteTemplate(w, "login.gohtml", nil)
}


qiita.com