パスワードを安全に保存する
iOSでパスワードを保存したい時は、キーチェーンを利用する場合がほとんどだと思います。このキーチェーンはパスワードを「暗号化」して保存する仕組みになっています。
ただ、昨今パスワード漏洩が大きな問題となっている為、やはりクリティカルなパスワードは「ハッシュ化」して保存したいという場合があります。
(つい先日も大規模な漏洩があったので、急遽「パスワードはハッシュ化してくれ」って言われたのがこの記事のきっかけだとか・・・・)
iOSの場合、PBKDF2であれば特に外部のライブラリを使うことなくハッシュ化を実装できます。
ただ、ちょっと実装がいるので記事としてまとめました。
PBKDF2の実装
まずCommonCrypto
というライブラリをインポートします。
(現在はこの一文だけでインポートは完了します)
import CommonCrypto
次にPBKDF2の設定値を決めておきます。
let iterations = UInt32(100000) // イテレーション回数
let prf = kCCPRFHmacAlgSHA256 // 利用するハッシュ関数
let saltLength = 64 // ソルトの長さ
let hashedLength = 256 // 出力されるハッシュの長さ
設定値によってセキュリティの強度が変わるので、ここの設定は案件毎にセキュリティに詳しい人と相談して決めましょう。
なお、ハッシュ関数については、
設定値 | |
---|---|
kCCPRFHmacAlgSHA1 | HMAC-SHA1 |
kCCPRFHmacAlgSHA224 | HMAC-SHA224 |
kCCPRFHmacAlgSHA256 | HMAC-SHA256 |
kCCPRFHmacAlgSHA384 | HMAC-SHA384 |
kCCPRFHmacAlgSHA512 | HMAC-SHA512 |
から選択できます。
設定値が決まれば、いよいよハッシュ化の処理です。
ハッシュ化はCCKeyDerivationPBKDF
というメソッドを使いますが、元はCのAPIなのでSwiftからはちょっと使いにくいです。
それで、次のようなSwift用にラップしたメソッドを作ります。
func pbkdf2(password: String, salt: Data, iterations: UInt32) -> Data {
var hashed = Data(count: hashedLength)
let saltBuffer = [UInt8](salt)
let result = hashed.withUnsafeMutableBytes { data in
CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2),
password, password.count,
saltBuffer, saltBuffer.count,
CCPseudoRandomAlgorithm(prf),
iterations,
data, hashedLength)
}
guard result == kCCSuccess else { fatalError("pbkdf2 error") }
return hashed
}
また、パスワードをハッシュ化する場合のソルトは、ランダムで生成されたものが良いのでソルトを生成するメソッドも用意します。
func generateSalt(length: Int) -> Data {
return Data(bytes: (0..<length).map { _ in UInt8.random(in: 0...UInt8.max) })
}
アプリで利用する
アプリ上でハッシュ化してパスワードを保存する場合、
- パスワードをハッシュ化する
- 入力されたパスワードと保存済のハッシュ化されたパスワードを比較
という2つの処理が必要です。
パスワードをハッシュ化する
ユーザに決めてもらったパスワードを保存する時のコードです。
func encode(password: String) -> (hash: String, salt: String) {
// ランダムなソルトを生成する
let salt = generateSalt(length: saltLength)
// パスワードをハッシュ化する
let hash = pbkdf2(password: password, salt: salt, iterations: iterations)
// 保存しやすいようにBase64化しておく
return (hash: hash.base64EncodedString(), salt: salt.base64EncodedString())
}
アプリから呼び出す時は以下のようになります。
let password = "ユーザが入力したパスワード"
let result = encode(password: password)
print("ハッシュ化されたパスワード: ", result.hash)
print("ソルト: ", result.salt)
あとは、ハッシュ化されたパスワードとソルトの両方を保存します。
保存先はキーチェーンにしておくとより確実かと思います。
なお、イテレーション回数が変わるとハッシュ化の結果が違ってきますので、もしイテレーション回数を可変する場合はイテレーション回数も保存が必要です。
パスワードを比較する
次に、保存されているパスワードとユーザが入力したパスワードを比較する時のコードです。
func verify(password: String, hash: String, salt: String) -> Bool {
// Base64化されたソルトをDataに戻す
guard let saltData = Data(base64Encoded: salt) else { return false }
// 保存されていたソルトを使って入力したパスワードをハッシュ化する
let inputHash = pbkdf2(password: password, salt: saltData, iterations: iterations)
// ハッシュ化されたパスワードと保存済のハッシュ化されたパスワードを比較
return inputHash.base64EncodedString() == hash
}
アプリから呼び出す時は以下のようになります。
// let savedHash = 保存しておいたハッシュ化されたパスワード(Base64)
// let savedSalt = 保存しておいたソルト(Base64)
let ok = verify(password: "ユーザが入力したパスワード", hash: savedHash, salt: savedSalt)
print("結果: ", ok) // true
let ng = verify(password: "違うパスワード", hash: savedHash, salt: savedSalt)
print("結果: ", ng) // false
参考: ハッシュ化は必要か?
一般的にパスワードを安全に保存しておくには「暗号化」ではなく「ハッシュ化」しておくことが必要1だと言われています。
というのも、パスワードが漏れてしまった時に、ハッシュ化していれば簡単に元のパスワードに戻すことができませんが、暗号化したパスワードの場合は、復号する為の鍵も一緒に漏れると簡単に元のパスワードに戻されてしまうからです。
では、ハッシュ化せずにキーチェーンに保存しておくのは安全では無いのか?と言われると、一概にそうは言えません。詳しくは参考資料2やiOSのセキュリティガイドなどを参照していただきたいのですが、キーチェーンのデータの復号用の鍵はアプリ側で保管されていなかったり、暗号化専用のハードが用意されていたりと、暗号化したデータと一緒に鍵も漏れてしまうということが起こりにくいように色々と対策が取られています。
という訳で、一般的な個人のパスワード程度であれば、このキーチェーンへの保存で十分に安全と言えますし、Appleのリファレンスでもパスワードはキーチェーンへ保存するよう記載されています。
ただ、やっぱりパスワード系はハッシュ化していないと心配で眠れないとか、なんか気持ち悪いって場合は、そんなに実装コストのかかるものでも無いのでさくっとハッシュ化してしまいましょう!
-
ただし、ハッシュ化して保存すると元のパスワードに戻せなくなる為、平文のパスワードが必要な場合はハッシュ化ではなく暗号化での保存が必要です。例えば、Safariのパスワード保存のようにアプリから別サービスへパスワードを使ってログインをするような場合は、暗号化で保存しないといけません。 ↩
-
https://github.com/OWASP/owasp-mstg/blob/master/Document/0x06d-Testing-Data-Storage.md ↩