[swift4]SecurityフレームワークでRSA暗号を実装

#目的
swiftを使用したiOSアプリにおいて、**SecKeyEncrypt**など従来から存在するAPIで暗号化を行なっている記事は数多く見られたが、それらはAppleの公式Developerドキュメントでは **Legacy**という扱いになっている。

そこで、**Appleが提供しているSecurityフレームワークの中で、最新(2018/10/18時点)のAPI**を使用して暗号化を試してみる。
[Apple Developer Documentation(Security->Keys)](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys)

##やりたいことリスト
1. 鍵ペア生成
2. 秘密鍵をKeychainに保存
3. Keychainに保存された秘密鍵の読み出し
4. 公開鍵で暗号化
5. 秘密鍵で復号
6. ハッシュ化した文字列に電子署名を付与
7. 電子署名の検証

おまけ
1. 任意の文字列をハッシュ化
2. Dictionary<String, Any>型を、Data型に変換

#動作環境
- swift 4.2
- xcode 10.0

#前提
- 鍵ペアは **RSA 2048-bit** を使用
- ハッシュ関数は **SHA-512** を使用

#実装

##鍵ペア生成
参考：[Generating New Cryptographic Keys](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/generating_new_cryptographic_keys)

```swift
//秘密鍵につけるタグ(keychainから秘密鍵を取得する際に使用)を指定
let tagForPrivateKey = "com.example.keys.mykey".data(using: .utf8)!

//秘密鍵のattributeを指定
let attributes: [String: Any] = [
    kSecAttrKeyType as String: kSecAttrKeyTypeRSA, // 暗号鍵のタイプ（ここではRSAを指定）
    kSecAttrKeySizeInBits as String: 2048,         // 暗号鍵のビット数（2048-bitを指定）
    kSecPrivateKeyAttrs as String: [
        kSecAttrIsPermanent as String: true,       // keychainに保存するか否か
        kSecAttrApplicationTag as String: tagForPrivateKey]　　// タグを指定     
]

var error: Unmanaged<CFError>?

//秘密鍵の作成
guard let generatedPrivateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue() as Error
}

//公開鍵の作成
let generatedPublicKey = SecKeyCopyPublicKey(generatedPrivateKey)

print("privatekey: \(generatedPrivateKey)")
print("publickey : \(generatedPublicKey! as Any)")
```

実行結果

```
privatekey: <SecKeyRef algorithm id: 1, key type: RSAPrivateKey, version: 4, block size: 2048 bits, addr: 0x600001778b60>

publickey : <SecKeyRef algorithm id: 1, key type: RSAPublicKey, version: 4, block size: 2048 bits, exponent: {hex: 10001, decimal: 65537}, modulus: 8B5D3EDDA15CCC311C41A80DCB6FE5689DB025BBAD69D0105C056E62860FEF999BD0EF0F51202C3EC4870E8A72A73CD664F19493A47BCDC54D0C66271D62BD019138EDA3ED64C0A9C08A9B1B92F54DC18CFDB0062D277C5B7D9EBC2470B036DF19468CF46BF3D861392849129D71318D56DB99F16F283073EC0FC8C5FDE62623498AD7F2202D4791FB940F4BAD5885D6FBB7288BB3761AE9400B44A3B572169D68904446C2B4369DBD17220628FFECF2EC9692AC790A5CF3B21F5C28B7AE9D9A6F2715009D891B011F5572640B995CA366B03E4596646C4BE37FB07555E6D18A8AEE6470C8513F72B679209CF2D093D457B48CD3D7AB319DEBD53210EBB08DD9, addr: 0x6000017724a0>
```
暗号鍵（privateKey/publicKey）の作成に成功。

また、暗号鍵の作成の際に、指定できるパラメータ一覧は以下から確認可能。
[Key Generation Attributes](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys)


