Python

[実践]Pythonでハッシュ関数を実装しよう!〜公式ドキュメントを参考に〜

cardene

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーションの開発をしているかるでねです!

今回は「Pythonでハッシュ関数を実装」していこうと思います!

ハッシュ関数」とは、暗号について学んでいるとよく出てくる、「入力したデータを一見適当に見える値にして返す」関数です。

ハッシュ関数」についてはいかにわかりやすくまとめているので、ぜひ参考にしてください!

Pythonのドキュメントを参考に実装を進めていきます。

Pythonに限らず、プログラミング関係のドキュメントは非常に読みづらいです。

参考までに以下のPythonのドキュメントを見てみてください。

https://docs.python.org/ja/3/library/hashlib.html

どうでしょうか?

読むのみ抵抗があるくらい細かいですよね?

なのでこの記事ではドキュメントの重要な部分を抜き出してわかりやすく解説していきます!

ハッシュ関数を実装してみたい

Pythonのドキュメント読んだけど実装までできなかった

このような人のお役に立てれば幸いです。

前置きは早々に早速みていきましょう!

「ハッシュ関数」をPythonで実装するにはPythonの基礎理解が必要です。もし学んでいない方は以下を参考にしてください。

hashlib

Pythonで「ハッシュ関数」を実装するには「hashlib」というモジュールをインポートする必要があります。

Pythonファイルを作成して、一番上に以下のコードを貼り付けてください。

import hashlib

これでインポートできました!

hashlibの基本的な使い方

まずは「hashlib」関数の基本的な使い方からみていきましょう。

基本

word = "かるでね"
word2 = "cardene"

# sha256
sha256 = hashlib.sha256()

# updateするたびにたされていく
sha256.update(word.encode()) # かるでね
sha256.update(word2.encode()) # かるでねcardene

print(f"sha256に渡されたダイジェスト値を確認: {sha256.digest()}")
# sha256に渡されたダイジェスト値を確認: b'*u\xb7\xef\xe1\xcc\xd1\x00t\xbe\x9e\xc0E\xb8@g\xee\xd7\xb0\xa0\x02AG\x16WW\xb0\xf7\x1d\xa1Z\x9a'

1つ1つ確認していきます。

word = "かるでね"
word2 = "cardene"

まずは「ハッシュ関数」に通す変数を2つ用意しています。

# sha256
sha256 = hashlib.sha256()

ここでは使用する「ハッシュアルゴリズム」を指定しています。

今回は「sha256」という「ハッシュアルゴリズム」を使用していきます。

# updateするたびにたされていく
sha256.update(word.encode()) # かるでね
sha256.update(word2.encode()) # かるでねcardene

ここでは文字列を「バイト文字列」に変換して先ほどの「ハッシュアルゴリズム」に渡しています。

変数.encode()

このようにすることで「バイト文字列」に変換できます。

ハッシュ関数」に通すには「バイト文字列」に変換する必要があるので忘れないようにしましょう。

ちなみに以下のような書き方でも「バイト文字列」に変換できます。

b"文字列"

詳しく知りたい方は以下の本を読むか、自分で調べてみてください!

Pythonを学ぶならこちらがおすすめ!(Clickできます)

Amazon

入門 Python 3

print(f"sha256に渡されたダイジェスト値を確認: {sha256.digest()}")

最後に「ハッシュ関数」の出力を確認していきましょう。

digest()」とすると「ダイジェスト値」を出力してくれます。

ダイジェスト値 = ッシュ関数により求められた値。

# sha256に渡されたダイジェスト値を確認: b'*u\xb7\xef\xe1\xcc\xd1\x00t\xbe\x9e\xc0E\xb8@g\xee\xd7\xb0\xa0\x02AG\x16WW\xb0\xf7\x1d\xa1Z\x9a'

訳わからないですね。

以下のようにすると読みやすくなります。

print(sha256.hexdigest())
# 2a75b7efe1ccd10074be9ec045b84067eed7b0a0024147165757b0f71da15a9a

読みやすくなったとはいえさっぱりわからない英数字が並んでいますね。

これが「ハッシュ値」となります。

短く書く

コードが何行にも渡ってしまい少し長くなってしまいましたね。

実はもっと短く書く方法があるのでみていきましょう。

sha256_word = hashlib.sha256(word.encode()).hexdigest()

print(f"ハッシュ関数[sha256]に通した結果(16進形式文字列で表示): {sha256_word}")
# ハッシュ関数(sha256)に通した結果: 3eb903c1a5d61e64e5d9de548763c133db857bfeb4c6fa1ac4f046bf

