まったり技術ブログ

Technology is power.

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

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

Go言語を使ったWeb開発で認証機能を実装したくて調べてみたら、「sessionauth」というパッケージが見つかった。

github.com

 認証の有無のセッションを管理してくれるのは便利なのだが、パスワード格納(DBへの保存)の機能は提供されておらず、自分でそこは実装する必要がありそう。

現にサンプルコードではパスワードは平文
$ sqlite3 martini-sessionauth.bin
SQLite version 3.16.0 2016-11-04 19:09:39
Enter ".help" for usage hints.
sqlite> .table
users
sqlite> select * from users;
1|testuser|password

そんな訳で、パスワードの格納の定番と言えば、『ハッシュ化』ということで、Go言語でのパスワードハッシュに関して調べてみた。
下記のサイトが参考になった。

hachibeechan.hateblo.jp

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

  • scrypt
  • bcrypt

今回は『bcrypt』を使ってハッシュ化する方法を紹介します。

bcrypt - GoDoc

パスワードのハッシュ化

 GenerateFromPassword関数を利用することで、パスワード文字列をByte型のハッシュ値に変換することができます。

// パスワードのハッシュ化
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

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

 「ハッシュ値」と「パスワード文字列」の比較はCompareHashAndPassword関数を利用することで可能です。

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

ハッシュ値の生成・比較を1つのコードにまとめる

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

package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

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

    // ハッシュ値の生成
    hash, err := bcrypt.GenerateFromPassword([]byte(storePass), bcrypt.DefaultCost)
    if err != nil {
        return
    }

    hashStr := string(hash)

    // ハッシュ値とパスワード文字列を比較
    err = bcrypt.CompareHashAndPassword([]byte(hashStr), []byte(inputPass))
    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(loginId, 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: loginId, Password: string(hash)})
}

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

func main() {
    users = Users{}

    // 登録
    register("user1", "password1")
    register("user2", "password2")
    register("user3", "password3")
    register("user4", "password4")
    register("user5", "password5")

    // 認証
    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.099179s    Success user1/password1
0.094606s   Success user2/password2
0.092299s   Success user3/password3
0.092068s   Success user4/password4
0.094260s   Success user5/password5
0.000001s   Failure user6/password1  // ← 他に比べて認証時間短い!?
0.095547s   Failure user1/
0.092764s   Failure user3/password1
0.090236s   Failure user3/password2
0.089934s   Success user3/password3
0.091101s   Failure user3/password4

 正常に認証処理ができていることが確認できました。

 しかし、認証時間を着目してみると、ログインIDに「user6」を指定した時だけ処理時間が異様に短いことが分かります。
 存在しないユーザIDであり、保存されているハッシュ値が格納されるようになっている hashStr変数 に空文字が入っており「CompareHashAndPassword」の処理が短くなっているからだと思われる。
 細かい点だが、レスポンス時間からユーザの有無が知られるのは認証機能としては、イマイチなので少し修正してみる。

 空文字の初期化ではなく、文字列を格納するような初期化を行なってみる。

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

 出力結果は下記のようになり、ユーザの有無に関係なく、認証処理時間に偏りがなくなったことが確認できました。

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