##秘密鍵をKeychainに保存
参考：[Storing Keys in the Keychain](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain)
公開鍵もKeychainに保存することはできるが、秘密鍵があればいつでも作成できるため、Apple側も秘密鍵のみ保存することを推奨している。

なお、上で作成した秘密鍵（privateKey）をKeychainに格納するには、attributesにkSecAttrIsPermanentパラメータを追加し、trueを指定するとOK。
つまり、特別やることはない。

```swift
let attributes: [String: Any] = [
    kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
    kSecAttrKeySizeInBits as String: 2048,              
    kSecPrivateKeyAttrs as String: [                   
        kSecAttrIsPermanent as String: true,      <- ここ      
        kSecAttrApplicationTag as String: tag] 
]
```

##Keychainに保存された秘密鍵の読み出し
参考：[Storing Keys in the Keychain](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_keychain)
ここでのポイントは、秘密鍵を作成した際につけたタグを指定してあげること。

```swift
//取得する秘密鍵のタグを指定
let tagForPrivateKey = "com.example.keys.mykey".data(using: .utf8)! //秘密鍵の作成の際につけたタグを指定

//秘密鍵を検索するクエリを作成
let getquery: [String: Any] = [
    kSecClass as String: kSecClassKey, //取得する情報の種類(暗号鍵、証明書、パスワードなど)
    kSecAttrApplicationTag as String: tagForPrivateKey, //秘密鍵についているタグ
    kSecAttrKeyType as String: kSecAttrKeyTypeRSA, //秘密鍵のタイプ
    kSecReturnRef as String: true //秘密鍵の参照情報を取得するか否か（この参照情報を使用して公開鍵の作成などを行う）
]

var item: CFTypeRef?
let status = SecItemCopyMatching(getquery as CFDictionary, &item)
//秘密鍵が取得できたかどうか検証
guard status == errSecSuccess else {
    throw error!.takeRetainedValue() as Error
}

//取得した秘密鍵を、変数retrievedPrivateKeyに代入
let retrievedPrivateKey = item as! SecKey
```

##公開鍵で暗号化
参考：[Using Keys for Encryption](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/using_keys_for_encryption)
ここでのポイントは、以下。
1. 暗号化する平文は、公開鍵のブロック長より**130バイト以上小さく**ないといけない
2. 暗号化する平文は、**Data型**でないといけない

また、公開鍵を受け渡す先が、PEM形式を希望している場合、base64で文字列にエンコードする必要があるため、その処理も記載した。


```swift

//暗号化に使用するアルゴリズムを定義
let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA512

//指定した公開鍵が、上記のアルゴリズムをサポートしているか検証
guard SecKeyIsAlgorithmSupported(publickey, .encrypt, algorithm) else {
    throw error!.takeRetainedValue() as Error
}

//暗号化する文字列を用意
let plainText = "Hello,Wolrd"

//平文と公開鍵のblock長を比較
guard (plainText!.count < (SecKeyGetBlockSize(generatedPublicKey!)-130)) else {
    throw error!.takeRetainedValue() as Error
}

//平文をData型に変換
let plainTextData = plainText?.data(using: .utf8)!

//平文の暗号化
guard let cipherText = SecKeyCreateEncryptedData(
    publickey,
    algorithm,
    plainTextData! as CFData,
    &error
    ) as Data? else {
        throw error!.takeRetainedValue() as Error
}

//暗号化した平文をbase64でデコードし、string型に変換
let cipherTextString = cipherText.base64EncodedString()
print("cipherTextString: \(cipherTextString)")
```

実行結果

