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

Swift 小技 -- Keychainアクセス

ググるとKeychain関連の話は山ほど出てくる。中でも良くまとまっているのはこの記事。

Cocoaの日々: [iOS] Keychain Services とは

原理説明的にも、手順書としても詳細まで非常に良くまとまっているので個人的にエバノで管理しているくらいだし、実際拙作アプリもこれを参考に作った。ObjCなら何ら問題はない。

 

ただ、swiftでは一工夫いるのねというのが、今日の備忘。

厳密にはKeychainそのもではなく、UnsafePointerとかCOpaquePointerが主題。

 ググり倒して、最初に書いたkeychainのload関数はこんな感じ

    class func load(key: String) -> NSData? {

        let query = [

            kSecClass as String       : kSecClassGenericPassword,

            kSecAttrAccount as String : key,

            kSecReturnData as String  : kCFBooleanTrue,

            kSecMatchLimit as String  : kSecMatchLimitOne ]

        var dataTypeRef :AnyObject?

        let status: OSStatus = SecItemCopyMatching(query, &dataTypeRef)

        

        if status == noErr {

            let p = UnsafeMutablePointer<CFTypeRef>.alloc(sizeof(CFTypeRef))

            return NSData(bytes: p, length: sizeofValue(p))

        } else {

            return nil

        }

    }

 サイトによっては、UnsafeMutablePointer<CFType>ではなく、Unmanaged<AnyObject>になっている。

ただ、 Xcode ⌘ + クリックでsecurity内、SecItemCopyMatchingのswift宣言を見ると、

public func SecItemCopyMatching(query: CFDictionary, _ result: UnsafeMutablePointer<AnyObject?>) -> OSStatus

 となっている。

ありゃ?見た記事が古かったか。

と思って、UnsafeMutablePointerからallocでゴリゴリ取得しているのが、最初のテストコード。

これは、もちろんコンパイル通るし、なんかうまく動いてる風の振る舞いもする。

よく言われる、Keychain 常にnilを返す問題も、コンパイルオプションで最適化をNoneにすれば問題はなさそう。。。だが、なぜか初回起動のみ、必ずnil返しというバグに見舞われた。

 

フロー上は初回起動時には必ずNSUserDefaultのextensionに保存した設定値と、UserDefaultの顔して、実はKeychainという値(パスワードとか)もチェックしてセットアップするのだけど、全くもってうまくいかない。

途中でパスワード書き換えるテストは通る。なぜなら初回起動は考慮しないから。でも、デバッグ設定パネルからKeychain全消去して、起動しなおすと必ずパスワードだけ\0 * 8なんておかしな値を持って、期待しないルートを通って初期化されている。。。

 

もう、原因究明に数時間費やしてしまっていたので、手段としてObjCブリッジにするか、KeychainやめてAESでブロック暗号かけてやろうかと本気で思ったくらいのところで次の記事を見つけた。

うーん、つまりこうか!

    class func load(key: String) -> NSData? {

   ...

        var extractedData: AnyObject?

        let status: OSStatus = SecItemCopyMatching(query, &extractedData)  

        guard status == noErr else { return nil }

        let retrievedData: NSData? = extractedData as? NSData

        return retrievedData

    }

guardの使い方とダウンキャストは荒いけど、期待通りに動いている気はする。

そして、考えていたより超簡単。何よりswiftっぽい。

もう!OS、コンパイラ屋なら過去に一度でも決めた仕様はバックコンパチちゃんとして!

 

 ちなみにこう書くと、最適化オプション付けても問題無いみたいでした(今のところ)ので、思い切ってこうしてやりました!

f:id:setminami:20160802221313p:plain

 

 

ただし、まだ絶賛テスト中なので、これ見た人安易に信じないでください。あくまでAYORで採用のほどを。

あと、Mac App Store向けではないので、大前提としてcapabilityはkeychainのみでSANDBOX外してあります。そもそもsudoやらチップのレジスタやら複数スレッドからパラ(からの〜XPCでdaemon経由)で 一気にひっぱたいて廻るようなアプリなのでMAS向けアプリ制作の参考にはならんです。

ちなみに、Xcode 7.3.1での話。 

 

 

 

 

ふぅ、目的は何を置いても、動くことなので最終的に期待通り動けばそれでいいんだけど、いつもの調子で宣言追っかけたらLLVMのObjCブリッジ生煮え箇所にうっかり巻き込まれ、問題解決の妨げになったというお話でした。そういやPKDの小説に"知識は汝の身を滅ぼす"的なセンテンスがあったなぁ。聖書の引用らしいけど。

 

満足解の探索アルゴリズムは消去法に持ち込まれた瞬間、ただの手数合戦になるから嫌い!