たった1行で済んでいますね。

こちらの方が使い勝手が良さそうですよね。

hashlibを詳しく見る

hashlib」の使い方の基礎を確認したので、次にもっと詳しくみていきましょう。

先ほどのコードを使っていきます。

word = "かるでね"
word2 = "cardene"

# sha256
sha256 = hashlib.sha256()

# updateするたびにたされていく
sha256.update(word.encode()) # かるでね
sha256.update(word2.encode()) # かるでねcardene

print(f"sha256に渡されたダイジェスト値を確認: {sha256.digest()}")
# sha256に渡されたダイジェスト値を確認: b'*u\xb7\xef\xe1\xcc\xd1\x00t\xbe\x9e\xc0E\xb8@g\xee\xd7\xb0\xa0\x02AG\x16WW\xb0\xf7\x1d\xa1Z\x9a'

では早速みていきましょう!

バイト数を確認

まずは生成された「ハッシュ値」のバイト数を確認しましょう!

ハッシュオブジェクト」の後ろに「digest_size」をつけることで確認できます。

print(f"バイト数: {sha256.digest_size}")
# バイト数: 32

ブロックのバイト数を確認

次に「ハッシュアルゴリズム」のバイト数を確認しましょう。

ハッシュオブジェクト」の後ろに「block_size」をつけることで確認できます。

print(f"ブロックのバイト数: {sha256.block_size}")
# ブロックのバイト数: 64

ハッシュアルゴリズム名の確認

以下は「ハッシュアルゴリズム」の正規名を確認できます。

ハッシュオブジェクト」の後ろに「name」をつけることで確認できます。

print(f"使用しているハッシュアルゴリズムの正規名の確認: {sha256.name}")
# 使用しているハッシュアルゴリズムの確認: sha256

ハッシュオブジェクトをコピー

前章で作成した「ハッシュオブジェクト」のコピーを作成してみましょう。

ハッシュオブジェクト」の後ろに「copy()」をつけることで確認できます。

print(f"ハッシュオブジェクトのコピー: {sha256.copy()}")
# ハッシュオブジェクトのコピー: <sha256 _hashlib.HASH object @ 0x10abac3f0>

デフォルトで用意されていないハッシュアルゴリズムを使用

次に「hashlib」にデフォルトで用意されていない「ハッシュアルゴリズム」を使う方法を確認していきましょう。

hash_new = hashlib.new('ripemd160')
hash_new.update(word.encode())
print(hash_new.hexdigest())

# 081c4625995b968616e6e82bb1a80a25f5e59395

ハッシュオブジェクト」の後ろに「new()」をつけるて、その中で指定すれば使用できます。

ハッシュアルゴリズムの一覧①

すべてのプラットフォームでサポートされていることが保証される「ハッシュアルゴリズム」の一覧を確認していきます。

print(hashlib.algorithms_guaranteed)
# {'sha3_384', 'sha256', 'shake_256', 'sha384', 'sha224', 'sha3_512', 'sha1', 
# 'blake2s', 'sha3_256', 'md5', 'sha3_224', 'blake2b', 'sha512', 'shake_128'}

先程使用した「sha256」がありますね。

こちらは以下のように使用できます。

sha256 = hashlib.sha256()

ハッシュアルゴリズムの一覧②

ここでは「new()」に渡すことができる「ハッシュアルゴリズム」の一覧を確認していきましょう。

2つ前の「new()」に渡すことができる「ハッシュアルゴリズム」を確認できます。

print(hashlib.algorithms_available)
# {'sha384', 'blake2s', 'md5', 'sha512', 'sha512_256', 'sha256', 'shake_256', 
# 'ripemd160', 'sha224', 'sha3_224', 'whirlpool', 'sha3_512', 'sha512_224', 
# 'blake2b', 'sm3', 'mdc2', 'sha3_384', 'md5-sha1', 'sha1', 'sha3_256', 'shake_128', 'md4'}

たくさんありますね。

迷ったらこちらを実行して確認してみましょう。

鍵導出

鍵導出」とは以下のようなものです。

セキュアなパスワードのハッシュ化のために設計されたもの。

前章の「ハッシュアルゴリズム」を使って、より安全に暗号化してくれます。

まずは基本的な部分を確認していきましょう。

pbkdf2_hmac_hash = hashlib.pbkdf2_hmac('sha256', word.encode(), b'cardene', 100000)

先程よりも長いですね...。

引数を1つ1つ確認していきましょう。

第1引数

'sha256'

これは前章でも使用した「sha256」という「ハッシュアルゴリズム」です。

第1引数では使用する「ハッシュアルゴリズム」を指定します。

