24
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-18

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

そこで、Appleが提供しているSecurityフレームワークの中で、最新(2018/10/18時点)のAPIを使用して暗号化を試してみる。
Apple Developer Documentation(Security->Keys)

##やりたいことリスト

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

おまけ

  1. 任意の文字列をハッシュ化
  2. 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
ここでのポイントは、以下。

  1. 暗号化する平文は、公開鍵のブロック長より130バイト以上小さくないといけない
  2. 暗号化する平文は、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

###用意するコード

Bridging-Header.h
#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)

参考: Swift - Dictionaryを保存する

24
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?