[iOS] 複数のキーチェーンアクセスグループを持つアプリではアクセスグループを明示的に指定しよう

2016.11.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんちは。モバイルアプリサービス部の加藤潤です。
今回の記事の内容を一言で表すと掲題の通りです。
と言っても何を言いたいのかよくわからないと思いますので順を追って説明したいと思います。キーチェーンのAPIは直接は扱いづらいので自分でラッパーを作るか、OSSなどを利用している人も多いのではないでしょうか。今回はKeychainAccessを使って説明します。

キーチェーンアクセスグループが1つの場合

まず、キーチェーンアクセスグループが1つの場合の例として、 Keychain Sharingを有効にしていない( アプリがkeychain-access-groupsのentitlementsを持たない)場合を見ていきます。その場合はapplication identifier(TeamID + Bundle identifier)がデフォルトのアクセスグループとして使われます。

例えば作成と削除は以下のようになります。

let keychain = Keychain(service: "testService")
// 作成
do {
    try keychain.set("testValue", key: "testKey")
} catch let error {
    print(error)
}

// 削除
do {
    try keychain.remove("testKey")
} catch let error {
    print(error)
}

上記の場合、明示的にアクセスグループを指定していませんが、 その場合上述のデフォルトアクセスグループとなるため、作成したアイテムのアクセスグループ(kSecAttrAccessGroup)はTeamIDが12ABCD3E4F、Bundle identifierがcom.example.KeychainSharingSampleの場合、12ABCD3E4F.com.example.KeychainSharingSampleとなります。

このように、1つのアクセスグループのみ使用するアプリはアクセスグループを明示的に指定しなくても特に問題はありません。

キーチェーンアクセスグループが2つ以上の場合

次にKeychain Sharingを有効にしてアプリが複数のアクセスグループに所属するようにした場合を見てみます。 Keychan Sharingを有効にすると、自動でデフォルトのアクセスグループが作られますが、今回は自分でもアクセスグループを1つ追加し、そちらを他のアプリと共有したいとします。

keychain-access-groups

自分で追加したアクセスグループに対してアイテムを保存、削除するには以下のようにアクセスグループ名を指定します。

let sharedkeychain = Keychain(service: "testService", accessGroup: "12ABCD3E4F.com.example")

// 作成や削除の方法はデフォルトアクセスグループの時と同じ

アクセスグループを指定しなかった場合、デフォルト以外のアクセスグループのアイテムにアクセスできてしまう

さて、アプリの中にアクセスグループが2つある場合、それぞれにアクセスするために以下のようにKeychainのインスタンスを生成してしまうと思います。

let keychain = Keychain(service: "testService")
let sharedkeychain = Keychain(service: "testService", accessGroup: "12ABCD3E4F.com.example")

実は上記のようにした場合、アイテムのKeyが重複するとkeychainを通して、アクセスグループが12ABCD3E4F.com.exampleのアイテムの取得、変更、削除ができてしまいます。 それぞれについて見ていきたいと思いますが、準備としてsharedkeychainの方にアイテムを1つ作成しておきましょう。 ちなみに、アクセスグループを指定する時にコードに直接TeamIDを含めたくない場合、以下のようにInfo.plistにアクセスグループを定義しておくといい感じになります。

shared_access_group

// Info.plistからSharedAccessGroupを取得
let sharedAccessGroup = Bundle.main.object(forInfoDictionaryKey: "SharedAccessGroup") as! String

// アクセスグループを指定してKeychainのインスタンスを生成
let sharedkeychain = Keychain(service: "testService", accessGroup: sharedAccessGroup)

// アイテムの作成
do {
    try sharedkeychain.set("testValue", key: "testKey")
} catch let error {
    print(error)
}

// 確認
let data = sharedkeychain["testKey"]
print("data: \(data)")

// 出力
data: Optional("testValue")

アイテムの取得

まずはkeychainを通してアクセスグループが12ABCD3E4F.com.exampleのアイテムが取得できるか試してみます。

let keychain = Keychain(service: "testService")

// 取得
do {
    let data = try keychain.get("testKey")
    print("data: \(data)")
} catch let error {
    print(error)
}

// 出力
data: Optional("testValue")

keychainを通して、アクセスグループが12ABCD3E4F.com.exampleのアイテムが取得できました。

アイテムの変更

続いてアイテムの変更をしてみます。

// 変更
do {
    try keychain.set("dummy", key: "testKey")
} catch let error {
    print(error)
}

// 確認
let data = sharedkeychain["testKey"]
print("data: \(data)")

// 出力
data: Optional("dummy")

こちらもkeychainを通して、アクセスグループが12ABCD3E4F.com.exampleのアイテムの値を変更できました。

アイテムの削除

最後にアイテムの削除です。

// 削除
do {
    try keychain.remove("testKey")
} catch let error {
    print(error)
}

// 確認
let data = sharedkeychain["testKey"]
print("data: \(data)")

// 出力
data: nil

こちらもkeychainを通して、アクセスグループが12ABCD3E4F.com.exampleのアイテムの削除ができました。

まとめ

上記のように、アクセスグループを指定しなかった場合、キーが一致してしまえば、アプリが所属するアクセスグループに属する全アイテムに(グループをまたがって!)アクセスできてしまいます。 これはKeychain Services Programming Guideにも書かれているので、どうやら仕様のようです。この仕様を理解していないと、思わぬところでアイテムの変更や削除を行ってしまいそうです。
アクセスグループが複数あるアプリの場合は明示的にアクセスグループを指定するようにしましょう。

参考

Keychain Services Programming Guide - Access Groups