第2引数

word.encode()

こちらも前章で見たように、「ハッシュ化」したい文字列を「バイト文字列」に変換していますね。

第3引数

b'cardene'

やっていることは第2引数と同じで、「ハッシュ化」したい文字列を「バイト文字列」に変換しています。

これを何のための引数かというと、「salt(ソルト)」というものです。

入力する値の前後に文字列をくっつけてからハッシュ化する。

salt」とは上記のようなもので、今回の場合の入力値である「かるでね」という文字列の前後に「cardene」という文字列をくっつけて、より「ハッシュ化」する値を複雑にします。

これにより元の値が分かりにくくなるため安全性が高まります。

第4引数

最後に第4引数を確認していきます。

10000

何が「10000」なのかというと、「ハッシュアルゴリズム」の計算回数のことです。

10000回「ハッシュアルゴリズム」を回すという指定をすることで、より安全性が高まります。

pbkdf2_hmacでハッシュ化

引数を確認できたので、早速「ハッシュ化」していきましょう。

print(f"pbkdf2_hmacによるハッシュ化: {pbkdf2_hmac_hash.hex()}")
# pbkdf2_hmacによるハッシュ化: 56bf066c700c4ba982fce9b1bd08f2cb781467547b8b1e7b5d5897d20445c4ec

こちらも簡単にできましたね。

よりセキュアに「ハッシュ化」したい場合はこちらを使うということを頭に入れておきましょう!

scryptでハッシュ化

先ほどの「pbkdf2_hmac」以外にもセキュアに「ハッシュ化」してくれるものがあります。

scrypt」というものです。

詳細は省きますが、こちらも複数の引数を指定して「ハッシュ化」してくれます。

詳しく知りたい方はPythonの公式ドキュメントを参考にしたり、自分で調べてみてください!

pbkdf2_scrypt = hashlib.scrypt(word.encode(), salt=b'cardene', n=2, r=1, p=1)
print(f"pbkdf2_scryptによるハッシュ化: {pbkdf2_scrypt.hex()}")
# pbkdf2_scryptによるハッシュ化: 5e95356ca96ab48b632fd4588ca35aa776e655506c4872fce7d673a9ed151c4230c6ec33b5a12ffbaebee2449e1d6d1f43bfc8d5e6e7aca959309f9e79efb29a

BLAKE2

BLAKE2」とはハッシュ関数の1つです。

主に「BLAKE2b」と「BLAKE2s」の2つの種類があり、それぞれ以下のような特徴があります。

ポイント

  • BLAKE2b
    • 1〜64バイトの任意のサイズのダイジェストを生成。
  • BLAKE2s
    • バイトから32バイトの間の任意のサイズのダイジェストを生成。

この「BLAKE2」は引数がめちゃくちゃ多いので一部だけ紹介していこいと思います。

引数

  • data
    • ハッシュ化するデータ。
  • digest_size
    • 出力するダイジェストのバイト数。
  • key
    • 鍵付きハッシュの鍵(BLAKE2bでは最大64バイト、BLAKE2sでは最大32バイト)。
  • salt
    • ランダムハッシュのためのソルト(BLAKE2bは16バイトまで、BLAKE2sは8バイトまで)。
    • 前章で解説しています。
  • person
    • パーソナライズ文字列(BLAKE2bでは最大16バイト、BLAKE2sでは最大8バイト)。

これで一部なのでほんと多いですよね...。

気になる方や試してみたい方はぜひPythonのドキュメントを見たり、調べたりしてみてください。

基本的な使い方

まずは基本的な使い方を見ていきましょう。

from hashlib import blake2b

blake2b_hash = blake2b()
blake2b_hash.update(word.encode())

print(f"blake2b_hashに渡されたダイジェスト値を確認: {blake2b_hash.hexdigest()}")
# blake2b_hashに渡されたダイジェスト値を確認: a0cdd440decbce669998423c8736741e185119eef29fce3d87a5848a3a9c1c8fc7ce870e715bdc3df782417bbd05bba0333caac66b8dabf55b9cdf32a4c14011

# 短く書くと...
blake2b_hash_word = blake2b(word.encode()).hexdigest()

print(f"blake2b_hashに渡されたダイジェスト値を確認: {blake2b_hash_word}")
# blake2b_hashに渡されたダイジェスト値を確認: a0cdd440decbce669998423c8736741e185119eef29fce3d87a5848a3a9c1c8fc7ce870e715bdc3df782417bbd05bba0333caac66b8dabf55b9cdf32a4c14011

全体を確認したので1つ1つ解説していきます。

from hashlib import blake2b

