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を使用していました。

```objc
#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でもインポートすることで使用でき、以下の通りです。

```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

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

```swift
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`のプロパティを変更する可能性があるとのこと（`Data`の`description`の挙動が変わったこともありました）で、値のチェックには`description`を使用せずに`map`を使用しています。

### sha256とmd5の実装

```swift
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などで予め作成してその値をハードコードして確認をしました。

```swift
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から作成することが多いようです。
対象鍵方式では、共通の鍵を使用することで、暗号化、復号が可能になります。

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

### ChaCha20-Poly1305方式

```swift
/// 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)
    }
}
```

### テストコード

```swift
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方式

```swift
/// 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)
    }
}
```

### テストコード

```swift
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で作成したデジタル署名とその検証

```swift
// 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)
    }
}
```

### テストコード

```swift
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のことは知らなかったのですが、互いの秘密鍵と公開鍵から共通鍵を作成するという方法で、現在はあまり良いアイデアは思い浮かばないですが、引き出しとして知ることができて良かったです。

```swift
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)
    }
}
```

### テストコード

```swift
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のみならず勉強会が盛んで、互いに勉強する環境を作成しようとしていっています。

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

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

- https://developer.apple.com/documentation/cryptokit
- https://medium.com/swlh/common-cryptographic-operations-in-swift-with-cryptokit-b30a4becc895
- https://schiavo.me/2019/cryptokit-operations/
- https://www.andyibanez.com/posts/common-cryptographic-operations-with-cryptokit/