iOS13未満でCommonCryptoで共通鍵暗号(AES-CBC)を使う

はじめに

iOS13未満もサポートするアプリで、共通鍵暗号(AES-CBC)を使うことがありました。
iOS13未満でも使えるライブラリCommon Cryptoを使った暗号化処理をSwiftで実装したのですが、その方法がググってもあまり出てこなかったので、ここにまとめます。

前提

  • Xcode 14.3
  • Deployment Target(IPHONEOS_DEPLOYMENT_TARGET)は11.2。開発しているアプリはiOS11.2以上をサポートしています。
  • Password Based Encryption(ユーザーが入力したパスワードとsaltを組み合わせて共通鍵を作る方法)ではありません。シンプルに乱数を生成し共通鍵として使っています。

技術選定

共通鍵で暗号化するには以下の方法があるのですが、CommonCryptoを採用しました。

  • CryptoKit: iOS標準。iOS13以降でのみ使えるため、今回は使えない。
  • Security Framework: iOS標準。公開鍵で暗号化するAPIはあるが、共通鍵で暗号化するAPIはdeprecatedになっている。代わりに使うべきAPIも見当たらない。
    • 以下の通り、共通鍵を生成し平文を暗号化して暗号文と共通鍵を一緒に公開鍵で暗号化して出力するAPIはあるようです。単純に共通鍵を渡して共通鍵暗号方式で暗号化するAPIはないようです。

      Instead, you call SecKeyCreateEncryptedData(_:_:_:_:) to create a symmetric key for you. This function creates the symmetric key, uses it to encrypt your data, and then encrypts the key itself with the public key that you provide. It then packages all of this data together and returns it to you.
      Creating Symmetric Key

  • Common Crypto: Appleのオープンソースライブラリ。Apple公式でも言及されている。
  • CryptoSwift: サードパーティ。
  • IDZSwiftCommonCrypto: サードパーティ。Common Cryptoのラッパー。Common CryptoをSwiftらしく使えるようにしてくれている。

実質 使えるのが、Common Crypto, CryptSwift, IDZSwiftCommonCryptoの3つでした。
今回は、Appleの公式ドキュメントで言及されていることと、iOSのSDKに標準で含まれている(CocoaPods等で依存関係を追加する必要がない)ためシンプルになることから、Common Cryptoを使ってみることにしました。

コード

Common CryptoのCCCrypt()メソッドをラップします。それに利用したいアルゴリズム(今回、AES)を渡して暗号化・復号します。