BLAKE2b」をインポートしています。

blake2b_hash = blake2b()
blake2b_hash.update(word.encode())

blake2b_hash」という「ハッシュオブジェクト」を定義して、この記事の最初の方で定義した「word」を「バイト文字列」にして渡しています。

print(f"blake2b_hashに渡されたダイジェスト値を確認: {blake2b_hash.hexdigest()}")
# blake2b_hashに渡されたダイジェスト値を確認: a0cdd440decbce669998423c8736741e185119eef29fce3d87a5848a3a9c1c8fc7ce870e715bdc3df782417bbd05bba0333caac66b8dabf55b9cdf32a4c14011

sha256」などと同じように「ハッシュ値」が出力されていますね。

最後にここまでのコードを短く書く手順を見てみましょう。

# 短く書くと...
blake2b_hash_word = blake2b(word.encode()).hexdigest()

print(f"blake2b_hashに渡されたダイジェスト値を確認: {blake2b_hash_word}")
# blake2b_hashに渡されたダイジェスト値を確認: a0cdd440decbce669998423c8736741e185119eef29fce3d87a5848a3a9c1c8fc7ce870e715bdc3df782417bbd05bba0333caac66b8dabf55b9cdf32a4c14011

こちらの方が簡潔で分かりやすいですね!

実装する場合はこちらを使う方が多いのではないかと思います。

複数回updateを行う

update」とは以下の部分を指します。

blake2b_hash.update(word.encode())

実はこの「update」は複数回行うことができます。

複数回行うとどんどん文字列が結合されていきます。

コードで確認してみましょう。

# 複数回updateする。
from hashlib import blake2s

items = [word.encode(), word2.encode(), word.encode()]
print(f"items: {items}")
# items: [b'\xe3\x81\x8b\xe3\x82\x8b\xe3\x81\xa7\xe3\x81\xad', b'cardene', b'\xe3\x81\x8b\xe3\x82\x8b\xe3\x81\xa7\xe3\x81\xad']

blake2s_hash = blake2s()

for index, item in enumerate(items):
    check_blake2s_hash[index].update(item)
    print(f"blake2s_hashに渡されたダイジェスト値を確認: {check_blake2s_hash[index].hexdigest()}")

    # blake2s_hashに渡されたダイジェスト値を確認: 1fc7b0196993e849b37f27302641625005c0601ad2c512cb56ebfe8d209d003e
    # blake2s_hashに渡されたダイジェスト値を確認: 024a59ee743577946904002ac936cebc67657006aa1c79b9ee4d4c6fd55fb59c
    # blake2s_hashに渡されたダイジェスト値を確認: b2db59219a86b52c14d43cfbfe7bbdde788aa61ee844dda5e46b7b8d54c08d2d

1つ1つ確認していきます。

from hashlib import blake2s

まずは「BLAKE2s」をインポートしています。

items = [word.encode(), word2.encode(), word.encode()]
print(f"items: {items}")
# items: [b'\xe3\x81\x8b\xe3\x82\x8b\xe3\x81\xa7\xe3\x81\xad', b'cardene', b'\xe3\x81\x8b\xe3\x82\x8b\xe3\x81\xa7\xe3\x81\xad']

ここでは「update」を複数回行うために文字列を「items」というリストに格納しています。

バイト文字列」にして格納しているのが出力から確認できます。

for item in items:
    blake2s_hash.update(item)

ここで複数回の「update」を行なっています。

item」に入ってきた文字列が全てたしあわされるので、出力は「かるでねcardeneかるでね」となります。

for index, item in enumerate(items):
    check_blake2s_hash[index].update(item)
    print(f"blake2s_hashに渡されたダイジェスト値を確認: {check_blake2s_hash[index].hexdigest()}")

    # blake2s_hashに渡されたダイジェスト値を確認: 1fc7b0196993e849b37f27302641625005c0601ad2c512cb56ebfe8d209d003e
    # blake2s_hashに渡されたダイジェスト値を確認: 024a59ee743577946904002ac936cebc67657006aa1c79b9ee4d4c6fd55fb59c
    # blake2s_hashに渡されたダイジェスト値を確認: b2db59219a86b52c14d43cfbfe7bbdde788aa61ee844dda5e46b7b8d54c08d2d

しっかり出力されていますね。

items」リストの1つ目の要素と3つ目の要素は「かるでね」という同じ文字列です。

ハッシュ関数」の特徴として、「全く同じ入力の出力は同じになる」というものがあります。

詳しくはこちら。

もし「update」で要素が足し合わされず入れ替わっている場合、1回目の出力と3回目の出力が一致するはずです。

