【Go実践】パスワードをハッシュ化する

シェアする

前回の記事で、フォームへの入力値をデータベースに登録する方法を実装しました。
【Go実践】フォームで入力された情報をデータベースに登録する

そこでは、フォーム値の受け渡しとテーブルへの登録方法のを主眼としていたため、パスワードをそのままテーブルへ登録しましたが、この実装ではセキュリティに問題があります。

パスワード等の機密情報を通信上でやり取りする場合、なんらかのセキュリティ的な措置が必要です。
今回は、パスワードをハッシュ化する方法を解説するとともに、汎用的に利用できるハッシュ化関数を実装してみたいと思います。

ハッシュ化について簡単に解説

本題に入る前に、ハッシュ化について簡単に解説しておきます。

ハッシュ化とは、元のデータを一定のアルゴリズムに従って変換することです。
ハッシュ化された値をハッシュ値と呼びます。
ハッシュ値には以下のような特徴があります。

  • 固定長である(長さはアルゴリズムによって異なる)
  • 規則性がなく、一見してランダム生成された文字列に見える
  • ハッシュ値から元の値に復元することはできない

特に、元の値への復元が不可能であるという点が暗号化と異なります。

暗号を元の値に復元することを復号化といいますが、復号化が不可能である方が当然ながらセキュリティ強度は強いといえます。
ログイン認証に使用するパスワードなら、ハッシュ値同士を比較すればよいので、元の値に復元する必要がありませんね。
このため、会員制サイトなどのパスワードは、基本的にハッシュ化することが一般的です。

ハッシュアルゴリズムには、md5shaなどがあります。
当然ながらアルゴリズムによって求められるハッシュ値は異なるため、アプリケーション内で採用するアルゴリズムは統一しておくことが望ましいでしょう。

ハッシュ化処理の実装

それではハッシュ化の処理を実装していきたいと思います。

ハッシュ化は様々なアプリケーションで汎用的に利用できる機能ですので、使い回しがしやすいように実装しておくと便利です。
utilityという汎用パッケージを作成し、そのパッケージ内にハッシュ化関数を実装することにしましょう。

新たにutilityというディレクトリを作成し、ディレクトリ内にcipher.goファイルを作成してください。
このファイルにハッシュ化関数を記述します。

cryptoパッケージ

Goでハッシュ化を行うには、cryptoパッケージを使用します。
cryptoは標準パッケージに含まれており、よく使われる暗号化の定数を含んでいます。

cryptoパッケージのサブディレクトリにmd5やsha1、sha256などのハッシュアルゴリズムが含有されており、該当するアルゴリズムでのハッシュ値を求める関数がシンプルに実装されています。

ハッシュ化関数のサンプル

それでは、ハッシュ化関数のサンプルを見てみましょう。
今回は、md5、sha1、sha256、sha512という4種のアルゴリズムに対応する関数を実装することにします。
これ以外のアルゴリズムが必要な場合はcryptoパッケージのドキュメントを参照してください。

package utility // 独自のユーティリティパッケージ

import (
    "crypto/md5"
    "crypto/sha1"
    "crypto/sha256"
    "crypto/sha512"
    "encoding/hex"
)

/* ****************************************
 ハッシュ化関数
 # md5,sha1,sha256,sha512に対応
 # デフォルトはsha256
**************************************** */
func HashStr(trg, alg string) string {

    // ハッシュ化文字列の初期値
    hashed := ""

    // ハッシュ対象文字列をバイト型スライスに変換
    b := []byte(trg)

    // バイト型スライスを指定されたアルゴリズムでハッシュ化し文字列へエンコード
    switch alg {
    case "md5":
        // md5
        md5 := md5.Sum(b)
        hashed = hex.EncodeToString(md5[:])
    case "sha1":
        // sha1
        sha1 := sha1.Sum(b)
        hashed = hex.EncodeToString(sha1[:])
    case "sha512":
        //sha512
        sha512 := sha512.Sum512(b)
        hashed = hex.EncodeToString(sha512[:])
    default:
        // 上記以外が指定された場合または未指定の場合はsha256
        sha256 := sha256.Sum256(b)
        hashed = hex.EncodeToString(sha256[:])
    }

    // ハッシュ化した文字列をリターンする
    return hashed

}

この関数は、第1引数でハッシュ化の対象文字列を、第2引数で使用するハッシュアルゴリズムを受け取り、戻り値としてハッシュ化した文字列を返します。

まずは22行目に着目してください。

    // ハッシュ対象文字列をバイト型スライスに変換
    b := []byte(trg)