```
cipherTextString : eYbZdTV2euitOG8YgKRNEDFB5JGNGEJpgC2DB++J39wOz0U9WfQikjEziP/EuutvOzcbpjQnKnIBTYY4dXO0Drx99hMGanXoouvilmXik86Sa5WV5DAq4mReDtehdgMKl2DCS/y7bek7HNPCvTcdodj4sM9kpyqipjl1vmYF7TD7cRDZeaV4njAGucPkSC1NX+9IV9hV6QkaPqttLGLSR3rCfY63UqXQcy7lHrwHigWLgdDDElNu/g3r22ncWLxIGN/nHKjhRUWSuyPrMpRDFFl7wMyBdhVPjY5KA+nHl+fQY9u+Wop8jnk1k7UP1NAIcOKK9chz1yawiPgZxn06zQ==
```


##秘密鍵で復号
ここでは、base64でエンコードされた暗号文をデコードし、data型に戻した上で復号しています。
余談ですが、暗号化の対義語って復号化ではないんですね。（以下記事より）
[「暗号化」の対語が「復号化」ではない理由
](http://kei-sakaki.jp/2013/08/09/encryption-and-decryption/)

```swift

//string型の暗号文をbase64でデコードし、data型に変換
let decryptedCipherText = Data(base64Encoded: cipherTextString, options: [])

//復号に使用するアルゴリズムを定義（暗号化に使用したアルゴリズムと同様）
let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA512

//指定した秘密鍵が、上記のアルゴリズムをサポートしているか検証
guard SecKeyIsAlgorithmSupported(privateKey, .decrypt, algorithm) else {
    throw error!.takeRetainedValue() as Error
}

//暗号文と秘密鍵のblock長を比較
guard cipherText.count == SecKeyGetBlockSize(privateKey) else {
    throw error!.takeRetainedValue() as Error
}

//data型に変換した暗号文を復号
guard let clearText = SecKeyCreateDecryptedData(
    generatedPrivateKey,
    algorithm,
    decryptedCipherText! as CFData,
    &error
    ) as Data? else {
        throw error!.takeRetainedValue() as Error
}

//復号したデータを、string型に変換
let clearTextString: String? = String(data: clearText, encoding: .utf8)
print("clearTextString:\(clearTextString!)")
```

実行結果

```
clearTextString: Hello,Wolrd
```

##ハッシュ化した文字列に電子署名を付与
参考：[Signing and Verifying](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/signing_and_verifying)
初めは、ハッシュ化と電子署名の作成を別々に実装しようと考えていたが、Appleが提供するAPIでは、なんと1つの関数でハッシュ化と電子署名の作成を同時に行ってくれる。非常に便利。

なお、ハッシュ化せず、署名だけ作成することも可能。

```swift
//暗号化アルゴリズムの指定
let algorithm: SecKeyAlgorithm = .rsaSignatureMessagePKCS1v15SHA512 //ハッシュ関数にはSHA-512を使用

//秘密鍵が指定したアルゴリズムに対応しているか検証
guard SecKeyIsAlgorithmSupported(privateKey!, .sign, algorithm) else {
    throw error!.takeRetainedValue() as Error
}

//電子署名作成（データのハッシュ化&秘密鍵で暗号化）
guard let signature = SecKeyCreateSignature(
    privateKey!,
    algorithm,
    data! as CFData,
    &error) as Data? else {
        throw error!.takeRetainedValue() as Error
}

//署名したハッシュ値をbase64でエンコードし、string型に変換
let signatureString = signature.base64EncodedString()
print("signatureString: \(signatureString)")
```

実行結果

```
signatureString: 7Ydo2PiNSeitOG8YgKRNEDFB5JGNGEJpgC2DB++J39wOz0U9WfQikjEziP/EuutvOzcbpjQnKnIBTYY4dXO0Drx99hMGanXoouvilmXik86Sa5WV5DAq4mReDtehdgMKl2DCS/y7bek7HNPCvTcdodj4sM9kpyqipjl1vmYF7TD7cRDZeaV4njAGucPkSC1NX+9IV9hV6QkaPqttLGLSR3rCfY63UqXQcy7lHrwHigWLgdDDElNu/g3r22ncWLxIGN/nHKjhRUWSuyPrMpRDFFl7wMyBdhVPjY5KA+nHl+fQY9u+Wop8jnk1k7UP1NAIcOKK9chz1yawiPgZxn06zQ==
```

##電子署名の検証
参考：[Signing and Verifying](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/signing_and_verifying)

```swift

//string型に変換したsignatureをbase64でエンコードし、data型に変換
let signature = Data(base64Encoded: signature, options: [])

//署名に使用したアルゴリズムの指定
let algorithm: SecKeyAlgorithm = .rsaSignatureMessagePKCS1v15SHA512

//指定した秘密鍵が、上記のアルゴリズムをサポートしているか検証
guard SecKeyIsAlgorithmSupported(publicKey, .verify, algorithm) else {
    throw error!.takeRetainedValue() as Error
}

//署名の検証
guard SecKeyVerifySignature(
    publicKey,
    algorithm,
    data! as CFData,
    signature! as CFData,
    &error) else {
        throw error!.takeRetainedValue() as Error
}

```

##つまづいたところ
ここでは、実装を行う中で疑問に思ったこと、つまづいたところをメモ程度に記載。
###事象1
作成したprivateKey（**generatedPrivateKey**とする）と、その後keychainに保存した後に`SecItemCopyMatching`メソッドで取得したprivateKey　(**retrievedPrivateKey**とする)のaddrが異なる。なぜ？

```plaintext:ログ出力

generatedPrivateKey: <SecKeyRef algorithm id: 1, key type: RSAPrivateKey, version: 4, block size: 2048 bits, addr: 0x1c402c5a0>
retrievedPrivateKey: <SecKeyRef algorithm id: 1, key type: RSAPrivateKey, version: 4, block size: 2048 bits, addr: 0x1c003c6a0>   <- addrが異なる。
```

####理由
`SecItemCopyMatching`メソッドは、keychainに保存されたprivateKey自体を取得するのではなく、**手元にコピーする**メソッド。そのため、generatedPrivateKeyとretrievedPrivateKeyは異なるオブジェクト（という言い方が適切か不安ですが）であり、参照先（addr）は異なる。

```swift
#generatedPrivateKeyをbase64でエンコードした文字列に変換
let generatedPrivateKeyER = SecKeyCopyExternalRepresentation(generatedPrivateKey, &error)
var generatedPrivateKeyERData: Data = generatedPrivateKeyER! as Data
let generatedPrivateKeyERString = generatedPrivateKeyERData.base64EncodedString()

#retrievedPrivateKeyをbase64でエンコードした文字列に変換
let retrievedPrivateKeyER = SecKeyCopyExternalRepresentation(retrievedPrivateKey, &error)
var retrievedPrivateKeyERData: Data = retrievedPrivateKeyER! as Data
let retrievedPrivateKeyERString = retrievedPrivateKeyERData.base64EncodedString()

#2つの秘密鍵の文字列を比較
if generatedPrivateKeyERString == retrievedPrivateKeyERString {
    print("同じ")
} else {
    print("違う")
}
```

実行結果は以下。2つの秘密鍵の文字列が同一であることが確認できた。

```
同じ
```


###事象2
同一のタグがついた秘密鍵が複数存在すると、`SecItemCopyMatching`メソッドでうまく取得することができない。

####理由
検索するキーがタグのみなので当然っちゃ当然。
秘密鍵を作成する際に、すでに存在するタグをつけないように心がける。公式ドキュメントでも言っている。

参考ページ：　[Generating New Cryptographic Keys](https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/generating_new_cryptographic_keys)


#おまけ
ここではAppleが提供する最新のAPIではないが、処理を行う中で必要であったことを記載しました。参考までに。

##任意の文字列をハッシュ化
Swiftでハッシュ化を行うにはObjective-CのCommonCryptoを利用する必要があるため、あらかじめBridging-Header.hを作成する必要があります。
CryptoSwiftなどの暗号化ライブラリも用意されていますが、今回は使用しない方向で。

実装は、以下ページを参考にしました。
https://dishware.sakura.ne.jp/swift/archives/243

###用意するコード

```objc:Bridging-Header.h
#ifndef Bridging_Header_h
#define Bridging_Header_h
#import "CommonCrypto/CommonHMAC.h"
#endif /* Bridging_Header_h */
```

```swift
enum CryptoAlgorithm {
    case MD5, SHA1, SHA224, SHA256, SHA384, SHA512
    
    var digestLength: Int {
        var result: Int32 = 0
        switch self {
        case .MD5:      result = CC_MD5_DIGEST_LENGTH
        case .SHA1:     result = CC_SHA1_DIGEST_LENGTH
        case .SHA224:   result = CC_SHA224_DIGEST_LENGTH
        case .SHA256:   result = CC_SHA256_DIGEST_LENGTH
        case .SHA384:   result = CC_SHA384_DIGEST_LENGTH
        case .SHA512:   result = CC_SHA512_DIGEST_LENGTH
        }
        return Int(result)
    }
}
extension String {
    var md5:    String { return digest(string: self, algorithm: .MD5) }
    var sha1:   String { return digest(string: self, algorithm: .SHA1) }
    var sha224: String { return digest(string: self, algorithm: .SHA224) }
    var sha256: String { return digest(string: self, algorithm: .SHA256) }
    var sha384: String { return digest(string: self, algorithm: .SHA384) }
    var sha512: String { return digest(string: self, algorithm: .SHA512) }
    
    func digest(string: String, algorithm: CryptoAlgorithm) -> String {
        var result: [CUnsignedChar]
        let digestLength = Int(algorithm.digestLength)
        if let cdata = string.cString(using: String.Encoding.utf8) {
            result = Array(repeating: 0, count: digestLength)
            switch algorithm {
            case .MD5:      CC_MD5(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA1:     CC_SHA1(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA224:   CC_SHA224(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA256:   CC_SHA256(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA384:   CC_SHA384(cdata, CC_LONG(cdata.count-1), &result)
            case .SHA512:   CC_SHA512(cdata, CC_LONG(cdata.count-1), &result)
            }
        } else {
            fatalError("Nil returned when processing input strings as UTF8")
        }
        return (0..<digestLength).reduce("") { $0 + String(format: "%02hhx", result[$1])}
    }
}
```

###ハッシュ化

```swift
let text = "foo bar buz"
print("   MD5: \(text.md5)")
print("  SHA1: \(text.sha1)")
print("SHA224: \(text.sha224)")
print("SHA256: \(text.sha256)")
print("SHA384: \(text.sha384)")
print("SHA512: \(text.sha512)")
```

実行結果

```
   MD5: 0844c8a9954059836ec1e494fccddb5f
  SHA1: 4510a64548a57d900a517628b9c132738c4a4a15
SHA224: 95544f973183821c0172e4d0402e207526e9fe5442098cb88bfa9889
SHA256: 004467f99020d53e57fb79a99bcb8fd5ee646a2bb8542f0110266d796acbd665
SHA384: 83f37298e4b20785e1f77467399545629f24b400e46a27e5b7aa50dc4cf969791a4a31b84927ef464ac6181804f41223
SHA512: 7c60bc750a0cbce5f42c234866f06b9b23575f687876d710f3a3c23282f66b07f545c4b4bb7fee0d76d3183556498c6ed03cb4eb8bec9b9d1972e041cd054343
```
##Dictionary<String, Any>型を、Data型に変換
Dictionary型を暗号化する必要があったが、初めData型への変換方法が分からなかった。（SecKeyCreateSignature関数では、暗号化したいデータはCFData型にする必要がある）

以下のコードでDictionary型からData型への変換ができた。

```swift
let data = try? NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
```
参考：　[Swift - Dictionaryを保存する](http://umadash.hatenadiary.jp/entry/2016/06/18/211138)