【Swift】KeyChainでデータをセキュリティを高めてローカルに保存する方法!

この記事からわかること
- Swiftでローカルにセキュリティを高く保存する方法
- KeyChainの使い方と実装方法
- クエリの構築方法:CFDictionar
index
[open]
\ アプリをリリースしました /
参考文献:Using the iOS KeyChain in Swift
iOSアプリで端末(ローカル)にデータを永続的に保存する方法
Swiftではアプリ内からデータを永続的に保存する手段がいくつか存在します。
- テキストファイル
- UserDefaults
- Realm Swift(ライブラリ)
- KeyChain
それぞれに一長一短がありますが、今回はKeyChainを使用した方法をまとめていきます。
KeyChainとは?
KeyChainはAppleのプラットフォームで提供されるセキュアなストレージです。ユーザーのログイン情報となるパスワードやクレジットカード情報、暗号鍵など秘密にしておきたい小さな情報を保存するためのデータベースとしての活用が公式からは推奨されています。
KeyChainを使用することでユーザーがログイン情報を安全に端末内に保持することが可能になり、ログアウト時でも保存した情報を使用して簡単にアクセスできるような仕組みを実装することも可能です。UserDefaultsなどにログイン情報を貯めることも可能ですが、UserDefaultsはプロパティリストに生のデータをそのまま格納しているだけなので、知見のある人には閲覧できてしまうため安全面のことを考えるとあまり得策とは言えません。
一方KeyChainでは保存対象のデータを暗号化し、プロビジョニングプロファイルと関連づけることで安全性を確保しています。またUserDefaultsはアプリを削除するとデータも一緒に削除されますが、KeyChainは削除されないのも特徴の1つです。
Xcodeにはデフォルトで組み込まれているので導入の手順は必要なく使用することが可能ですが、KeyChainを使いやすくするためのライブラリなどはあるので状況に応じて活用可能です。今回はライブラリなどは無しで実装していきます。
まとめ
- セキュアなストレージ
- パスワードなど秘密の小さな情報を保存
- データは暗号化
- アプリを削除しても消えない
ここからは実際にKeyChainの使用方法をまとめていきます。
クエリの構築:CFDictionary
まず基本となるのがクエリです。KeyChainのクエリはデータを保存・検索するために使用するパラメータで、ここに実際に保存したいデータやアクセスするためのキーを持たせます。データ型はCFDictionary
型と呼ばれる辞書型になっています。保存したいデータはData
型である必要があるため、String
型からのキャストが必要になります。
辞書型のキー値にはあらかじめ用意されているkSecValueData
やkSecClass
などを保存や検索などの状況に応じて渡します。
let query = [
kSecValueData: "パスワード".data(using: .utf8)!,
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
キー | 概要 |
---|---|
kSecValueData | 実際のデータをData型で渡す |
kSecClass | 保存したいデータの項目(クラス)を選択(※) |
kSecAttrService | サービス名(com.XXX.app / MyAppなど) |
kSecAttrAccount | アカウント名(email.email.com / username など) |
kSecReturnData | 結果をデータのみで返すかどうか(true/false) |
kSecClassで渡せる値
※kSecClass
に渡す値は保存したいデータの項目によって異なります。以下の5つが用意されているので該当のものを選んで渡します。
- kSecClassInternetPassword:インターネット関連のパスワード
- kSecClassGenericPassword:一般的なパスワード
- kSecClassCertificate:証明書
- kSecClassKey:鍵(公開鍵/秘密鍵など)
- kSecClassIdentity:識別子
データを保存する:SecItemAdd
func SecItemAdd(
_ attributes: CFDictionary,
_ result: UnsafeMutablePointer<CFTypeRef?>?
) -> OSStatus
実際にデータを保存するのはSecItemAdd
メソッドです。引数に保存するCFDictionary
を渡し、その結果がOSStatus
型で返ります。
let query = [
kSecValueData: "パスワード".data(using: .utf8)!,
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
SecItemAdd(query, nil)
データを取得する:SecItemCopyMatching
func SecItemCopyMatching(
_ query: CFDictionary,
_ result: UnsafeMutablePointer<CFTypeRef?>?
) -> OSStatus
キーチェーンに保存されているデータを取得するのはSecItemCopyMatching
メソッドです。引数に渡すのは検索用のCFDictionary
で、kSecReturnData: true
を含めることでもう1つの引数result
から取得できる値をデータのみにすることができます。
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
kSecReturnData: true
] as CFDictionary
var result:AnyObject?
let status = SecItemCopyMatching(query, &result)
if let data = result as? Data {
let text = String(data: data, encoding: .utf8)
}
resultの中身はAnyObject?
型のままなのでData
型→String
型へとキャストして取り出します。
データを更新:SecItemUpdate
func SecItemUpdate(
_ query: CFDictionary,
_ result: UnsafeMutablePointer<CFTypeRef?>?
) -> OSStatus
データを更新するのはSecItemUpdate
メソッドを使用します。
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
SecItemUpdate(query, [kSecValueData: "Newパスワード".data(using: .utf8)!] as CFDictionary)
データの削除:SecItemDelete
func SecItemDelete(_ query: CFDictionary) -> OSStatus
データを削除するのはSecItemDelete
メソッドを使用します。
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
let status = SecItemDelete(query)
OSStatus型
public typealias OSStatus = Int32
OSStatus
型はKeyChain独自のものではなく、実行結果の状態を保持するInt32
型のタイプエイリアスです。OSStatus
型としてさまざまなエラーラベルが用意されており以下のように分岐させることができます。
switch status {
case errSecItemNotFound:
// データが見つからなかったよ
case errSecSuccess:
// データがあったよ
case noErr:
print("noError")
default:
print("Error")
}
全体像:Swift UI
最後にKeyChainを使用したプロジェクトの全体像を載せておきます。
import SwiftUI
struct ContentView: View {
func entry(password: String) {
let query = [
kSecValueData: password.data(using: .utf8)!,
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
let status = SecItemCopyMatching(query, nil)
switch status {
case errSecItemNotFound:
SecItemAdd(query, nil)
break
case errSecSuccess:
SecItemUpdate(query, [kSecValueData: password.data(using: .utf8)!] as CFDictionary)
break
default:
print("Error")
}
}
func getData() {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
kSecReturnData: true
] as CFDictionary
var result:AnyObject?
let status = SecItemCopyMatching(query, &result)
if let data = result as? Data {
let text = String(data: data, encoding: .utf8)
print(text)
}else{
print("データがないよ")
}
}
func delete() {
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.test.app",
kSecAttrAccount: "email.email.com",
] as CFDictionary
let status = SecItemDelete(query)
}
var body: some View {
VStack {
Button {
entry(password: "12345678")
} label: {
Text("entry")
}
Button {
getData()
} label: {
Text("getData")
}
Button {
delete()
} label: {
Text("delete")
}
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。