引数で受け取った対象文字列をバイト型のスライス(≒可変長配列)に変換しています。
ハッシュ値を求める関数はバイト型スライスを引数にとるため、この変換が必要になります。

次にswitch節で、指定されたハッシュアルゴリズムごとに処理分岐をしています。
分岐先の処理内容は、アルゴリズムの違い以外は同一で、各ハッシュアルゴリズムによるチェックサムをバイト型配列で取得し、文字列へ変換しています。

        md5 := md5.Sum(b)
        hashed = hex.EncodeToString(md5[:])

チェックサムの取得には、各アルゴリズムディレクトリのSum関数またはそれに準ずる関数を使用しています。
これらの関数は各アルゴリズムによるチェックサムを固定長配列で返します。

Goは型に厳格な言語であり、固定長配列型は長さまで同じでないと同一の型とみなされません。
そのため、チェックサムの代入先にそれぞれ異なる変数を使用する必要があります。

動作確認

それでは、データベース登録処理にハッシュ化処理を追加し、ハッシュ化が正常に行われるか確認してみましょう。

前回作成したreq_handler/h_UserRegistered.goファイルを以下のように修正してください。

package req_handler // 独自のHTTPリクエストハンドラパッケージ

import (
    "database/sql"
    "fmt"
    "html/template"
    "net/http"

    "../conf"    // 実装した設定パッケージの読み込み
    "../query"   // 実装したクエリパッケージの読み込み
    "../utility" // 実装したユーティリティパッケージの読み込み
    _ "github.com/go-sql-driver/mysql"
)

// 登録結果の確認画面
func HandlerUserRegistered(w http.ResponseWriter, req *http.Request) {

    // POSTデータINSERT関数を実行
    result := insertPostedUser(req)

    // テンプレートをパースする
    tpl := template.Must(template.ParseFiles("templates/user-registered.gtpl"))

    // テンプレートに出力する値をマップにセット
    values := map[string]string{
        "result": result,
    }

    // マップを展開してテンプレートを出力する
    if err := tpl.ExecuteTemplate(w, "user-registered.gtpl", values); err != nil {
        fmt.Println(err)
    }
}

// POSTデータINSERT関数
func insertPostedUser(req *http.Request) string {

    // 正常終了時のreturn値
    result := "ユーザ情報の登録に成功しました。"

    // 設定ファイルを読み込む
    confDB, err := conf.ReadConfDB()
    if err != nil {
        result = "設定ファイルの読み込みに失敗しました。"
    }

    // 設定値から接続文字列を生成
    conStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s", confDB.User, confDB.Pass, confDB.Host, confDB.Port, confDB.DbName, confDB.Charset)

    // データベース接続
    db, err := sql.Open("mysql", conStr)
    if err != nil {
        result = "データベースへの接続に失敗しました。"
    }
    // deferで処理終了前に必ず接続をクローズする
    defer db.Close()

    // パスワードをハッシュ化(sha256)
    hashedPwd := utility.HashStr(req.FormValue("passwd"), "sha256")

    // POST値を渡してINSERT処理を実行
    _, err = query.InsertUser(req.FormValue("account"), req.FormValue("name"), hashedPwd, db)
    if err != nil {
        result = "ユーザ情報の登録に失敗しました。"
    }

    // 結果をreturnする
    return result
}

まず11行目に、今回作成したutilityパッケージのインポートを追記しています。

次に59行目で、同じく今回実装したハッシュ化関数を追記し、POSTされたパスワードをハッシュ化しています。
ハッシュアルゴリズムにはsha256を指定しています。

最後に62行目で、ユーザマスタへのINSERT処理の引数にハッシュ化文字列を渡すように修正しています。

それではmain.goを実行し、ブラウザでhttp://localhost:8080/user-formにアクセスしましょう。
フォームに入力を行い、[確認画面へ]をクリックします。

フォームへ入力した値が確認画面に出力されたら、登録ボタンをクリックします。

以下のように登録処理が正常終了した旨が表示されます。

最後にデータベースのユーザマスタテーブルも確認してみましょう。
前回までとは異なり、PASSWORDカラムに平文ではなくハッシュ値が登録されていることが確認できます。

終わりに

今回はハッシュ化の実装方法を紹介しました。
実装したハッシュ化関数は充分実用に耐えると思いますが、さらにハッシュ値の突合を避けるならば、元の値にランダム文字列(ソルト)を付加してからハッシュ化するなどの措置を行ってください。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク