Help us understand the problem. What is going on with this article?

CryptoKitの調査、CryptoKit以前とCryptoKitでできることをサンプルコードとテストで説明

Qiita 初投稿になります。

ゆめみ Advent Calendar 2019の9日目の記事となります。

iOS13で紹介されているCryptoKitに関して調査しましたので、実際の使い所とコードとそのテストを見ながら共有したいと思います。CryptoKitは調べてもまだ日本語ではほとんど記事になっておらず、試しにどんなものか見てみました。

インデックス

  • CryptoKitの概要
  • CryptoKitでできること
  • CryiptoKit以前のsha256ハッシュ値生成
  • CryptoKitを使ったハッシュ値生成
  • CryptoKitを使った対象鍵暗号(Symmetric Encryption)
  • CryptoKitを使ったデジタル署名(Cryptographic Signature)
  • CryptoKitを使ったKey-Agreement-Protocol

CryptoKit の概要

CryptoKitはiOS13 から使用できる Appleの公式ライブラリです。ハッシュ値生成、暗号化、署名に関する操作を安全に、効率的に行うことができます。

一つネックなのは、CryptoKitは、iOS13以上を要求しており、2019年12月現在で、 iOS12以下を切って、 iOS13以上のみ対応するという案件はあまり存在していないため、実際のコードに導入するという機会はまだありませんが、来るべき iOS13以上対応アプリに備えて知っておいても良いですね。

https://developer.apple.com/documentation/cryptokit


CryptoKit でできること

概要にも書いたようにCryptoKitでできることは多岐に渡り、以下のことが実現できます。

  • ハッシュ値生成(Hashing Data) > 文字列やデータ、ファイルなどを sha128, sha256, sha384 などのハッシュ値に変換することができます。また、sha1やmd5などインセキュアなハッシュ値も生成が可能です。
  • 対象鍵暗号(Symmetric Encryption)による暗号化と復号 > 対象鍵をChaCha20-Poly1305方式やAES-GCM方式でを使用し、暗号化と復号を行うことができます。
  • デジタル署名 > Curve25519, P521, P384, P256 などの楕円曲線を使用し、デジタル署名を行い、また、検証をすることができます。
  • Key-Agreement-Protocol の実装 > 自分の秘密鍵と相手の公開鍵からShared Encryption Key を作り、それを使用することで、データの暗号化、復号をすことができます。

CryptoKit以前のsha256ハッシュ値生成

sha256でハッシュ値生成してほしいというニーズは結構あり、アプリのユーザが持つデータのユニーク性を担保するためにアプリ内で生成、保存したsha256文字列をサーバに送ることなどがありました。

CommonCrypto with Objective-C

Objective-Cでsha256を実現するためには、よくC言語のライブラリのCommonCryptoを使用していました。

#include <CommonCrypto/CommonCrypto.h>

NSString *str = @"Hello, Yumemi";
NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];

NSMutableData *ccHashed = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CC_SHA256(data.bytes, data.length, ccHashed.mutableBytes);
NSString *ccHashedString = [ccHashed base64EncodedStringWithOptions:0];
NSLog(@"CommonCrypto: %@", ccHashedString);

CommonCrypto with Swift

また、同ライブラリはSwiftでもインポートすることで使用でき、以下の通りです。

import CommonCrypto

let str = "Hello, Yumemi"
let data = Data(str.utf8)

// CommonCrypto Swift5 でちょっと書き方が Deprecated になったので修正済み
var ccHashed = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
    _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &ccHashed)
}
let ccHashedString = ccHashed.compactMap { String(format: "%02x", $0)}.joined()
print("CommonCrypto: " + ccHashedString)

CryptoSwift

ゆめみ内で私が関わった案件でも使用されていた、ライブラリで、現行一番使われていると思います。

import CryptoSwift

let str = "Hello, Yumemi"
let data = Data(str.utf8)

