【Webサービス開発】ユーザ情報を保護する方法
概要
Webサービスで個人情報を取り扱う際に、どのように各レイヤー層において個人情報を保護すべきか、ということを説明する。
まずはWebサービスにおいて、どのようなレイヤーが存在するのか説明する。サービスにおいて、Frontend / APIサーバ / データベースが存在する。
- Frontend:ユーザが直接目にする場所。Amazonで言えば、ユーザが直接操作できるAmazonのウェブサイトが該当する
- APIサーバ:ユーザが入力した情報をデータベースと連携し格納するためのもの。Amazonで言えば、ユーザが購入、お気に入り登録など、ユーザが行なった操作をデータベースに対して保存するための処理
- データベース:決まった形式により整理されたデータがある場所。Amazonで言えば、商品情報やユーザ情報が保存されている。
データベース層
暗号化によるデータ保護
データベース層におけるデータ保護について、平文でデータ保護すると、攻撃者にデータ情報を抜き取られると、各個人アカウントが意図しない形で使用されてしまう。
このような事象を避けるために、データベース情報は暗号化して保管することが一般的である。
そうすることで、攻撃者はデータベース情報のパスワードを復号化する必要があり、データベース情報が流出しても個人アカウントが悪用されることを免れる。復号化されたパスワードをログインフォームで使用しても、平文のパスワードとマッチしないため、攻撃者はログインできない。
上記施策による懸念点
ただし、二点懸念事項がある。一つ目は、攻撃者がデータベース情報と復号鍵を取得すれば、個人情報が悪用されてしまうこと。二つ目は、FrontendとAPIサーバで通信中の情報は、平文であるため、取得されると悪用されてしまうこと。
ハンズオン
どのように、データを暗号化し復号化するか、例を下記に示す。
暗号化
# 暗号化に必要なパッケージをインストールする
from cryptography.fernet import Fernet
# pythonライブラリのcryptograpyを使って、暗号化ライブラリFernetを初期化する
# 上記のため、最初に暗号化で使用するキーを定義しインスタンス作成でキーを使用している
# Fernetインスタンスを作成することで、暗号化や復号化の各種メソッドが使用できる
key=b'8cozhW9kSi5poZ6TWFuMCV123zg-9NORTs3gJq_J5Do='
f = Fernet(key)
# 暗号化したいメッセージを定義する
message = b'encrypting is just as useful'
# メッセージを暗号化して、その結果を出力する
ciphertext = f.encrypt(message)
print(ciphertext)
復号化
# 暗号化に必要なパッケージをインストールする
from cryptography.fernet import Fernet
# pythonライブラリのcryptograpyを使って、暗号化ライブラリFernetを初期化する
# 上記のため、最初に暗号化で使用するキーを定義しインスタンス作成でキーを使用している
# Fernetインスタンスを作成することで、暗号化や復号化の各種メソッドが使用できる
key=b'8cozhW9kSi5poZ6TWFuMCV123zg-9NORTs3gJq_J5Do='
f = Fernet(key)
# 暗号化されたメッセージが格納されている。
message = b'gAAAAABc8Wf3rxaime-363wbhCaIe1FoZUdnFeIXX_Nh9qKSDkpBFPqK8L2HbkM8NCQAxY8yOWbjxzMC4b5uCaeEpqDYCRNIhnqTK8jfzFYfPdozf7NPvGzNBwuuvIxK5NZYJbxQwfK72BNrZCKpfp6frL8m8pdgYbLNFcy6jCJBXATR3gHBb0Y='
# 暗号化されたメッセージを復号化して、その結果を出力する。
decryptedtext = f.decrypt(message)
print(decryptedtext)
通信データ
SSL/TLSによる暗号化
Frontend ⇄ APIサーバ ⇄ データベース間の通信データ暗号化について、解説する。「データベース」章で記載した通り、二つの懸念事項がある。
- 一つ目は、攻撃者がデータベース情報と復号鍵を取得すれば、個人情報が悪用されてしまうこと。
- 二つ目は、FrontendとAPIサーバで通信中の情報は、平文であるため、取得されると悪用されてしまうこと。
それぞれ下記のように、対応する必要がある。
- 一つ目:一つの鍵に依存せず、二つの鍵を使用すること。(*現状、一つの鍵が暗号化と復号化の両方の役割を担っているため)
- 二つ目:通信中データを暗号化すること
結論から言うと、上記課題を解決するのがSSL/TLSである。
対称暗号化 / 非対称暗号化
一つの鍵が暗号化と復号化の両方の役割を担っているため、「データベース層」で解説した暗号化は、対称暗号化である。一方で、公開鍵or秘密鍵を使用し暗号化して、復号化する際に秘密鍵or公開鍵使用する方法を非対称暗号化と呼ぶ。公開鍵によって暗号化されたデータは、秘密鍵でのみ復号化ができる。どのような仕組みか、下記に示す。
対称暗号化は、同じ鍵で暗号化と復号化を行うため、下記の通りである。
一方で非対称暗号化は、公開鍵or秘密鍵を使用し暗号化して、復号化する際に秘密鍵or公開鍵使用するので、下記の通りとなる。
送信者と受信者の観点からすると、両者は公開鍵と秘密鍵の両方を生成した公式の組織に接続している必要がある。送信者は、受信者の公開鍵情報を公開鍵ディレクトリで調べて、メッセージを暗号化して送信する。受信者は秘密鍵を使用して、そのメッセージを復号化する
最初の質問に戻ると、
- 一つ目:一つの鍵に依存せず、二つの鍵を使用すること。(*現状、一つの鍵が暗号化と復号化の両方の役割を担っているため)
- → 暗号鍵と復号鍵を使用している。(*復号鍵が流出するのは問題)
- 二つ目:通信中データを暗号化すること
- SSL/TLSにより通信中データを保護。
上記施策による懸念点
一つ目に記載の通り、両方の鍵が流出してしまうと、DB上の個人情報が読み取られてしまう。鍵の流出は個人に委ねられる場合もあるため、システム側でコントロールすることが難しい。
豆知識
ブラウザに、下記のようなアイコンがあれば、SSL/TLS通信をしている。
また、OSI参照モデルにおいて、SSl/TLSはセッション層におけるセキュリティ手法である。
ハッシュ化
ハッシュ化とは
ハッシュ化とは、データをランダムな文字列に置き換えた後、元に戻せないように処理する技術のことである。暗号化は、復号鍵と秘密鍵があれば、暗号化データを元に戻すことができた。一方で、ハッシュ化されたデータは、元の文字列に戻すことができない。
具体的なハッシュ化の手法については、以下の4つがある。bcryptとscryptと比較して、SHA-1とMD5は安全性が低いため、現在はbcryptとscryptの使用が推奨されている。
- bcrypt
- scrypt
- SHA-1
- MD5
SHA-1とMD5の安全性が低い理由
SHA-1とMD5の安全性が低い理由については、以下の通り。
- コリジョン攻撃の脆弱性
- コリジョン攻撃とは、ハッシュ関数の脆弱性を利用した攻撃手法で、異なる二つの入力データが同じハッシュ値を生成するように見つけ出すことである。ハッシュ関数は、異なる入力データに対して異なるハッシュ値を生成するように設計されている。しかし理論的には、ハッシュ値は一定の長さであるため、無限の入力データの組み合わせに対して、必ずコリジョン(重複)が存在する。これを利用して、正当なデータと同じハッシュ値をもつ偽のデータを作成して、それをシステム内に潜入させることである。
- 走力攻撃の進歩
- 現代のコンピュータ技術の進歩により、全てのパスワードの組み合わせを試すブルートフォース攻撃が効果的となっている。全ての可能な入力値を試す走力攻撃に対して、SHA-1とMD5は十分な耐性を保持していない
- 計算速度
- SHA-1とMD5は高速で効率的なハッシュ関数である一方、その速度がセキュリティ上の問題となっている。攻撃者は、短時間で多くのハッシュ値を生成・比較することで、その脆弱性を突ける。
ハンズオン
総力攻撃の手法について、ハンズオンを行い理解を深める。今回のハンズオンでは、下記流れを想定する。
- 攻撃者がデータベース上のユーザIDとハッシュ化されたパスワードを取得
- 攻撃者がハッシュ化されたパスワードの平文を見つけるため、「よく使用されるパスワードリスト」をハッシュ化して、そのハッシュ値を入手済みのユーザパスワードと比較
# 10000個の「よく使用されるパスワードリスト」をインストールする
with open('nist_10000.txt', newline='') as bad_passwords:
nist_bad = bad_passwords.read().split('\n')
print(nist_bad[1:10])
# DB情報が漏洩したことを想定。
# ユーザ名とハッシュ化されたパスワードが攻撃者に取得される。
leaked_users_table = {
'jamie': {
'username': 'jamie',
'role': 'subscriber',
'md5': '203ad5ffa1d7c650ad681fdff3965cd2'
},
'amanda': {
'username': 'amanda',
'role': 'administrator',
'md5': '315eb115d98fcbad39ffc5edebd669c9'
},
'chiaki': {
'username': 'chiaki',
'role': 'subscriber',
'md5': '941c76b34f8687e46af0d94c167d1403'
},
'viraj': {
'username': 'viraj',
'role': 'employee',
'md5': '319f4d26e3c536b5dd871bb2c52e3178'
},
}
# hashlibライブラリをインポート
import hashlib
# 例として、blueberryという文字列をハッシュ化
word = 'blueberry'
hashlib.md5(word.encode()).hexdigest()
下記コードでは、実際にレインボー攻撃を行なっている。
# レインボー攻撃をするため、インストールした「よく使用されるパスワードリスト」をハッシュ化し保存している
rainbow_table = {}
for word in nist_bad:
hashed_word = hashlib.md5(word.encode()).hexdigest()
rainbow_table[hashed_word] = word
# 上記でハッシュ化したパスワードを、実際に取得したユーザのパスワードと比較
# 値がマッチすれば、実際のユーザパスワードの平文が手に入る。
for user in leaked_users_table.keys():
try:
print(user + ":\t" + rainbow_table[leaked_users_table[user]['md5']])
except KeyError:
print(user + ":\t" + '******* hash not found in rainbow table')
上記施策の懸念点
ハッシュ化は常に同じ関数を用いて処理されるため、ハッシュ化された値が常に同じになるという問題がある。例えば、別々のユーザ同士が同じパスワードを使用している場合、ハッシュ値が同じになってしまう。そのため、パスワードが外部に漏洩しても、比較的推測しやすいパスワードの場合は、攻撃者に元データを特定される可能性がある。この懸念点について、次章「ハッシュ化のソルト」で説明する。
ハッシュ化のソルト
ハッシュ化のソルトとは
ハッシュ化のソルトとは、パスワードをハッシュ値へ変換する際に、パスワードに付与するランダムな文字列のこと。ランダムな文字列を付与することで、同じ値であっても別々のハッシュ値が算出される。ソルトが長くなれば長くなるほど、攻撃者はそのハッシュ値の元の平文を特定することが困難となる。
ハンズオン
# bcrypt(ソルト化)に必要なライブラリをインポートする
import sys
!{sys.executable} -m pip install bcrypt
import bcrypt
password = b"studyhard"
# ハッシュ化のラウンド数を指定し強度を決めている、ここでは14と指定。
# ラウンド数が多いほどハッシュかに必要な時間とリソースが増えて、解読が困難となる。
salt = bcrypt.gensalt(14)
# hashpw関数を使用して、ソルトを使ってパスワードをハッシュ化している。
hashed = bcrypt.hashpw(password, salt)
print(salt)
print(hashed)
# bcrypt関数を使ってハッシュ化したパスワードのハッシュ値が正しいかどうかチェックしている
bcrypt.checkpw(password, hashed)
引用
Discussion