利用する処理

  • (1) 今回作成するクラスCommonCryptoWrapperのgenerateRandomBytes()を呼び出して、ランダムな文字列を作ります。これを初期化ベクトル(initialization vector, iv)として使います。
    • このgenerateRandomBytes()メソッドについては後で説明します。
    • AESのブロックサイズは128ビットなので、ivのサイズとしてkCCBlockSizeAES128を指定します。

      AESはSPN構造のブロック暗号である。ブロック長は128ビットであり、鍵長には128ビット・192ビット・256ビットの3種類が選択できる(鍵長が大きいほうが暗号強度が高い)。
      https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard

  • (2) 同様にランダムな文字列を作ります。これは、共通鍵(Symmetric Key)として使います。
    • AESの鍵長は、128, 192, 256bitから選べます。今回、256ビットにするので、kCCKeySizeAES256を指定します。
  • (3) 今回作成するクラスAES_CBC_PKCS7Padding.encrypt()を呼び出し暗号化します。keyとiv, 暗号化したいdata(Hello worldの文字列)を渡します。
    • (3') 暗号化されているので、元のdataとencryptedDataは同じになりません。
  • (4) 同様に、decrypt()を呼び出し復号します。keyとiv, 復号したいencryptedDataを渡します。
    • (4') 元のdataとdecryptDataは等しくなります。
    func testEncryptionAndDecryption() {
        let plainText = "hello world"
        let data = plainText.data(using: .utf8)!

        let iv = try! CommonCryptoWrapper.generateRandomBytes(count: kCCBlockSizeAES128) // (1)
        print("iv: \(iv.base64EncodedString())") // iv: gqV/Q0cL2S3lNs7qXJYgzA==
        let key = try! CommonCryptoWrapper.generateRandomBytes(count: kCCKeySizeAES256) // (2)
        print("kCCKeySizeAES256: \(kCCKeySizeAES256)") //  kCCKeySizeAES256: 32
        print("key: \(key.base64EncodedString())") // key: 1rMIkAEHuuv0njsv9FDPbB1yvEUYisQglqTzBAzN/oQ=

        let encryptedData = try! AES256_CBC_PKCS7Padding.encrypt(key: key, iv: iv, data: data) // (3)
        print("dataIsEqualToEncryptedData: \(data == encryptedData)"); // (3') false

        let decryptedData = try! AES256_CBC_PKCS7Padding.decrypt(key: key, iv: iv, data: encryptedData) // (4)
        print("dataIsEqualToDecryptedData: \(data == decryptedData)"); // (4') true
    }

AES-CBCの暗号化をする処理

  • (1) 暗号化する処理です。今回作成するCommonCryptoWrapperクラスのcrypt()メソッドを呼び出します。
    • 暗号化するので、kCCEncryptを渡します。
    • AESを使うので、kCCAlgorithmAESを渡します。
    • 暗号利用モードは、optionsにkCCOptionECBModeを指定しない限り、デフォルトでCBCモードになります。
    • CBCモードは、パディングが必要な暗号利用モードです。そのため、optionsにkCCOptionPKCS7Paddingを指定して、パディングを有効にします。
    • あと、key, iv, dataを指定します。
  • (2) 復号する処理です。(1)とほぼ同じですが、こちらはoperationにkCCDecryptを渡します。
import CommonCrypto

class AES256_CBC_PKCS7Padding {
    static func encrypt(key: Data, iv: Data, data: Data) throws -> Data {
        // (1)
        return try CommonCryptoWrapper.crypt(
            operation: kCCEncrypt,
            algorithm: kCCAlgorithmAES,
            options: kCCOptionPKCS7Padding,
            key: key,
            iv: iv,
            dataIn: data
        )
    }

    static func decrypt(key: Data, iv: Data, data: Data) throws -> Data {
        // (2)
        return try CommonCryptoWrapper.crypt(
            operation: kCCDecrypt,
            algorithm: kCCAlgorithmAES,
            options: kCCOptionPKCS7Padding,
            key: key,
            iv: iv,
            dataIn: data
        )
    }
}

Common Cryptoのラッパー

  • (1) Data型のkey(共通鍵, Symmetric key)を、バイト配列(UInt8の配列)にしています。iv, dataInについても同じようにしています。
  • (2) このバイト配列(UInt8の配列) keyBytesの参照を、Common CryptoのCCCrypt()メソッドに渡します。&は値を渡すのではなく参照を渡すということを表しています。iv, dataInについても同じようにしています。
    • CCCrypt()の定義は以下の通りです。&をつけることで、&keyBytesはkeyBytes(UInt8配列)を参照するUnsafeRawPointerとして扱われます。
    • public func CCCrypt(_ op: CCOperation, _ alg: CCAlgorithm, _ options: CCOptions, _ key: UnsafeRawPointer!, _ keyLength: Int, _ iv: UnsafeRawPointer!, _ dataIn: UnsafeRawPointer!, _ dataInLength: Int, _ dataOut: UnsafeMutableRawPointer!, _ dataOutAvailable: Int, _ dataOutMoved: UnsafeMutablePointer<Int>!) -> CCCryptorStatus
    • &をつけることで、参照を渡すことになるということ、pointerを渡すことになることについては、以下のページが参考になります。

      It works as an inout to make the variable an in-out parameter. In-out means in fact passing value by reference, not by value. And it requires not only to accept value by reference, by also to pass it by reference, so pass it with & - foo(&myVar) instead of just foo(myVar)
      ...
      It can also be used for calling C APIs from Swift with the traditional C meaning of the "address of" a variable. In Swift this takes the form of UnsafePointer.

      What does an ampersand (&) mean in the Swift language?
      I know about the ampersand as a bit operation but sometimes I see it in front of variable names. What does putting an & in front of variables do?
  • (3) dataOutBytesに入っているデータのうち、CCCrypt()処理を施した部分をdataOutMovedで切り取って、返します。
  • (4) generateRandomBytes()メソッドでは、CCRandomGenerateBytes()を呼び出して、ivやsymmetric keyに使う乱数を返します。CCRandomGenerateBytes()は、以下の通り、暗号論的にセキュアな乱数が返るようになっています。

    @abstract Return random bytes in a buffer allocated by the caller.
    @discussion The PRNG returns cryptographically strong random bits suitable for use as cryptographic keys, IVs, nonces etc.

    CCRandomGenerateBytes()のドキュメント。

  • (5) convertCCCryptorStatusToName()では、Int32型のstatusを、名前に変えています。エラーが発生した時に、ログやCrashlytics等に表示される時にInt32型だとパッとみてわからないです。わかりやすくするため名前に変換しています。必須の処理ではないです。
import CommonCrypto

class CommonCryptoWrapper {
    static func crypt(
        operation: Int,
        algorithm: Int,
        options: Int,
        key: Data,
        iv: Data,
        dataIn: Data
    ) throws -> Data {
        var keyBytes = [UInt8](key) // (1)
        var ivBytes = [UInt8](iv)
        var dataInBytes = [UInt8](dataIn)

        let maxPaddingSize = kCCBlockSizeAES128
        let dataOutSize = dataIn.count + maxPaddingSize
        var dataOutBytes = [UInt8](repeating: 0, count: dataOutSize)
        var dataOutMoved: Int = 0

        let statusInt32 = CCCrypt(
            CCOperation(operation),
            CCAlgorithm(algorithm),
            CCOptions(options),
            &keyBytes, // (2)
            keyBytes.count,
            &ivBytes,
            &dataInBytes,
            dataInBytes.count,
            &dataOutBytes,
            dataOutSize,
            &dataOutMoved
        )
        guard Int(statusInt32) == kCCSuccess else { throw MyError(message: "Failed to call CCCrypt(). status: \(statusInt32), status name: \(convertCCCryptorStatusToName(statusInt32: statusInt32))") }
        return Data(dataOutBytes.prefix(dataOutMoved)) // (3)
    }

    static func generateRandomBytes(count: Int) throws -> Data {
        var bytes = [UInt8](repeating: 0, count: count)
        let status = CCRandomGenerateBytes(&bytes, bytes.count) // (4)
        guard status == kCCSuccess else { throw MyError(message: "Failed to call CCRandomGenerateBytes(). status: \(status), status name: \(convertCCCryptorStatusToName(statusInt32: status))") }
        return Data(bytes)
    }

    // (5)
    private static func convertCCCryptorStatusToName(statusInt32: CCCryptorStatus) -> String {
        switch Int(statusInt32) {
        case kCCSuccess:
            return "kCCSuccess"
        case kCCParamError:
            return "kCCParamError"
        case kCCBufferTooSmall:
            return "kCCBufferTooSmall"
        case kCCMemoryFailure:
            return "kCCMemoryFailure"
        case kCCAlignmentError:
            return "kCCAlignmentError"
        case kCCDecodeError:
            return "kCCDecodeError"
        case kCCUnimplemented:
            return "kCCUnimplemented"
        case kCCInvalidKey:
            return "kCCInvalidKey"
        default:
            return "unexpected status"
        }
    }
}

補足

  • この記事は以下のページを参考にしています。いずれもCommon CryptoでAES-CBCを使って暗号化しています。
    • (1) SwiftでCCCryptでAES(256Bit)してみた話
    • (2) SHA256-Bridging-Header.h
    • (3) Pure Swift 5 CommonCrypto AES Encryption
    • メモ: (1)と(2)は、Password Based Encryptionをしています。ユーザーが入力したパスワードをsaltと組み合わせて共通鍵を作ります。(3)とこの記事では、単純に乱数を生成し共通鍵として使っています。
    • メモ2: (1)と(2)は、乱数の生成に、AppleのSecurity FrameworkのSecRandomCopyBytes()を使っています。一方、(3)とこの記事ではCommon CryptoのCCRandomGenerateBytes()を使っています。(1)と(2)がこのようにしている理由は不明です。(3)のように、Common Cryptoの乱数生成メソッドを使うのが自然だと思います。
    • メモ3: (1)~(3)では、CCCrypt()を呼び出すのにUnsafePointerを多用しています。一方、この記事では多少メモリを使いますがわかりやすさのためPointerを使わない方法で実装してみました。
  • 今回使ったAESやCBCなどの暗号利用モードについて、詳しく知りたい方は、以下の資料がおすすめです。
    • Wikipedia
    • 結城浩「暗号技術入門 第3版」
      • 共通鍵暗号、公開鍵暗号、認証、証明書など一連の暗号技術についてわかりやすく説明してくれます。新人時代の自分にすすめたい本です。また、今もときどき見返す本です。

最後に

この記事ではiOS13未満もサポートするiOSアプリで、共通鍵暗号を使う方法を紹介しました。
古めのアプリのメンテナンス等でこの記事が役に立つとうれしいです。

コメント

タイトルとURLをコピーしました