let csHashed = data.sha256()
let csHashedString = csHashed.compactMap { String(format: "%02x", $0)}.joined()
print("CryptoSwift: " + csHashedString)

CryptoKitを使ったハッシュ値生成

CryptoKit 以前は上で書いたような方法でsha256などのハッシュ値を使用していましたが、CryptoKitを使用すると以下のようになります。簡単なテストコードも付けました。hashの値を文字列化する際にdescriptionを使用することも可能ですが、Appleはdescriptionのプロパティを変更する可能性があるとのこと(Datadescriptionの挙動が変わったこともありました)で、値のチェックにはdescriptionを使用せずにmapを使用しています。

sha256とmd5の実装

import CryptoKit

// HashProtocol
protocol HashProtocol {
    func hash(str: String) -> String?
}

protocol Sha256HashProtocol: HashProtocol {
}

extension Sha256HashProtocol {
    func hash(str: String) -> String? {
        let data = Data(str.utf8)
        let hashed = SHA256.hash(data: data)

        return hashed.compactMap { String(format: "%02x", $0) }.joined()
    }
}

protocol Md5HashProtocol: HashProtocol {
}

extension Md5HashProtocol {
    func hash(str: String) -> String? {
        let data = Data(str.utf8)
        let hashed = Insecure.MD5.hash(data: data)

        return hashed.compactMap { String(format: "%02x", $0) }.joined()
    }
}

テストコード:

プロトコルのデフォルト実装として用意しておくと、テストも楽で良いですね。sha256やmd5の値などはPHPなどで予め作成してその値をハードコードして確認をしました。

func testSuccessCaseForSha256Hashing() {
    struct SUT: Sha256HashProtocol { }
    XCTAssertEqual("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", SUT().hash(str: "test"), "sha256 strings generated")
}

func testSuccessCaseForMd5Hashing() {
    struct SUT: Md5HashProtocol { }
    XCTAssertEqual("098f6bcd4621d373cade4e832627b4f6", SUT().hash(str: "test"), "md5 strings generated")
}

CryptoKitを使った対象鍵暗号(Symmetric Encryption)

テストコードにあるように、鍵は 256bits形式で作成することができますが、SharedSecretから作成することが多いようです。
対象鍵方式では、共通の鍵を使用することで、暗号化、復号が可能になります。

/// SymmetricEncryptionProtocol
protocol SymmetricEncryptionProtocol {
    var cryptoKey: SymmetricKey { get }
    func encrypt(str: String) -> Data?
    func decrypt(data: Data) -> String?
}

ChaCha20-Poly1305方式

/// ChaChaPolyEncryptionProtocol
protocol ChaChaPolyEncryptionProtocol: SymmetricEncryptionProtocol {
}

extension ChaChaPolyEncryptionProtocol {
    func encrypt(str: String) -> Data? {
        let data = Data(str.utf8)
        guard let sealedBox = try? ChaChaPoly.seal(data, using: cryptoKey) else { return nil }

        return sealedBox.combined
    }

    func decrypt(data: Data) -> String? {
        guard let sealedBox = try? ChaChaPoly.SealedBox(combined: data) else { return nil }
        guard let decryptedData = try? ChaChaPoly.open(sealedBox, using: cryptoKey) else { return nil }

        return String(data: decryptedData, encoding: .utf8)
    }
}

テストコード

func testSuccessCaseForChaChaPolyEncryption() {
    struct SUT: ChaChaPolyEncryptionProtocol {
        var cryptoKey: SymmetricKey {
            return SymmetricKey(data: Data("12345678901234567890123456789012".utf8))
        }
    }
    let sut = SUT()
    let signature = sut.encrypt(str: "HOGEHOGE")!
    XCTAssertEqual("HOGEHOGE", sut.decrypt(data: signature))
}

AES-GCM方式

/// AESGCMEncryptionProtocol
protocol AESGCMEncryptionProtocol: SymmetricEncryptionProtocol {
}

