Blow Up by Black Swan

Python-ハッシュ関数とハッシュライブラリー

今回は、ハッシュ関数について記事を書いてみました。きっかけとしては、ブロックチェーンのコードを勉強するときに、ハッシュ関数が一体なんなのかわからなかったからです。(きっかけとなった記事のリンクは一番下にひっそりと載せさせて頂いてます)この分野も私は全く素人で一から調べた内容ですので、こんな説明の仕方、図があったらわかりやすいんじゃないか、という自分の感覚で書いてます。

1. ハッシュ関数とは

1.1 用語のざっくりとした定義

  • メッセージ・・・インプットされるデータのこと。文字列やテキストだけでなく、画像や動画など幅広く使用できる。
  • ハッシュ値・・・出力されるデータのこと。メッセージダイジェストとも呼びますが、この記事ではハッシュ値と呼びます。

1.2 大雑把な理解

ハッシュ関数シメージ図

ハッシュ関数は、まさに関数のブラックリストです(関数の中身は高度な数学が使われているようです・・・)。様々なデータを入れることで、指定したデータ量のアウトプット(ハッシュ値)をしてくれます。

  • ハッシュ値(アウトプット)からインプットを予測できない(不可逆的とも言い、最大の特徴)
  • インプットはデータであればなんでも良く、データ量はいくらでも構わない
  • インプットデータが少し変わるだけで、ハッシュ値は全く異なる(プログラミング部分で検証)
  • ただし、インプットが同じであればハッシュ値は同じ(ランダムとは異なる)
  • アウトプットはハッシュ関数で指定したデータ量になる(プログラミング部分で検証)

ある意味インプットの場合の数は無限大ですが、アウトプットのハッシュ値はデータ量が決まっているため有限になり、異なるインプットで同じハッシュ値となるということも理論上ありえます。これをハッシュ衝突と言います。このハッシュ衝突は理論上あり得ますが、アウトプットのデータ量が多いハッシュ関数を新たに開発するなどして、起こりにくくなっています。

2. ハッシュ関数の利用シーン

ハッシュ関数を利用する場合、ハッシュ関数の不可逆性の部分(アウトプットからインプットが予測できない)や、インプットが少しでも変わるとアウトプットが全く違ったものになることを利用します。

例)定期的に重要ファイルのハッシュ値を算出し、前回のものと検証する。これが変わっていた場合、データ内容に何かしらの変更があったことがわかる。

2.1 HMACによるセキュリティ対策

HMACでハッシュ関数が利用されるのは以下のような利点があるためです。また、利用の流れは以下のようになります。 

  • ネットワーク内でデータが改ざんされたか確認ができる
  • 高速処理が可能なため、IPSecやSSLではパケットごとにMACが添付されている
macイメージ図2
  • 事前準備:共有鍵をアリスとボブで共有
  • <Aliceの対応>
    • ①メッセージを作成し、メッセージと共有鍵をインプットしたハッシュ値(MAC:Message Authentication Code)を添付
    • ②送信(送信内容:(a)メッセージ、(b)MAC)
  • <Bobの対応>
    • ③Bobはデータを受信すると、自身でもメッセージと共有鍵のハッシュ値を作成(c)し、MAC(b)と照合
    •  ※相違があれば改ざんされたことがわかり、一致すれば内容が正しいことがわかります。

一方で欠点は、以下のようになります。 

  • メッセージは暗号化されていないため、秘匿性がない
  • 共有鍵を持っている相手としかこの手法が使えない

2.2 電子署名

電子署名は、ネットワーク上でデータの送り主が本人であることを証明するための仕組みです。公開鍵暗号方式が利用されています。

※公開鍵暗号方式では、公開鍵と秘密鍵がペアで作られ、秘密鍵で暗号化したものは公開鍵で復元(元に戻す)できるようになっています。公開鍵は広く公開されており、秘密鍵は本人しか知り得ないものとして、プライベートな環境下で保管されるものです。ただし、電子署名においてもメッセージが秘匿されるわけではありません。

