iOS 16：秘密値の保存、FaceID認証に基づく個人情報の表示/非表示（LARight）

iOS 16でリリースされた新しいLARightオブジェクトを使用すると、以下のことが可能になります。
- キーチェーンにデータを簡単に保存 (例えば、ログイントークンの保存など)
- 生体認証やデバイスのパスコードによる認証 (例えば、FaceID を使ってアプリの一部のエリアをロックするなど)
- アプリ全体で生体認証のステータスを共有する。
- `.redacted`を使用して、ユーザーが認証していない場合の情報を隠します。

![Screenshot 2022-07-19 at 15.55.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a1b21ca3-5627-ef13-bfb3-5b00e02ef592.png)


新しいiOS 16.1 betaでは、ユーザーがLARightのロックを解除しようとすると、特別なシステムダイアログが表示されるようになりました。

![laright-demo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/cc66bb68-5e52-95c8-c70d-4bb9575d2a35.jpeg)

# キーチェーンにデータを簡単に保存

センシティブなデータを保存するには、まずデータを `Data` 形式に変換する。例えば、ユーザトークンの文字列をutf8エンコーディングでデータに変換することができる。

```swift
guard let encodedData = secretValue.data(using: .utf8) else {
    return
}
```

次に、将来的にデータを取得する方法を定義します（たとえば、生体認証またはデバイスのパスコードフォールバック付きの生体認証など）。ここでは、デバイスのパスコードを使用するフォールバックのある生体認証（FaceIDまたはTouchID）を使用するように定義しています。

```diff
func store(data: Data, withIdentifier: String) async throws {
+    let right = LARight(requirement: .biometry(fallback: .devicePasscode))
}
```

そして、その定義されたメソッドを使って認証を試み、使えるかどうかを確認します。その後、クレデンシャルを識別子（例えばapi_key）付きで保存します。

```diff
func store(data: Data, withIdentifier: String) async throws {
    let right = LARight(requirement: .biometry(fallback: .devicePasscode))
+    try await right.authorize(localizedReason: "Authenticate to access stored secret data.")
+    _ = try await LARightStore.shared.saveRight(right, identifier: withIdentifier, secret: data)
}
```

FaceID機能を使用するために、Info.plistファイルに`NSFaceIDUsageDescription`を追加することを忘れないでください。

また、`let right = LARight(requirement: .biometry)` という権限で、バイオメトリクス認証のみを定義することもできます。ただし、そのようにすると、バイオメトリクス認証でしかデータにアクセスできなくなります。例えば後でユーザーがFaceIDをオフにした場合、データにアクセスすることはできなくなります。

識別子が毎回変わる場合は、Core DataやUserDefaultsなどのどこかに識別子を保存することを忘れないようにしましょう。

## キーチェーンからのデータ取得

過去に保存したデータを再利用するには、まず識別子から`LARight`をロードします。次に、その`LARight`を認証します。
このとき、システムは権利の作成時に定義された認証方法を使用します。
認証が完了したら、データを読み込みます。

ここでは、Swiftの新しい機能である `await` を使用していることに注意してください。
これは、データの読み取りが認証を待つことを意味します。


```swift
func fetchData(withIdentifier: String) async throws -> Data {
    let right = try await LARightStore.shared.right(forIdentifier: withIdentifier)
    try await right.authorize(localizedReason: "Authenticate to access stored secret data.")
    return try await right.secret.rawData
}
```

## 完全なコード

データの読み書きのデモのための完全なコードです。