extension AESGCMEncryptionProtocol {
    func encrypt(str: String) -> Data? {
        let data = Data(str.utf8)
        guard let sealedBox = try? AES.GCM.seal(data, using: cryptoKey) else { return nil }

        return sealedBox.combined
    }

    func decrypt(data: Data) -> String? {
        guard let sealedBox = try? AES.GCM.SealedBox(combined: data) else { return nil }
        guard let decryptedData = try? AES.GCM.open(sealedBox, using: cryptoKey) else { return nil }

        return String(data: decryptedData, encoding: .utf8)
    }
}

テストコード

func testSuccessCaseForAESGCMEncryption() {
    struct SUT: AESGCMEncryptionProtocol {
        var cryptoKey: SymmetricKey {
            return SymmetricKey(data: Data("12345678901234567890123456789012".utf8))
        }
    }
    let sut = SUT()
    let signature = sut.encrypt(str: "HOGEHOGE")!
    XCTAssertEqual("HOGEHOGE", sut.decrypt(data: signature))
}

CryptoKitを使ったデジタル署名(Cryptographic Signature)

デジタル署名では、メッセージの送り主が秘密鍵で署名した内容の検証が可能です。

Curve25519で作成したデジタル署名とその検証

// CryptoSigningProtocol
struct CryptoSignature {
    var signature: Data
    var signedData: Data
}

protocol CryptoSigningProtocol {
    var rawPrivateKey: Data { get }
    func createKey() -> Data?
    func sign(str: String) -> CryptoSignature?
    func isValid(rawPublicKey: Data, signature: CryptoSignature) -> Bool
}

extension CryptoSigningProtocol {
    func createKey() -> Data? {
        guard let privateKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: rawPrivateKey) else { return nil }
        return privateKey.publicKey.rawRepresentation
    }

    func sign(str: String) -> CryptoSignature? {
        guard let data = str.data(using: .utf8),
        let privateKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: rawPrivateKey),
        let signature = try? privateKey.signature(for: data) else { return nil }

        return CryptoSignature(signature: signature, signedData: data)
    }

    func isValid(rawPublicKey: Data, signature: CryptoSignature) -> Bool {
        guard let signingPublicKey = try? Curve25519.Signing.PublicKey(rawRepresentation: rawPublicKey) else { return false }

        return signingPublicKey.isValidSignature(signature.signature, for: signature.signedData)
    }
}

テストコード

func testSuccessCaseForCryptoSigning() {
    struct SUT: CryptoSigningProtocol {
        var rawPrivateKey: Data {
            return Data(base64Encoded: "EDpGUyQuE0Xtjt3/j8KmxtBdaKQNP+7uTU3nJg7pzsg=")!
        }
    }
    let sut = SUT()
    let rawPublicKey = sut.createKey()!
    let signedSignature = sut.sign(str: "HOGEHOGE")!

    // 検証は公開鍵と受け取ったデータがあれば別人の環境で可能
    XCTAssertTrue(sut.isValid(rawPublicKey: rawPublicKey, signature: signedSignature))
}

CryptoKitを使ったKey-Agreement-Protocol

CryptoKitを調査するまでは、Key-Agreement-Protocolのことは知らなかったのですが、互いの秘密鍵と公開鍵から共通鍵を作成するという方法で、現在はあまり良いアイデアは思い浮かばないですが、引き出しとして知ることができて良かったです。

struct SealedMessage {
    var publicKey: Data
    var cipherText: Data
    var signature: Data
}

protocol KeyAgreementProtocol {
    var secretKey: String { get }
    func encrypt(_ data: Data, to theirEncryptionKey: Curve25519.KeyAgreement.PublicKey, signedBy ourSigningKey: Curve25519.Signing.PrivateKey) throws -> SealedMessage
    func decrypt(_ sealedMessage: SealedMessage,
                 using ourKeyEncryptionKey: Curve25519.KeyAgreement.PrivateKey,
                 from theirSigningKey: Curve25519.Signing.PublicKey) throws -> Data
}

