SHA256 でパスワードを二重に暗号化 (まだ疑問点あり)

SHA256 でパスワードを二重に暗号化 (まだ疑問点あり):


書いてあること

Webアプリケーションのパスワードを、SHA256で暗号化してハッシュにするためのコードと、そのへんの防御にまつわる疑問点。

「二重」というのは、SHA256暗号化したものに、更にsaltと呼ばれるidに固有の文字列を連結してもう一度暗号化しているためです。

日本語訳して「お塩を振る」と考えてみるとちょっと可愛い。


基本的なしくみ

DBにはユーザのidのほかに、

  • salt値
  • 二重にハッシュ化したパスワード
を持っておき、認証の際には

  1. 平文パスワードをSHA256でハッシュ化
  2. 1に「salt値」を文字列連結
  3. 2を更にSHA256でハッシュ化
  4. 3とDBの「二重にハッシュ化したパスワード」が一致していればログイン
という手順を踏みます。

(どこまでクライアントでどこまでサーバなのかは私はよくわかっていません。以下の「疑問点」差参照)


タッチした経緯

以前Redmine1.1⇒3.2という劇的なバージョンアップを行った際、パスワードの保存方法が変わっていて一手間必要だったもので、その時書いたコードの流用です。

1.1ではパスワードを一回ハッシュ化しただけの文字列がDBに保存されていたのですが、3.2では上記のようにsaltという値を新しく持って二重にハッシュ化するという仕様になっていました。

単純にバージョンアップするとテーブルの中のsaltカラムは空なわけで、ログインできないため、saltを任意に設定し、二重暗号化した後のハッシュ値で値を更新しました。


コード


JavaScriptでハッシュ化

https://github.com/Caligatio/jsSHAを使わせてもらいました。


sha256.jsを読み込んでおいて、

// 入力されたパスワード 
var pass = "passworddayo"; 
 
// ハッシュ化 
var shaObj = new jsSHA("SHA-256", "TEXT"); 
shaObj.update(pass); 
var passhash = shaObj.getHash("HEX"); 
※GitHubにあるサンプルそのまんまです。


Javaでハッシュ化・二重ハッシュ化

java.security.*とjava.util.UUIDをimportしといて、

// 文字列にSHA256をかけてハッシュ化するメソッド 
public static String sha256(String orgStr) { 
    String hashed = ""; 
 
    try { 
        MessageDigest md = MessageDigest.getInstance("SHA-256"); 
        byte[] hash = md.digest(orgStr.getBytes()); 
        BigInteger bi = new BigInteger(1, hash); 
        hashedStr = String.format("%0" + (hash.length << 1) + "x", bi); 
 
    } catch (NoSuchAlgorithmException e) { 
        e.printStackTrace(); 
    } 
 
    return hashedStr; 
} 
 
// 上記メソッドの呼び出し方(二重) 
public static void main(String[] args) { 
 
    // ※本当は平文のパスワードがサーバに送られてくることはありません 
    String pass = "passworddayo"; 
 
    // 一回ハッシュ化。JavaScriptでHash化したものと同じ文字列になる 
    // なので本当はこの文字列がサーバに送られてくる 
    String tmpHash = CryptUtil.sha256(pass); 
    System.out.println("tmpHash : " + tmpHash); 
 
    // 桁数がRedmineの仕様にぴったりなので、今回saltにはUUIDを使用 
    // 本当ならsaltはDBに登録してある値を利用する 
    String salt = UUID.randomUUID().toString().replaceAll("-", ""); 
    System.out.println("salt    : " + salt); 
 
    // ハッシュとsaltを文字列連結して再度ハッシュ化 
    // これがDBに登録するhash値になる 
    String hash = CryptUtil.sha256(salt + tmpHash); 
    System.out.println("hash    : " + hash); 
} 
Javaの実行結果はこんな感じになります。

tmpHash : 198be4d7f6b01b0e9166cc1fe21ff81aefb7117aa51d3862671aba999fb7320c 
    salt    : 990e3886c87149cf98dcdad1a1cfc85b 
    hash    : 646bcd9ce0db63bf1aea73673b3c9863b1033cb1fbc8d78150fad1437db7941c 


疑問点:クライアントからサーバに送られるのはどういうデータなのか。

(時間があればRedmineのコード読みます)

(詳しい人いらしたらコメントください)

(最新のRedmineではまた仕様変わってるかも)

SHA256ハッシュは不可逆暗号ということになっていますが、よくあるパスワードのハッシュは当然リスト化されています。

saltを加える=(隠し味的に)お塩を加えることでリスト攻撃から守ろうという発想なのでしょう、が…

クライアント-サーバ間の通信は一体以下のどれが適切なのか???


idと、暗号化していないパスワードをサーバに送る?

ダメでしょう。(パケット覗かれたら終わる)


一回ハッシュ化されたパスワードとidが送られてくる?

  1. クライアントからサーバに、一回ハッシュ化したパスワードとidを送る
  2. サーバ側で送られてきたidを元にsalt値を引いてきて、二重ハッシュ化
  3. サーバ側でDBに保存されている二重ハッシュと2を比較して認証
この場合、リスト攻撃としてはハッシュ化パスワードをサーバに送りつけるだけでログイン成否の判定はできてしまう。

あんまり意味なくない?


idだけ送り、salt値をサーバからもらって、二重ハッシュを再度送る?

  1. クライアントからサーバにidを送る
  2. サーバ側で送られてきたidを元にsalt値を引いてきて、クライアントに返す
  3. クライアント側で二重ハッシュを作ってサーバに送る
  4. サーバ側でDBに保存されている二重ハッシュと3を比較して認証
これでも迂遠になっただけで、やっぱり適当なid(流出したメールアドレスとか)を送りつければあとは戻って来たsaltとパスワードリストを掛け合わせてサーバに送りつければ判定できる。

攻撃側の計算量は増えてしまうので、効率を落とせるというのは利点か。


そもそもDBの中身が流出したら?

saltの値も流出するので、やっぱりリスト照合はできてしまう。

攻撃側の計算量は増える。それだけ。


Redmineのコードを読めば上記のいずれなのかは分かる、けど…

攻撃側の計算量が増えることが無駄だとは言いませんが、saltを加えるという一手間も、「よくあるパスワードはリスト攻撃で破られる」という問題の解決にはなっていないような。

パスワード認証という形式である以上リスト攻撃を完全にブロックするのは無理なんでしょうか。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)