```swift
//
//  SecretStoreDemo.swift
//  LARightStoreDemo
//
//  Created by Sora on 2022/07/19.
//

import SwiftUI
import LocalAuthentication

struct SecretStoreDemo: View {
    
    @State private var dataIdentifier: String = "demo"
    @State private var secretValue: String = ""
    @State private var statusStr: String = ""
    
    var body: some View {
        Form {
            Section {
                TextField("Data identifier", text: $dataIdentifier)
                TextField("Secret value", text: $secretValue)
            }
            Section {
                Button("Save value") {
                    guard let encodedData = secretValue.data(using: .utf8) else {
                        self.statusStr = "Failed to encode string into data"
                        return
                    }
                    Task {
                        do {
                            try await store(data: encodedData, withIdentifier: self.dataIdentifier)
                            statusStr = "Saved!"
                            secretValue = ""
                        } catch {
                            self.statusStr = error.localizedDescription
                        }
                    }
                }
                .disabled(secretValue.isEmpty || dataIdentifier.isEmpty)
                
                Button("Read value") {
                    Task {
                        do {
                            let data = try await fetchData(withIdentifier: dataIdentifier)
                            guard let decodedStr = String(data: data, encoding: .utf8) else {
                                self.statusStr = "Failed to decode data into string"
                                return
                            }
                            self.secretValue = decodedStr
                        } catch {
                            self.statusStr = error.localizedDescription
                        }
                    }
                }
                .disabled(dataIdentifier.isEmpty)
                
                Button("Delete data") {
                    Task {
                        try? await removeData(withIdentifier: dataIdentifier)
                        secretValue = ""
                    }
                }
                .disabled(dataIdentifier.isEmpty)
            }
            Section {
                if !statusStr.isEmpty {
                    Text(statusStr)
                }
            }
        }
    }
    
    func store(data: Data, withIdentifier: String) async throws {
        let right = LARight(requirement: .biometry(fallback: .devicePasscode))
        try await right.authorize(localizedReason: "Authenticate to access stored secret data.")
        _ = try await LARightStore.shared.saveRight(right, identifier: withIdentifier, secret: data)
    }
    
    func fetchData(withIdentifier: String) async throws -> Data {
        let right = try await LARightStore.shared.right(forIdentifier: withIdentifier)
        try await right.authorize(localizedReason: "Authenticate to access stored secret data.")
        return try await right.secret.rawData
    }
    
    func removeData(withIdentifier: String) async throws {
        try await LARightStore.shared.removeRight(forIdentifier: withIdentifier)
    }
    
}
```

# アプリ全体のFaceID認証

ユーザーの詳細なプロフィール情報など、アプリにはセンシティブな情報を含むエリアが複数存在する場合があります。各ビューが表示されるたびにFaceID認証を呼び出すのは難しいかもしれません。

LARightを使えば、アプリ全体でユニバーサルなFaceID認証を行うことができます。
ユーザーの認証状況を保持し、認証を解除することでユーザーをサインアウトさせることも可能です。

## アプリ全体で共有するオブジェクトを作成

まず、アプリ全体で共有されるオブジェクトを作成します。
これにより、ユーザーが認証されると、アプリ内のすべてのビューがこの変更を知ることができます。

```swift
import Foundation
import SwiftUI
import LocalAuthentication

class AppWideAuthentication: NSObject, ObservableObject {
    
    static let shared = AppWideAuthentication()
    
    @Published var loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
    @Published var currentLoginState: LARight.State = .unknown
    
}
```

上記のコードでは、共有オブジェクトを作成しています。アプリのすべてのビューで、`AppWideAuthentication.shared`を使用してアクセスします。

また、2つの `@Published` 変数があります。SwiftUIのビューでは、値が変更されたときに、公開された変数を参照するビュー要素が自動的にリロードされます。

次に、ログインとサインアウトのための関数コードを追加します。

```diff
class AppWideAuthentication: NSObject, ObservableObject {
    
    static let shared = AppWideAuthentication()
    
    @Published var loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
    @Published var currentLoginState: LARight.State = .unknown
    
+    func login() async throws {
+        try await loginRight.authorize(localizedReason: "Authenticate to access stored secret data.")
+        DispatchQueue.main.async {
+            self.currentLoginState = self.loginRight.state
+        }
+    }
+
+    func logout() async {
+        await loginRight.deauthorize()
+        DispatchQueue.main.async {
+            self.currentLoginState = self.loginRight.state
+        }
+    }
    
}
```