電子署名1
  • 事前準備:Aliceは自身の秘密鍵と公開鍵のペアを作り、公開鍵だけ公開しておく
  • <Aliceの対応>
    • ①メッセージ(a)を作成し、メッセージをハッシュ化したもの(b)を秘密鍵で暗号化した、暗号文(c、これを電子署名という)をメッセージに添付。
    • ②送信(送信内容:(a)メッセージ、(c)暗号文(電子署名))
  • <Bobの対応>
    • ③Bobがデータを受信
    • ④Bob自身もメッセージからハッシュ値を作成(d)し、またAliceの公開鍵で電子署名を復元し(b)、この2つを比較
    •  ハッシュ値が一致するかどうかで(HMACの例と同じように)メッセージが改ざんされたか判断。
    •  さらにAliceの公開鍵で復元できるか確認することで、本人確認にもなる

3. ハッシュライブラリー(Python)

ここでは、Pythonのhashlibライブラリーの使い方を紹介します。Pythonは、現時点で最新の3.6.5を利用しています。

3.1 取り扱いハッシュ関数

hashlibはビルトインではないので、インポートが必要です。まずはhashlibで使えるハッシュ関数一覧です。

#hashlib.algorithms_available:利用可能なハッシュアルゴリズムの種類の確認
import hashlib
h = hashlib.algorithms_available
h

戻り値です。 

{'sha', 'sha512', 'md4', 'SHA384', 'sha384', 'sha3_256', 'SHA256', 'sha1', 'shake_256', 'MD5', 'md5', 'whirlpool', 'dsaEncryption', 'ecdsa-with-SHA1', 'sha256', 'ripemd160', 'sha3_384', 'DSA-SHA', 'blake2b', 'sha224', 'RIPEMD160', 'MDC2', 'sha3_512', 'MD4', 'sha3_224', 'SHA', 'blake2s', 'DSA', 'dsaWithSHA', 'SHA512', 'SHA1', 'shake_128', 'SHA224', 'mdc2'}

3.2 hashlibの基本的な使い方

基本的な使い方としては、任意のハッシュ関数からHASHオブジェクトを作り、表現形式を決めるという形をとりります。

  • hashlib.任意のハッシュメソッド(引数(※byte型))•••HASHオブジェクトを作るメソッド。引数はbyte型のみです
  • HASHオブジェクト.digest()•••byte型でハッシュ値を返すメソッド
  • HASHオブジェクト.hexdigest()•••16進数でハッシュ値を返すメソッド
  • HASHオブジェクト.update(引数(※byte型))•••ハッシュ関数に渡すインプットデータを更新します。

HASHオブジェクトの生成です。

#hashlib.ハッシュアルゴリズム名(引数は任意(byte型に限定)): HASHオブジェクトの生成
import hashlib
hash_obj = hashlib.sha256(b'Hello World') #任意の引数はbyte型限定
type(hash_obj)

戻り値です。HASHオブジェクトが生成されていることがわかります。 

_hashlib.HASH

次はハッシュ値の表現形式です。

#ハッシュ値の表現形式
import hashlib
hash_obj = hashlib.sha256(b'Hello World')
print(hash_obj.digest())          #ハッシュ値をbyte型で表示
print(hash_obj.hexdigest())       #ハッシュ値を16進数で表示

戻り値です。byte型と16進数表示の2形式で表示しています。 

b'\xa5\x91\xa6\xd4\x0b\xf4 @J\x01\x173\xcf\xb7\xb1\x90\xd6,e\xbf\x0b\xcd\xa3+W\xb2w\xd9\xad\x9f\x14n'

a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e

HASHオブジェクトのインプットデータを変える(アップデートする)メソッドです。

#インプットデータの更新
import hashlib
hash_obj = hashlib.sha256(b'Hello World')
hash_obj.update(b'Do you like programming?')  #インプットを"Hello World"から"Do ~ programming?"に変更
print(hash_obj.digest())
print(hash_obj.hexdigest())

戻り値です。先ほど(1つ上のプログラミング)の戻り値と見比べると、インプットデータが変わっているため、ハッシュ値が変わっているのがわかると思います。 

b'\x8a\xc0\x94\xbdV\x19\xcc\x01)\xeeI\xe8\xe10\x13\xab\xb0KL\x9c4[\n-\xcd}Ex\xe4\x0f]\x85'

8ac094bd5619cc0129ee49e8e13013abb04b4c9c345b0a2dcd7d4578e40f5d85

