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

この記事からわかること

  • Swiftローカルセキュリティ高く保存する方法
  • KeyChain使い方実装方法
  • クエリ構築方法:CFDictionar

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

参考文献:Using the iOS KeyChain in Swift

iOSアプリで端末(ローカル)にデータを永続的に保存する方法

Swiftではアプリ内からデータを永続的に保存する手段がいくつか存在します。

それぞれに一長一短がありますが、今回はKeyChainを使用した方法をまとめていきます。

KeyChainとは?

公式リファレンス:KeyChain Services

KeyChainAppleのプラットフォームで提供されるセキュアなストレージです。ユーザーのログイン情報となるパスワードやクレジットカード情報、暗号鍵など秘密にしておきたい小さな情報を保存するためのデータベースとしての活用が公式からは推奨されています。

KeyChainを使用することでユーザーがログイン情報を安全に端末内に保持することが可能になり、ログアウト時でも保存した情報を使用して簡単にアクセスできるような仕組みを実装することも可能です。UserDefaultsなどにログイン情報を貯めることも可能ですが、UserDefaultsはプロパティリストに生のデータをそのまま格納しているだけなので、知見のある人には閲覧できてしまうため安全面のことを考えるとあまり得策とは言えません。

一方KeyChainでは保存対象のデータを暗号化し、プロビジョニングプロファイルと関連づけることで安全性を確保しています。またUserDefaultsはアプリを削除するとデータも一緒に削除されますが、KeyChainは削除されないのも特徴の1つです。

Xcodeにはデフォルトで組み込まれているので導入の手順は必要なく使用することが可能ですが、KeyChainを使いやすくするためのライブラリなどはあるので状況に応じて活用可能です。今回はライブラリなどは無しで実装していきます。

まとめ

ここからは実際にKeyChainの使用方法をまとめていきます。

クエリの構築:CFDictionary

まず基本となるのがクエリです。KeyChainのクエリはデータを保存・検索するために使用するパラメータで、ここに実際に保存したいデータやアクセスするためのキーを持たせます。データ型はCFDictionary型と呼ばれる辞書型になっています。保存したいデータはData型である必要があるため、String型からのキャストが必要になります。

辞書型のキー値にはあらかじめ用意されているkSecValueDatakSecClassなどを保存や検索などの状況に応じて渡します。

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つが用意されているので該当のものを選んで渡します。

データを保存する:SecItemAdd

公式リファレンス: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

公式リファレンス: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

公式リファレンス: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

公式リファレンス: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")
            }
        }
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index