サインインとサインアウトのボタンをビューに追加します。

```swift
struct FaceIDAuthDemo: View {
    
    @ObservedObject var appAuth = AppWideAuthentication.shared
    
    var body: some View {
        Form {
            Button("Login") {
                Task {
                    try? await appAuth.login()
                }
            }
            Button("Logout") {
                Task {
                    await appAuth.logout()
                }
            }
            Label((appAuth.currentLoginState == .authorized) ? "Authorized" : "No access", systemImage: (appAuth.currentLoginState == .authorized) ? "checkmark.circle.fill" : "xmark")
        }
    }
    
}
```

また、アプリの他のビューでも、ユーザーの認証状況を確認できるようになりました。

```swift
struct ContentView: View {
    
    @ObservedObject var appAuth = AppWideAuthentication.shared
    
    var body: some View {
        Label((appAuth.currentLoginState == .authorized) ? "Authorized" : "No access", systemImage: (appAuth.currentLoginState == .authorized) ? "checkmark.circle.fill" : "xmark")
    }
    
}
```

使い方としては、ユーザーがFaceIDで認証されていない場合に、`.redacted`というビューモディファイアを使ってプライベートな情報を隠すことができます。

![Screenshot 2022-07-19 at 15.55.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a1b21ca3-5627-ef13-bfb3-5b00e02ef592.png)

```swift
struct ContentView: View {
    
    @ObservedObject var appAuth = AppWideAuthentication.shared
    
    var body: some View {
        Text("Your account ID is xxxxxx")
            .redacted(reason: (appAuth.currentLoginState == .authorized) ? [] : .placeholder)
    }
    
}
```

ビューモディファイア「.redacted」について詳しく知りたい方は、こちらのqiitaの記事をご覧ください。

https://qiita.com/MaShunzhe/items/4ed187a140259775c23c

---


:thumbsup: お読みいただきありがとうございました。

[☺️ Twitter @MszPro](https://twitter.com/MszPro)
[🐘 Mastodon @me@mszpro.com](https://sns.mszpro.com/@me)

:sunny: <img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">

---


![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)

Written by MszPro~

---

# 関連記事

・[UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用（iOS 16, UIHostingConfiguration）](https://qiita.com/MaShunzhe/items/3d98fa897aa1bcffc74b#github-repository)

・[iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する（iOS16.1以降）](https://qiita.com/MaShunzhe/items/6cc8d9d204f40cbee19c)

・[iOS 16：秘密値の保存、FaceID認証に基づく個人情報の表示/非表示（LARight）](https://qiita.com/MaShunzhe/items/07c1e65277a5008106db)

・[iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完](https://qiita.com/MaShunzhe/items/693e6302876707d5684d)

・[SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)](https://qiita.com/MaShunzhe/items/4e0d1032748fe4f398de)

・[WWDC22、iOS16：iOSアプリに画像からテキストを選択する機能を追加（VisionKit）](https://qiita.com/MaShunzhe/items/ed3985d05975bf35292d)

・[WWDC22、iOS16：数行のコードで作成できるSwiftUIの新機能（26本）](https://qiita.com/MaShunzhe/items/0ed36d8a6de7c56b1d34)

・[WWDC22、iOS 16：SwiftUIでChartsフレームワークを使ってチャートを作成する](https://qiita.com/MaShunzhe/items/cae62af8b07f4f474fd8)

・[WWDC22, iOS 16: WeatherKitで気象データを取得](https://qiita.com/MaShunzhe/items/dce45ccdd05c1f70cf54)

・[WWDC 2022の基調講演のまとめ記事](https://qiita.com/MaShunzhe/items/037580d2ed51a5a6e9d9)