3.3 ハッシュ値の検証

次はハッシュ値の検証です。前半部分で以下のようにハッシュ関数の特性について触れました。

  • ハッシュ値(アウトプット)からインプットを予測できない(不可逆的とも言い、最大の特徴)
  • インプットはデータであればなんでも良く、データ量はいくらでも構わない
  • インプットデータが少し変わるだけで、ハッシュ値は全く異なる(プログラミング部分で検証)
  • ただし、インプットが同じであればハッシュ値は同じ(ランダムとは異なる)
  • アウトプットはハッシュ関数で指定したデータ量になる(プログラミング部分で検証)

それをここで検証してみます。まずは戻り値のデータサイズを検証します。HASHオブジェクトのdigest_size属性は、ハッシュの戻り値のバイトサイズを表します。

#ハッシュ値のサイズ(sha256の場合)
import hashlib
hash_obj1 = hashlib.sha256(b'choiulksogjoidjldfhoddoihgoaji247687d9jodijf')
hash_obj2 = hashlib.sha256(b'1')
print(hash_obj1.digest_size)
print(hash_obj2.digest_size)

戻り値です。文字列の長さに関係なくハッシュ値のデータサイズが同じなのがわかります。 

32
32

sha512の場合でも検証してみます。ハッシュ関数のsha512はsha256よりもハッシュ値のサイズが長いものになり、その分ハッシュ衝突の確率が低くなります。

#ハッシュ値のサイズ(sha512の場合)
import hashlib
hash_obj1 = hashlib.sha512(b'choiulksogjoidjldfhoddoihgoaji247687d9jodijf')
hash_obj2 = hashlib.sha512(b'1')
print(hash_obj1.digest_size)
print(hash_obj2.digest_size)

戻り値です。データサイズが今回も全く同じになっています。 

64
64

次はインプットがわずかでも異なる場合に、ハッシュ値(アウトプット)が異なる点を検証してみます。encodeを使っている部分は次節で説明します。

#インプットが少しだけ異なった場合のハッシュ値の比較検証
import hashlib
hash_obj1 = 2 ** 1000
hash_obj2 = 2 ** 1000 + 1
def hash_hex(obj):
    b_obj = str(obj).encode()
    hash_obj = hashlib.sha256(b_obj).hexdigest()
    return hash_obj
print(hash_hex(hash_obj1))
print(hash_hex(hash_obj2))

戻り値です。全く違うのがわかると思います。 

8c5d0b143c6a93c64bcd6f29fedfeea73a7198430f420372155ed5ace8c25e0a

6a7caf69debb6bd8dc6dec2eb09547bd5ee45591f9babdf9f3bb1693c785be0e

ちなみに「2の1000乗」とはこの数値になります。この数字は日本で使用されるくらいの数の最大単位「無量大数」をはるかに超えるものです。

「10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376」

ちなみにちなみにですが、一説によると無量大数は17世紀の書物が出典となってるのですが、3世紀の仏教書物にはさらに上の「不可説不可説転」という位が書かれているらしいです(この「不可説不可説転(ふかせつふかせつてん)」ですが、私の使っているMacbookで一発で変換できました笑)。

3.4 バイナリファイルでない文字列のハッシュ化

最後は、byte型でないデータをbyte型に変換してハッシュ化する一連の流れです(日本語などを扱う場合とかですね)。一旦文字コードメソッドのencodeを使ってbyte型に変換してからハッシュ関数の引数に渡すだけです。

#バイナリファイル出ない文字列をハッシュ化する場合
import hashlib
a = 'ハロー ワールド'
A = a.encode('utf-8')            #encodeメソッドでbyte型に変換
hashlib.sha256(A).hexdigest()

戻り値です。byte型以外のデータを引数に渡すとエラーになります。 

'284740a81a7fab0209c6072f102433728e4035ddf94915939ac280817385f86f'

4. まとめ

以上が、現状私がハッシュ関数について理解している部分です。概ねの概要やコーディングは非常にわかりやすいのに、セキュリティを高める上でなくてはならない存在であり、かなり興味深く勉強することができました。今回も、駄文ながら読んでくださった方がいれば、ありがとうございます。(日々どれくらいの訪問数があるかは理解してますが・・・)

5. 参考サイト