#目的
swiftを使用したiOSアプリにおいて、SecKeyEncryptなど従来から存在するAPIで暗号化を行なっている記事は数多く見られたが、それらはAppleの公式Developerドキュメントでは Legacyという扱いになっている。
そこで、Appleが提供しているSecurityフレームワークの中で、最新(2018/10/18時点)のAPIを使用して暗号化を試してみる。
Apple Developer Documentation(Security->Keys)
##やりたいことリスト
- 鍵ペア生成
- 秘密鍵をKeychainに保存
- Keychainに保存された秘密鍵の読み出し
- 公開鍵で暗号化
- 秘密鍵で復号
- ハッシュ化した文字列に電子署名を付与
- 電子署名の検証
おまけ
- 任意の文字列をハッシュ化
- Dictionary型を、Data型に変換
#動作環境
- swift 4.2
- xcode 10.0
#前提
- 鍵ペアは RSA 2048-bit を使用
- ハッシュ関数は SHA-512 を使用
#実装
##鍵ペア生成
参考:Generating New Cryptographic Keys
//秘密鍵につけるタグ(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
##秘密鍵をKeychainに保存
参考:Storing Keys in the Keychain
公開鍵もKeychainに保存することはできるが、秘密鍵があればいつでも作成できるため、Apple側も秘密鍵のみ保存することを推奨している。
なお、上で作成した秘密鍵(privateKey)をKeychainに格納するには、attributesにkSecAttrIsPermanentパラメータを追加し、trueを指定するとOK。
つまり、特別やることはない。
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
ここでのポイントは、秘密鍵を作成した際につけたタグを指定してあげること。
//取得する秘密鍵のタグを指定
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
ここでのポイントは、以下。
- 暗号化する平文は、公開鍵のブロック長より130バイト以上小さくないといけない
- 暗号化する平文は、Data型でないといけない
また、公開鍵を受け渡す先が、PEM形式を希望している場合、base64で文字列にエンコードする必要があるため、その処理も記載した。
//暗号化に使用するアルゴリズムを定義
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型に戻した上で復号しています。
余談ですが、暗号化の対義語って復号化ではないんですね。(以下記事より)
「暗号化」の対語が「復号化」ではない理由
//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
初めは、ハッシュ化と電子署名の作成を別々に実装しようと考えていたが、Appleが提供するAPIでは、なんと1つの関数でハッシュ化と電子署名の作成を同時に行ってくれる。非常に便利。
なお、ハッシュ化せず、署名だけ作成することも可能。
//暗号化アルゴリズムの指定
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
//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が異なる。なぜ?
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)は異なる。
#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
#おまけ
ここではAppleが提供する最新のAPIではないが、処理を行う中で必要であったことを記載しました。参考までに。
##任意の文字列をハッシュ化
Swiftでハッシュ化を行うにはObjective-CのCommonCryptoを利用する必要があるため、あらかじめBridging-Header.hを作成する必要があります。
CryptoSwiftなどの暗号化ライブラリも用意されていますが、今回は使用しない方向で。
実装は、以下ページを参考にしました。
https://dishware.sakura.ne.jp/swift/archives/243
###用意するコード
#ifndef Bridging_Header_h
#define Bridging_Header_h
#import "CommonCrypto/CommonHMAC.h"
#endif /* Bridging_Header_h */
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])}
}
}
###ハッシュ化
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型を、Data型に変換
Dictionary型を暗号化する必要があったが、初めData型への変換方法が分からなかった。(SecKeyCreateSignature関数では、暗号化したいデータはCFData型にする必要がある)
以下のコードでDictionary型からData型への変換ができた。
let data = try? NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)