20201127のiOSに関する記事は4件です。

Firebase Authenticationを使ってiOSアプリのログイン機能を実装する

最近、iOSアプリにFirebase Authentication を使ってSMS認証を実装する機会がありました。
実際に利用してみた感想としては、ログイン機能新規会員登録 などの複雑を要する機能を比較的簡単に品質の高く実装できるのでおすすめ出来ると感じました。

前提

 

参考となる資料

Firebaseはドキュメントが充実しており、環境構築に関しては公式のドキュメントの手続きに沿えば基本的に問題ないと感じました。

Firebaseへ電話番号の送信を行う

[注意点] 日本の電話番号コード +81 を電話番号の先頭に付与する必要があるので、注意してください。

func sendPhoneNumber(phoneNumber: String) {

    let num = "+81" + phoneNumber
    // Firebaseへ電話番号を送信
    PhoneAuthProvider.provider().verifyPhoneNumber(num, uiDelegate: nil) { [weak self] verificationID, error in
        if let error = error { // Errorの場合はこちらへ }
        if let id = id {
            // 取得したauthVerificationIDをUserDefaultsへ保存
            UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
            // SMS画面へ遷移させる
        }
    }
}

verificationID を永続領域に保存するメリットについては、Firebaseの公式ドキュメントに記載がある通りです。

確認 ID を保存し、アプリの読み込み時に復元します。これにより、ユーザーがログインフローを完了する前にアプリが終了した場合でも(たとえば SMS アプリへの切り替え時など)、有効な確認 ID を残すことができます。

確認コードを使ってユーザーをログインさせる

以下の関数を使ってユーザの クレデンシャル を発行します。
クレデンシャル とは、ユーザのIDやPWなどのユーザ情報におけるパラメータの総称です。
クレデンシャルを生成するための関数の第一引数には電話番号を送信する際に永続領域に保存した verificationID を渡し、第二引数にはユーザが実際に入力した確認コードを渡します。

コールバックでログイン結果が返されるので、返ってきた結果によってエラー文言を出し分けたり、インジケータを表示してユーザに通信状態を伝えるとより良いと思いました。

// ユーザから入力された確認コードで
func sendSMSAuthCode(code: String) {
    // クレデンシャルの発行
    let id = userDefault.object(forKey: "authVerificationID") as? String ?? ""
    let credential = PhoneAuthProvider.provider().credential(withVerificationID: id, verificationCode: code)

    Auth.auth().signIn(with: credential) { [weak self] authResult, error in
        if let error = error {
            guard let errorCode = AuthErrorCode(rawValue: error._code) else { return }
            switch errorCode {
            case .invalidVerificationCode: print("認証コードが正しくありません")
            default: print("エラーが起きました。しばらくしてから再度お試しください。")
            }
        }
        if let _ = authResult {
            let currentUser = Auth.auth().currentUser
            currentUser?.getIDTokenForcingRefresh(true) { idToken, _ in
                guard let _ = idToken else {
                    print("トークンの取得に失敗しました")
                    return
                }
                // LOGIN SUCCESS!!!!
            }
        }
    }
}

以上で、ログイン機能は完了で??

アプリ初回起動時にログイン状態にあるかどうかを取得したい

ログイン機能は実装できましたが、アプリ起動時にユーザにログインフローを毎回踏んで貰うのは現実的ではありません。
Firebaseは特にユーザに操作をさせず、ログイン済みかどうかのステータスを取得する事ができます。したがって、AppDelegateやユーザ情報を取得する処理の中で以下の処理を実行するとログイン済みの場合はtokenが取得出来ます。
私はとりあえず、 tokenの取得成功 サインインが必要 エラー のステータスを用意してみました。

enum FirebaseTokenCallbackResult { case success(token: String), needSignIn, error(error: Error?) }

func fetchFirebaseToken() -> Observable<FirebaseTokenCallbackResult> {
    return Observable.create { observer in
        guard let currentUser = Auth.auth().currentUser else {
            // 自分自身のアカウントが取得できなかった場合は、新規会員登録のフローを踏む
            observer.onNext(.needSignIn)
            return Disposables.create()
        }
        currentUser.getIDToken(completion: { [weak self] idToken, error in
           if let token = idToken {
                self?.firebaseToken = token
                return observer.onNext(.success(token: token))
            }
            return observer.onNext(.error(error: error))
        })
        return Disposables.create()
    }
}

注意点

AppDelegateで以下の処理を呼び出して初期化すると思いますが、以下の処理が実行されるよりも先にAuthなどの処理を呼び出してしまうとクラッシュする可能性があります。そのため、以下の処理が実行されるよりも先に currentUser などを取得しないよう注意が必要です。

FirebaseApp.configure()

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サーバーサイドエンジニアが知っておくべきiOSアプリのデプロイとかテスト周りの知識をメモる

他にもいろいろ方法あるかもしれない。

firebase app distribution

  • sandboxアプリやqaアプリなどのベータ版アプリのダウンロードはこれ経由で行う

test flight

  • test flightが登録されたアプリを触って試すことができる
  • (ややこしい登録なしで)課金も試すことができる
  • testerとして招待してもらうことが必要
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モーダル遷移

ストーリーボードによるモーダル遷移から、前の画面に戻る方法

self?.dismiss(animated: true, completion: nil)

ストーリーボードによるモーダル遷移から、2つ前の画面に戻る方法

self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)```
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 上で巨大なファイルを復号する方法

エキサイトで刺身にたんぽぽを添える仕事をしているエンジニアの坂田です。
cocorus と言うアプリの iOS版 を開発したりしてます。

突然ですが 1GB 超の巨大なファイルの暗号化/復号を iOS (iPhone 実機)上で行おうとして OS 側からアプリを落とされる経験をされたことはありますか?
自分はあります。
当記事では巨大なファイルの復号を iOS 上で行うための方法を紹介します。

想定

  • マシン
    iPhone (iOS 14等)
  • 言語
    Swift 5
  • 想定暗号
    aes-128-cbc

一般的な復号コード

おそらくセオリー通り実装すると下記のようなコードになるかと思います。

import CommonCrypto

class CommonCryptoUtility {
    /// 中略

    /// 復号処理
    ///
    /// - Parameters:
    ///   - encryptedData: 暗号化されたData型リソース
    ///   - keyData: 暗号キーバイナリ
    ///   - initializationVectorData: 初期化ベクターバイナリ
    ///   - isPadding: PKCS#7 パディングの有無
    /// - Returns: 復号されたData型リソース
    /// - Throws: CommonCryptoUtilityError のいずれか
    func decrypt(
        encryptedData: Data,
        keyData: Data,
        initializationVectorData: Data,
        isPadding: Bool = true
    ) throws -> Data {
        // 復号化後のメモリーサイズ
        let decryptBufferLength: Int = encryptedData.count
        // 復号化後のメモリを確保
        var decryptBufferData = Data(count: decryptBufferLength)
        // デコード処理バイト数
        var executeBytesCount: size_t = 0

        // デコード
        let options: UInt32 = isPadding ? CCOptions(kCCOptionPKCS7Padding) : 0
        do {
            try keyData.withUnsafeBytes { (keyBytes: UnsafeRawBufferPointer) in
                try initializationVectorData.withUnsafeBytes { (ivBytes: UnsafeRawBufferPointer) in
                    try encryptedData.withUnsafeBytes { (encryptedDataPointer: UnsafeRawBufferPointer) in
                        try decryptBufferData.withUnsafeMutableBytes { decryptBufferDataPointer in
                            guard let keyBytesBaseAddress = keyBytes.baseAddress,
                                let ivBytesBaseAddress = ivBytes.baseAddress,
                                let encryptedDataBytesBaseAddress = encryptedDataPointer.baseAddress,
                                let decryptBufferDataBytesBaseAddress = decryptBufferDataPointer.baseAddress
                            else {
                                throw CommonCryptoUtilityError.decryptFailedWhenConvertBaseAddress
                            }

                            let cryptStatus = CCCrypt(
                                CCOperation(kCCDecrypt),
                                CCAlgorithm(kCCAlgorithmAES),
                                options,
                                keyBytesBaseAddress,
                                keyData.count,
                                ivBytesBaseAddress,
                                encryptedDataBytesBaseAddress,
                                encryptedData.count,
                                decryptBufferDataBytesBaseAddress,
                                decryptBufferLength,
                                &executeBytesCount
                            )

                            guard cryptStatus == kCCSuccess else {
                                throw CommonCryptoUtilityError.decryptFailed
                            }
                        }
                    }
                }
            }
        } catch {
            throw CommonCryptoUtilityError.decryptFailedWhenConvertUnsafePointer
        }

        return decryptBufferData
    }

    // 中略
}

ただ、上記のような実装だと暗号化データをすべて渡すので、このままだとメモリを食いつぶして iOS にアプリを落とされてしまいます。
なので、この関数をラップして分割して復号したくなるのが心情です。
幸いなことに、AES 128 CBC と言う暗号モードは16バイト単位での暗号化を行っており、暗号ブロックの一つ前のブロックがそのまま今のブロックの初期化ベクターになるとのことです。
つまり、前の16バイトさえわかればどのブロックからでも複合できるということになります。

指定ブロックを取り出すための復号処理のラッパー

上記を踏まえてブロック毎に復号するように上記メソッドを呼び出すラッパーを実装すると以下のようになります。

import CommonCrypto

class CommonCryptoUtility {
    // 中略

    let blockSizeAES128: UInt64 = UInt64(kCCBlockSizeAES128)

    /// 指定された範囲を復号する
    /// - 2 GiB 以内のファイルのみ扱うものとする(32Bit)
    /// - 8 EiB 以内のファイルのみ扱うものとする(64Bit)
    /// - Parameters:
    ///   - sourceURL: ファイルURL
    ///   - keyData: 暗号キーデータ
    ///   - firstInitVectorData: 初期化ベクターデータ
    ///   - requiredSliceOffset: 切り出すオフセット
    ///   - sliceLength: 切り出すバイト数
    /// - Returns: 複合したバイナリデータ
    func decrypt(
        readFrom sourceURL: URL,
        keyData: Data,
        firstInitVectorData: Data,
        offset requiredSliceOffset: UInt64,
        sliceLength: Int
    ) throws -> Data {
        let fileHandler = try FileHandle(forReadingFrom: sourceURL)
        defer {
            fileHandler.closeFile()
        }
        guard let sourceFileSize = MediaManager.getFileSize(from: sourceURL) else {
            throw CommonCryptoUtilityError.destinationStreamOpenFailed
        }
        guard requiredSliceOffset + UInt64(sliceLength) <= sourceFileSize else {
            throw CommonCryptoUtilityError.requestSizeGreaterThanSource
        }
        guard 0 < sliceLength else {
            throw CommonCryptoUtilityError.lengthLessThanOne
        }

        let alignedOffset4decrypt: UInt64 = requiredSliceOffset / blockSizeAES128 * blockSizeAES128
        let heading: Int = Int(requiredSliceOffset - alignedOffset4decrypt)
        let totalDecryptBlockAmount: Int = (sliceLength + heading) / kCCBlockSizeAES128 + 1
        let totalDecryptBlockLength: Int = totalDecryptBlockAmount * kCCBlockSizeAES128
        let dropLength: Int = totalDecryptBlockLength - (sliceLength + heading)

        // iv設定
        let initVectorData: Data
        if alignedOffset4decrypt == 0 {
            initVectorData = firstInitVectorData
        } else {
            fileHandler.seek(toFileOffset: alignedOffset4decrypt - blockSizeAES128)
            initVectorData = fileHandler.readData(ofLength: kCCBlockSizeAES128)
        }
        fileHandler.seek(toFileOffset: UInt64(alignedOffset4decrypt))

        // 復号
        let encryptedChunkData = fileHandler.readData(ofLength: Int(totalDecryptBlockLength))
        let decryptedChunk: Data = try decrypt(
            encryptedData: encryptedChunkData,
            keyData: keyData,
            initializationVectorData: initVectorData,
            isPadding: alignedOffset4decrypt + UInt64(totalDecryptBlockLength) == sourceFileSize
        )

        // 要求されたバイト数を切り出し
        let extractData = decryptedChunk.subdata(in: heading..<(totalDecryptBlockLength - dropLength))

        return extractData
    }

    // 中略
}

これでどのオフセットからでもデータを切り出すことが出来るようになりました。

分割して復号しつつ保存する

最終目的である、暗号化されたファイルを読んで復号し、ファイルへ書き出すように上記メソッドを更にラップすると以下のようになります。

import CommonCrypto

class CommonCryptoUtility {
    // 中略

    // 最大処理チャンクサイズ(100MiB)
    let cryptChunkSize: Int = 104857600

    /// ファイルへ復号
    ///
    /// - 暗号化ファイルを読み出し、CommonCryptoUtility.chunkSize毎に復号処理をしてテンポラリファイルへ書き出す。
    /// - Parameters:
    ///   - sourceURL: 暗号化されたファイルのURL
    ///   - destinationURL: 保存先のURL
    ///   - keyData: 暗号キーバイナリ
    ///   - firstInitVectorData: 初期化ベクターバイナリ
    /// - Throws: CommonCryptoUtilityError のいずれか
    func decrypt(
        readFrom sourceURL: URL,
        storeTo destinationURL: URL,
        keyData: Data,
        firstInitVectorData: Data
    ) throws {
        do {
            // ファイルストリームを用意
            guard let destinationData = OutputStream(url: destinationURL, append: false),
                let _sourceFileSize = MediaManager.getFileSize(from: sourceURL)
            else {
                throw CommonCryptoUtilityError.destinationStreamOpenFailed
            }
            destinationData.open()
            defer {
                // ファイルストリームを閉じる
                destinationData.close()
            }
            let sourceFileSize = Int(_sourceFileSize)
            var totalSliceSize = 0
            while totalSliceSize < sourceFileSize {
                let currentChunkSize = sourceFileSize - totalSliceSize > cryptChunkSize ? cryptChunkSize : sourceFileSize - totalSliceSize
                let decryptedChunk: Data
                do {
                    // チャンク毎に復号
                    decryptedChunk = try decrypt(
                        readFrom: sourceURL,
                        keyData: keyData,
                        firstInitVectorData: firstInitVectorData,
                        offset: UInt64(totalSliceSize),
                        sliceLength: currentChunkSize
                    )
                } catch {
                    throw error
                }
                var decryptedBytes = [UInt8](decryptedChunk)
                // 保存先に追記
                let writeResult = destinationData.write(&decryptedBytes, maxLength: decryptedBytes.count)
                guard writeResult >= 0 else {
                    throw CommonCryptoUtilityError.failedToWriteToDestination
                }
                totalSliceSize += currentChunkSize
            }
        }
    }

    // 中略
}

これで実用的なメモリ使用量でファイルの復号が出来るようになりました。
しかしながら、複数の巨大な暗号化ファイルを連続して復号すると、mach_task_basic_info() などで正しくメモリを開放しているように見えても、Instruments 上ではメモリ使用量が増え続けました。
どうやら iOS 上(.app コンテナを実行する vm 上?)ではまだ開放されていないようで、それらが開放されるまで若干の時差があるようです。
なので、連続して複合処理をする場合は数秒のディレイを入れながら復号すると成功するかと思います。

参考

https://github.com/backslash-f/aescryptable/blob/master/Sources/AESCryptable/AESCryptable.swift

最後に

良いのか悪いのか cocorus iOS版 の実コードから引っ張ってきています。
ま、アプリはコンテンツが主体なのでこの程度は問題ないでしょう。
それに、これを実装するため参考にしたコードを公開している方々へ、せめてものお返しとして辛かった部分をあえて公開することにしました。

ちなみに切り出しているので上記に書かれていない enum などあるので実際使う場合は適宜置き換えるなり定義するなりしないと動きません。
コードも回りっくどいのは試行錯誤の跡だと思って大目に見てください。
(改善案はお待ちしてます!)

あ、そうそう。
エキサイト株式会社ではエンジニアを随時募集しているようです。
気軽な気持ちで応募してみても良いのではないでしょうか?
https://www.wantedly.com/companies/excite

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む