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以上対応アプリに備えて知っておいても良いですね。
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
のプロパティを変更する可能性があるとのこと(Data
のdescription
の挙動が変わったこともありました)で、値のチェックには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のみならず勉強会が盛んで、互いに勉強する環境を作成しようとしていっています。
ゆめみでは、一緒に働ける仲間を絶賛募集中です。
今回の記事を作成するにあたり、参考となったサイトは以下のとおりです。