一応確認してみましょう。

for item in items:
    blake2s_hash.update(item)
    print(f"blake2s_hashに渡されたダイジェスト値を確認: {blake2s_hash.hexdigest()}")
    # blake2s_hashに渡されたダイジェスト値を確認: 1fc7b0196993e849b37f27302641625005c0601ad2c512cb56ebfe8d209d003e
    # blake2s_hashに渡されたダイジェスト値を確認: 024a59ee743577946904002ac936cebc67657006aa1c79b9ee4d4c6fd55fb59c
    # blake2s_hashに渡されたダイジェスト値を確認: b2db59219a86b52c14d43cfbfe7bbdde788aa61ee844dda5e46b7b8d54c08d2d

check_blake2s_hash1 = blake2s()
check_blake2s_hash2 = blake2s()
check_blake2s_hash3 = blake2s()
check_blake2s_hash = [check_blake2s_hash1, check_blake2s_hash2, check_blake2s_hash3]

for index, item in enumerate(items):
    check_blake2s_hash[index].update(item)
    print(f"blake2s_hashに渡されたダイジェスト値を確認: {check_blake2s_hash[index].hexdigest()}")
    # check_blake2s_hash1に渡されたダイジェスト値を確認: 1fc7b0196993e849b37f27302641625005c0601ad2c512cb56ebfe8d209d003e
    # check_blake2s_hash2に渡されたダイジェスト値を確認: 42de51d9e114b2c879e423a07567cb9c57854e188499f5a9d6a19ab4d7bf6e1e
    # check_blake2s_hash3に渡されたダイジェスト値を確認: 1fc7b0196993e849b37f27302641625005c0601ad2c512cb56ebfe8d209d003e

ちょっと長いですが出力部分だけ見てください。

2つ目のfor文の方の1回目と3回目の出力の「ハッシュ値」が一致しています。

しかし1つ目のfor文の方では一致していません。

これにより足し合わされているのが確認できますね。

定数の確認

ここでは「ハッシュオブジェクト」の詳細を知るために用意されている定数を確認していこうと思います。

説明よりも見た方が早いと思うので早速コードで確認していきます!

print(f"blake2bのSALT_SIZE: {blake2b_hash.SALT_SIZE}")
# SALT_SIZE: 16
print(f"blake2sのSALT_SIZE: {blake2s_hash.SALT_SIZE}")
# blake2sのSALT_SIZE: 8

print(f"blake2bのPERSON_SIZE: {blake2b_hash.PERSON_SIZE}")
# blake2bのPERSON_SIZE: 16
print(f"blake2sのPERSON_SIZE: {blake2s_hash.PERSON_SIZE}")
# blake2sのPERSON_SIZE: 8

print(f"blake2bのMAX_KEY_SIZE: {blake2b_hash.MAX_KEY_SIZE}")
# blake2bのMAX_KEY_SIZE: 64
print(f"blake2sのMAX_KEY_SIZE: {blake2s_hash.MAX_KEY_SIZE}")
# blake2sのMAX_KEY_SIZE: 32

print(f"blake2bのMAX_DIGEST_SIZE: {blake2b_hash.MAX_DIGEST_SIZE}")
# blake2bのMAX_DIGEST_SIZE: 64
print(f"blake2sのMAX_DIGEST_SIZE: {blake2s_hash.MAX_DIGEST_SIZE}")
# blake2sのMAX_DIGEST_SIZE: 32

ここは特に説明いらないですね。

実際に使うときに詳しく見るくらいで十分だと思います。

出力のダイジェストサイズを調整

最後に「BLAKE2」の引数を紹介したところにある、「digest_size」を使用して、出力の「ダイジェストサイズ」を調整してみましょう。

print(f"ダイジェストサイズを5に指定: blake2b(digest_size=5).hexdigest()")
# 7d64c5272e

print(blake2b(digest_size=10).hexdigest())
# 6fa1d8fcfd719046d762

print(blake2b(digest_size=15).hexdigest())
# b7db87196c483405e40f8401fa1fc9

ちゃんと「ダイジェストサイズ」が変わっていますね。

このように引数を指定することで自分好みに出力を調整できるので、実際に使う際には自分で調べてみてください!

最後に

今回は「Pythonでハッシュ関数を実装」というテーマでした。

いかがだったでしょうか?

ハッシュ関数」や「hashlib」などについての知識がついたと思います。

普段はPythonやブロックチェーンメインに情報発信をしています。

Twiiterでは図解でわかりやすく解説する投稿をしているのでぜひフォローしてくれると嬉しいです!

-Python
-,