extension KeyAgreementProtocol {
    func encrypt(_ data: Data, to theirEncryptionKey: Curve25519.KeyAgreement.PublicKey, signedBy ourSigningKey: Curve25519.Signing.PrivateKey) throws -> SealedMessage {

        let protocolSalt = secretKey.data(using: .utf8)!
        let ephemeralKey = Curve25519.KeyAgreement.PrivateKey()
        let ephemeralPublicKey = ephemeralKey.publicKey.rawRepresentation
        let sharedSecret = try ephemeralKey.sharedSecretFromKeyAgreement(with: theirEncryptionKey)
        let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self,
                                                                salt: protocolSalt,
                                                                sharedInfo: Data(),
                                                                outputByteCount: 32)
        let ciphertext = try ChaChaPoly.seal(data, using: symmetricKey).combined
        let signature = try ourSigningKey.signature(for: ciphertext + ephemeralPublicKey + theirEncryptionKey.rawRepresentation)
        return SealedMessage(publicKey: ephemeralPublicKey, cipherText: ciphertext, signature: signature)
    }

    func decrypt(_ sealedMessage: SealedMessage,
                 using ourKeyEncryptionKey: Curve25519.KeyAgreement.PrivateKey,
                 from theirSigningKey: Curve25519.Signing.PublicKey) throws -> Data {

        let protocolSalt = secretKey.data(using: .utf8)!
        let ephemeralKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: sealedMessage.publicKey)

        let sharedSecret = try ourKeyEncryptionKey.sharedSecretFromKeyAgreement(with: ephemeralKey)
        let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self,
                                                                salt: protocolSalt,
                                                                sharedInfo: Data(),
                                                                outputByteCount: 32)
        let sealedBox = try ChaChaPoly.SealedBox(combined: sealedMessage.cipherText)
        return try ChaChaPoly.open(sealedBox, using: symmetricKey)
    }
}

テストコード

func testSuccessCaseForKeyAgreement() {
    struct SUT: KeyAgreementProtocol {
        var secretKey: String {
            return "DO_YOU_WANT_TO_KNOW_MY_SECRET"
        }
    }

    /// create sender Sign Key
    let senderSigningKey = Curve25519.Signing.PrivateKey()
    let senderSigningPublicKey = senderSigningKey.publicKey

    /// create receiver Sign Key
    let receiverEncryptionKey = Curve25519.KeyAgreement.PrivateKey()
    let receiverEncryptionPublicKey = receiverEncryptionKey.publicKey

    let sut = SUT()

    let sealedMessage = try! sut.encrypt(Data("YES, I DO!".utf8), to: receiverEncryptionPublicKey, signedBy: senderSigningKey)

    let decryptedMessage = try? sut.decrypt(sealedMessage, using: receiverEncryptionKey, from: senderSigningPublicKey)
    XCTAssertEqual("YES, I DO!", String(data: decryptedMessage!, encoding: .utf8)!)
}

おわりに

今回、CryptoKitを調べるにあたって、protocol extensionでどう書くかとか、テストを書きやすいようにしようとか、考えながら書くことができました。また、ChaCha20-Poly1305やCurve25519、Key-Agreement-Protocolなど知らなかった方式を学ぶこともでき良かったです。

もう少し整理したら、Githubにあげて、リンクを貼っておきます。実際に動くテストコードがあると、使い方やテストの書きやすいコードとかがわかりやすくなるかな、と思っています。

CryptoKitを調べた内容は以上になります。この内容は一度、もっと荒削りに調べた際に、社内のLTで話させていただいています。ゆめみでは、iOSのみならず勉強会が盛んで、互いに勉強する環境を作成しようとしていっています。

ゆめみでは、一緒に働ける仲間を絶賛募集中です。


今回の記事を作成するにあたり、参考となったサイトは以下のとおりです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした