読者です 読者をやめる 読者になる 読者になる

24/7 twenty-four seven

iOS/OS X application programing topics.

SwiftでKeychainを簡単に使うライブラリ "KeychainAccess" を書きました

Swift iOS

kishikawakatsumi/KeychainAccess · GitHub

そろそろSwiftをちゃんと勉強しようと思って作りました。 Swiftで書かれたKeychainのラッパーの中ではもっとも高機能でかつ簡単に使えるものができたと思います。

機能としては下記を備えています

  • 簡単に使えるインタフェース
  • アプリ間のキーチェーン共有
  • アクセシビリティ(バックグラウンド動作時の制限など)属性のサポート
  • iCloudによるキーチェーンの同期
  • Touch IDによるキーチェーンの保護(iOS 8〜)
  • iOSとOS Xの両方の動作をサポート

インストール

github "kishikawakatsumi/KeychainAccess"

pod 'KeychainAccess'
CocoaPodsを使う場合、CocoaPodsのバージョンはbeta版の0.36が必要です。
Pod Authors Guide to CocoaPods Frameworks - CocoaPods Blog

  • マニュアルインストール

1. Lib/KeychainAccess.xcodeprojをプロジェクトに追加(ドラッグ&ドロップ)します。
2. KeychainAccess.frameworkをターゲットにリンクします。
3. Build PhasesにCopy Files Build Phaseを追加して、上記のKeychainAccess.frameworkをバンドルのFrameworksディレクトリにコピーするようにします。

使い方

詳しくは READMEを見てください。

基本

値の保存、取得、削除

保存

まずはじめにKeychainのインスタンスを作成します。 サービス名を指定しなかった場合は、自動的にアプリケーションのバンドルIDが設定されます。 そして、作成したキーチェーンのset(value: String, key: String) -> NSError?メソッドでキーと値を設定します。 setメソッドはNSDataを引数に取るメソッドも用意してあります。

Objective-Cでは型の違う引数によるオーバーロードはできなかったので、名前を変える必要がありましたが(例えばsetString:setDataなど)Swiftでは型違いの引数によるメソッドのオーバーロードがサポートされているので、型の違いを気にすることなく自然に書けます。

let keychain = Keychain(service: "com.example.github-token")
keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

Subscriptingも利用できます。Subscriptingについては型違いによるオーバーロードはサポートされていない(Xcode 6.0ではできていた)ので、Stringのみ受け付けます。

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"

エラー処理

値の保存に失敗した場合はNSErrorオブジェクトが返るので、必要ならエラー処理をします。

if let error = keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi") {
    println("error: \(error)")
}
取得

値を取り出すには、getで始まるメソッド群を使用します。 戻り値の型の違いによって以下の5つのメソッドが用意されています。

get(key: String) -> String?
getString(key: String) -> String?
getData(key: String) -> NSData?
getStringOrError(key: String) -> KeychainAccess.FailableOf<String>
getDataOrError(key: String) -> KeychainAccess.FailableOf<NSData>

String型として値を取得する場合はgetもしくはgetStringを使用します。

let token = keychain.get("kishikawakatsumi")
let token = keychain.getString("kishikawakatsumi")

NSData型として値を取得する場合はgetDataを使用します。

let data = keychain.getData("kishikawakatsumi")

Swiftでは戻り値の型の違いによるオーバーロードもサポートされていますが、戻り値だけが異なるメソッドのオーバーロードは、受ける側の変数で型を明示するか、戻り値をキャストする必要があるため(そうしないとどちらの呼び出しか区別できない)、却って書きにくくなると思ったので、メソッド名自体を変えることにしました。 入力補完が使えるぶん、この方が書きやすいと思います。

エラー処理

エラーを区別(キーに対応する値が無いのかエラーが起こって取得に失敗したのか)したい場合getStringOrErrorもしくはgetDataOrErrorメソッドを使用します。

