まったり技術ブログ

Technology is power.

【Go言語】パスワードをハッシュ化(bcrypt)

f:id:motikan2010:20170213221701p:plain
・bcrypt.GenerateFromPassword
・bcrypt.CompareHashAndPassword
を使ってみる話です。

Go言語を使ったWeb開発で認証機能を実装したくて調べてみたらこんなリポジトリが見つかった。

github.com

よく見てみるとパスワード格納はライブラリに含まれておらず、サンプルコードでも無残にパスワードが平文で格納されているではありませんか・・・。

Go言語でのパスワードハッシュに関して調べてみると下記の記事が見つかった。

hachibeechan.hateblo.jp

ハッシュ化のライブラリには主に2つ用意されているらしい。
・scrypt
・bcrypt

本記事では、bcryptライブラリを使ってハッシュ化する方法を紹介します。

bcrypt - GoDoc

パスワードのハッシュ化

hash, err := bcrypt.GenerateFromPassword([]byte("パスワード"), bcrypt.DefaultCost)

// Byteで返されるので文字列に変換して表示
fmt.Println(string(hash))

// 毎回値の異なるハッシュ値が取得できる(ちなみに"password"のハッシュ値)
// $2a$10$iuJaubQvGTawiwa6UFa08uvOGwFaa25Wz29llEKEFHyPT3w262Qw6
// $2a$10$HFZ4bmj98bEePKO3gNsbZO3XsgXORvjFhexZV6HADm46/CuaE6M/m
// $2a$10$BSzyPPKOOs0YwC1h6UoD2eNFAyWYVfS.hmZQuQLLTRyC/Z.z3fzsy

パスワード文字列とハッシュ値を比較

認証部分は下記のように記述します。

err = bcrypt.CompareHashAndPassword([]byte("ハッシュ値"), []byte("パスワード"))
// 一致している場合はerrにnilが返されます。一致していない場合はエラーが返されます。

コードにまとめるとこのような感じ

下記のコードでは「Success」が表示されます。

package main

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    storePass := "password"
    loadPass := "password"

    hash, err := bcrypt.GenerateFromPassword([]byte(storePass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    hash_str := string(hash)

    err = bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(loadPass))
    if err != nil {
        fmt.Println("Failure")
    } else {
        fmt.Println("Success")
    }

}

認証のデモ

今回はパスワード認証機能が動作確認をしたいだけなので、データベースなどは用意せずにログインIDとパスワードを格納することができるUser構造体を使って動作確認を行う。

package main

import (
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
)

type User struct {
    LoginId  string
    Password string
}

type Users []*User

var users Users

func register(login_id, pass string) {
    /*
      bcrypt.MinCost = 4
      bcrypt.MaxCost = 31
      bcrypt.DefaultCost = 10
   */
    hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    users = append(users, &User{LoginId: login_id, Password: string(hash)})
}

func login(login_id, password string) {
    var hash_str = ""
    start := time.Now()
    for _, user := range users {
        if login_id == user.LoginId {
            hash_str = user.Password
            break
        }
    }
    err := bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(password))
    end := time.Now()
    fmt.Printf("%fs\t", (end.Sub(start)).Seconds())
    if err != nil {
        fmt.Print("Failure")
    } else {
        fmt.Print("Success")
    }
    fmt.Printf("\t%s/%s\n", login_id, password)
}

func main() {
    users = Users{}
  // 登録
    register("user1", "password1")
    register("user2", "password2")
    register("user3", "password3")
    register("user4", "password4")
    register("user5", "password5")
  fmt.Println()

  // 認証
    login("user1", "password1")
    login("user2", "password2")
    login("user3", "password3")
    login("user4", "password4")
    login("user5", "password5")
    login("user6", "password1")
    login("user1", "")
    login("user3", "password1")
    login("user3", "password2")
    login("user3", "password3")
    login("user3", "password4")

}
出力
0.106827s    Success user1 / password1
0.096496s   Success user2 / password2
0.099798s   Success user3 / password3
0.099758s   Success user4 / password4
0.100966s   Success user5 / password5
0.000001s   Failure user6 / password1
0.125239s   Failure user1 /
0.097884s   Failure user3 / password1
0.097816s   Failure user3 / password2
0.097296s   Success user3 / password3
0.096882s   Failure user3 / password4

処理時間を見てみると、ログインIDに「user6」を指定した時だけ処理時間が異様に短いことが分かる。
存在しないユーザであり「CompareHashAndPassword」の処理がとてつもなく短くなるからである。
レスポンス時間からユーザの有無を知られるなんて気分が悪いので、このように"hash_str"を初期化してみる。

var hash_str = "$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

こうすれば常に「CompareHashAndPassword」の処理時間が同等になる。

0.097851s Success user1 / password1
0.095720s  Success user2 / password2
0.097851s  Success user3 / password3
0.097147s  Success user4 / password4
0.104292s  Success user5 / password5
0.099853s  Failure user6 / password1
0.098755s  Failure user1 /
0.094102s  Failure user3 / password1
0.093256s  Failure user3 / password2
0.100131s  Success user3 / password3
0.097537s  Failure user3 / password4

これでレスポンス時間からユーザの有無を知られることはなくなるかと思う。