😬

[iOS] Keychainの書き込みでAccessGroupを指定しないときの挙動

2024/11/08に公開

要約

KeychainのAccessGroupが複数設定されている場合、書き込み時にAccessGroupを明示的に指定しないと、デフォルトのAccessGroupに書き込まれてしまう。
デフォルトのAccessGroupは、1番目に指定されているAccessGroupになる。
気付きづらいバグの原因になるため、AccessGroupを指定して書き込むことが望ましい。

環境

  • Xcode(16.0)

Keychainとは

iOSのデバイス上に、セキュアにデータを保存するためのしくみ。
UserDefaultと違い、データは暗号化されて保存される。
また、AccessGroupを指定することで、アプリ間でデータを共有することも可能。

KeychainのAccessGroupを使用したデータの保存

Keychainに保存するデータは、AccessGroupを設定することで、データを共有する範囲を制御できる。
AccessGroupの指定は、任意であるため、指定せずに読み書きすることも可能。
"書き込み"のときにAccessGroupを指定しない場合、デフォルトのAccessGroupで保存される。
このとき、デフォルトのAccessGroupは、1番目に指定されているAccessGroupになる。

Appleのドキュメント Storing keys in the keychain

The system considers the first item in that list as the default keychain group. If you omit the kSecAttrAccessGroup attribute when writing keychain items, the system automatically populates that attribute with the default keychain group, and therefore the order in which you specify your keychain groups matters.

システムは、AccessGroupのリストの最初の項目をデフォルトのキーチェーングループとして扱います。
Keychain Itemを書き込むときにkSecAttrAccessGroup属性を省略すると、システムはその属性をデフォルトのAccessGroup自動的に埋めるため、AccessGroupを指定する順序が重要です。


このとき、デフォルトのAccessGroupは、"YourTeamID.Ueeki.keychainTest"になる。

以下のコードを使用して、以下の4通りの場合でデータを保存してみる。

  • AccessGroup = .empty
  • AccessGroup = .group1
  • AccessGroup = .group2
  • AccessGroup = .group3
enum AccessGroup: String, CaseIterable {
    case empty = ""
    case group1 = "YourTeamID.Ueeek.keychainTest"
    case group2 = "YourTeamID.Ueeek.keychainTest2"
    case group3 = "YourTeamID.Ueeek.keychainTest3"
}

func storeDataInKeychain(_ key: String, data: String, group: AccessGroup) {
    var query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data.data(using: .utf8)!
    ]
    
    // .empty以外のときは、`kSecAccessGroup`を指定する
    if group != .empty {
        query[kSecAttrAccessGroup as String] = group.rawValue as AnyObject
    }
    
    let status = SecItemAdd(query as CFDictionary, nil)
}

storeDataInKeychain("key0", data: "data0", group: .empty)
storeDataInKeychain("key1", data: "data1", group: .group1)
storeDataInKeychain("key2", data: "data2", group: .group2)
storeDataInKeychain("key3", data: "data3", group: .group3)

保存したデータを以下のコードで取得してみる。

func readDataFromKeychain(group: AccessGroup) {
    var query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecReturnData as String: kCFBooleanTrue!,
        kSecReturnAttributes as String: kCFBooleanTrue!,
        kSecMatchLimit as String: kSecMatchLimitAll
    ]

    // .empty以外のときは、`kSecAccessGroup`を指定する
    if group != .empty {
        query[kSecAttrAccessGroup as String] = group.rawValue as AnyObject
    }
    
    var item: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &item)
    print(item?.count)
}

readDataFromKeychain(group: .empty) // "4" is printed
readDataFromKeychain(group: .group1) // "2" is printed
readDataFromKeychain(group: .group2) // "1" is printed
readDataFromKeychain(group: .group3) // "1" is printed
  • readDataFromKeychain(group: .empty)で"4"が表示されるのは、AccessGroupでフィルタせずに読み込んだため。(1 + 1 + 1 + 1)
  • readDataFromKeychain(group: .group1)で"2"が表示されるのは、AccessGroupを".group1"で指定したものと、AccssGroupを指定しなかったもの(デフォルトの.group1に保存された)が読み込まれたため。(1 + 1)

AccessGroupを指定せずに書き込むことによる問題の例

  • 意図せずに、ほかのアプリから見える場所にデータを書き込んでまう。
  • 新しくAccessGroupを追加したときに、デフォルトのAccessGroupが変化してしまう可能性がある。その際、ASISとTOBEで書き込む場所が変わってしまう。

まとめ

Keychainでデータを書き込むときは、潜在的な問題を防ぐため、AccessGroupを指定して書き込むことが望ましい。

Ref

Discussion