このメソッドの戻り値の型はFailableOf<String>またはFailableOf<NSData>になります。 FailableOf<T>SuccessFailureのいずれかの状態を取るenumです(ScalaやHaskellなど他の言語ではEitherとして知られています)。 FailableOf<T>の状態がSuccessなら処理は成功しています。(キーに対応する値が無い場合も成功になります。その場合、値はnilになります。 ) FailableOf<T>の状態がFailureの場合は、何らかの理由によりキーチェーンのアクセスに失敗しました。 このとき、errorオブジェクトにはエラーの詳細がNSErrorオブジェクトとして格納されます。 値は必ずnilになります。

実際の使い方は下記のようになります。

まず、getStringOrError(またはgetDataOrError)メソッドを呼び出します。

let failable = keychain.getStringOrError("kishikawakatsumi")

failableオブジェクトはSuccessまたはFailureのいずれかの状態を取ります。 Successならばvalueプロパティから実際の値が取得できます。(キーに対応する値が無い場合はnilFailureならばerrorプロパティにNSErrorオブジェクトが格納されています。(valueプロパティは必ずnil

switch failable {
case .Success:
  println("token: \(failable.value)")
case .Failure:
  println("error: \(failable.error)")
}

エラー処理についてはかなり悩んだのですが、Swiftにおいて同期処理で正常時には別の戻り値を返す必要があるという場合、Eitherを返すというのがたぶん良い方法だと思います。

非同期処理の場合はObjective-Cと同様に、コールバックのクロージャを分ける(onSuccessとonFailureなど)なり、エラーをクロージャの引数で渡すなどするのが良いかと思います。

単純に戻り値とエラーをタプルで返すという手もありますが、これはあまり使い勝手が良くなかったのでやめたほうが良いと思います。 ただし、下記にあるように現在のSwiftのコンパイラでは単にタプルを返すのに比べて多くのメモリが必要になる、という話もありますので注意が必要なケースもあります。

削除

removeメソッドを使うか、Subscriptingでnilを代入することで削除になります。 エラー処理はsetの時と同様です。

keychain["kishikawakatsumi"] = nil
keychain.remove("kishikawakatsumi")

設定を変えて保存する

アクセシビリティを変更したり、iCloudの同期を指定したりなど、項目によって設定を変えることができます。

下記はフォアグラウンド動作時のみ読み出せるようにして保存しています。 (この項目はバックグラウンド動作時は読み出せません。)

keychain
    .accessibility(.WhenUnlocked)
    .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

下記はiCloudによる同期を有効にする例です。

keychain
    .synchronizable(true)
    .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

メソッドチェーンによる設定の変更は、その時だけ一時的に有効になります。 共通の設定として使用する場合は、チェーンせずに変数に保管して使い回すようにします。

let keychain = Keychain(service: "com.example.github-token")
    .synchronizable(true)

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
keychain["hirohamada"] = "..."

Touch IDまたはパスコードで値を保護する(iOS 8〜)

iOS 8からTouch IDを使うAPIが追加されました。 同時にKeychainのAPIにもTouch IDのサポートが追加され、値を読み出すときと更新するときにTouch ID(またはパスコード)による認証を必要とすることができるようになりました。

KeychainAccessでは下記のようにaccessibilityの指定に加えてauthenticationPolicyを指定することで、その値はTouch IDによる保護が有効になります。 (authenticationPolicyで指定できるのは今の所UserPresenceだけです)

認証を必要とする可能性のある処理はバックグラウンドスレッドで実行する必要があります。 Touch ID(またはパスコード)による認証が必要な値にアクセスすると、システムが自動的にTouch IDまたはパスコードによる認証画面を表示します。 このとき、メソッドの実行はブロックされているので、認証画面を出そうとするけど、メインスレッドは止まっているという状態になってアプリケーション全体が止まってしまいます。 そのため、下記のようにバックグラウンドスレッドから実行するようにします。 (値の追加は認証を必要としませんが、すでに値が存在してそれが保護された値の場合、追加ではなく更新になるので認証が必要になります。つまり、削除以外はバックグラウンドスレッドから実行するようにしておくのが安全です。)

let keychain = Keychain(service: "com.example.github-token")

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    let error = keychain
        .accessibility(.WhenPasscodeSetThisDeviceOnly, authenticationPolicy: .UserPresence)
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

    if error != nil {
        // Error handling if needed...
    }
}

保護された値の取得は普通の値を取得するときと同様です。 ただし、認証を必要とするのでバックグラウンドスレッドから実行する必要があります。

let keychain = Keychain(service: "com.example.github-token")

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    let failable = keychain
        .authenticationPrompt("Authenticate to login to server")
        .getStringOrError("kishikawakatsumi")

    if failable.successed {
        println("value: \(failable.value)")
    } else {
        println("error: \(failable.error?.localizedDescription)")
        // Error handling if needed...
    }
}

削除については認証を必要としないので普通の値のときとまったく同様の書き方になります。

let keychain = Keychain(service: "com.example.github-token")

let error = keychain.remove("kishikawakatsumi")

if error != nil {
    println("error: \(error?.localizedDescription)")
    // Error handling if needed...
}

Swiftについて

Keychain APIはCのインタフェース(しかも使いにくい)しか用意されていなくて、Objective-Cでもラッパーが必要になるので、Swiftで書いたらまったくSwiftっぽくならなくて大変なだけじゃないのと思ってましたが、意外にもObjective-Cで書くよりも簡単に書けました。

例えば値を読み出すコードはObjective-Cだと下記のようになります。

NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[query setObject:service forKey:(__bridge id)kSecAttrService];
[query setObject:key forKey:(__bridge id)kSecAttrGeneric];
[query setObject:key forKey:(__bridge id)kSecAttrAccount];

CFTypeRef data = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &data);
if (status == errSecSuccess) {
    NSData *ret = [NSData dataWithData:(__bridge NSData *)data];
    if (data) {
        CFRelease(data);
    }
    return ret;
}

Swiftで書いた場合は下記のようになります。

var query = [String: AnyObject]()

query[kSecClass] = kSecClassGenericPassword
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData] = kCFBooleanTrue
query[kSecAttrService] = service
query[kSecAttrAccount] = key

var result: AnyObject?
var status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(query, UnsafeMutablePointer($0)) }

switch status {
case errSecSuccess:
    if let data = result as NSData? {
        return data
    }
default: ()
}

変なのはSecItemCopyMatchingでポインタのポインタを引数に与えてるところだけで、それ以外は非常にスッキリと書けています。 SwiftのDictionaryNSDictionaryが透過的に扱えて、NSDictionaryCFDictionaryRefがtoll-freeブリッジということでこのように簡単に書けるのですが、これは私にとっては意外でした。

Optionalについても、このライブラリではUIKitはまったく使ってないので特に困ることはありませんでした。 サンプルコードで少しUIを書きましたが、きちんと考えればOptionalはCocoaと一緒に使っても、パズルを組み立てるような気持ち良さが感じられると思います。

そんな感じで、基本的にはやはりObjective-Cに比べると言語自体の表現力が格段に高いので、少ない記述でキレイに書くことができます。 必然的に外部に公開するインタフェースも使いやすいものが書きやすいと思います。

ただし、現状ではSwiftにはリフレクションの仕組みがほぼサポートされていないので、メタプログラミングによって簡単に使えるAPIを提供する、ということはできません。

また、コンパイラはまだまだ未完成なようで、このライブラリを書いてる間に2つほどコンパイラがクラッシュするコード[1][2]を見つけましたし、本来なら型推論で書かなくて済むはず、というコードが実際は型やパラメータを明示する必要があったり、などという動作が散見されました。

また、コードの書き方によってはリリースビルド(コンパイラの最適化が働く)のときだけキーチェーンの値が取れないという問題もあったので、SwiftがObjective-C並みに安定するまでにはまだしばらくかかりそうです。

しかし、やはり言語による強力な表現のサポートは魅力的で、実際に少ない記述で美しく書けるので、使えるところでは積極的に使っていって、フィードバックをどんどんすべきかと思います。

Swiftの言語仕様はObjective-Cに比べるとそれなりに巨大ですが、まったく新しく設計された言語ということで一貫性もあるし、作りながら1週間もやればだいたいなんとかなるんじゃないでしょうか。

ただ個人的にはObjective-C 3.0としてGenericsと賢いenumとstructを入れる、、、とかから始めたら良かったんじゃないのと思ってたりします。