[IOS]IOS的key pair&sign与java互相转换

这个部分研究用了我很多时间,终于找到一个方法.

一.核心:

IOS的key pair和java的是不通的,没办法直接转换,原因在于他们的DER(Distinguished Encoding Rules)是不一样的.IOS接收的是ASN.1格式,而java接收的是X509格式.

解决思路就是把ASN.1转换成X509,反之亦然.

(同样道理,private key的格式也是不通的,iOS 的是PKCS1,而java的是PKCS8)

二.转换:

 
[IOS]IOS的key pair&sign与java互相转换
 具体流程就如上所示.

这里面需要用到另外一个库

参考:https://digitalleaves.com/blog/2015/10/sharing-public-keys-between-ios-and-the-rest-of-the-world/

"Exporting iOS-generated public keys to the outside world."这部分

这是GitHub:https://github.com/DigitalLeaves/CryptoExportImportManager

分析:

这个是export的总流程:

@IBAction func generateAndExportPublicKey(_ sender: AnyObject) {
        self.view.isUserInteractionEnabled = false
        self.textView.text = "Trying to get public key data from Keychain first..."
        let keyType = getKeyTypeFromSegmentedControl()
        if let publicKeyData = getPublicKeyData(kExportKeyTag + keyType) {
            self.textView.text = self.textView.text + "Success!\nPublic key raw bytes: \(publicKeyData.hexDescription)\n\n"
            self.exportKeyFromRawBytesAndShowInTextView(publicKeyData)
            self.view.isUserInteractionEnabled = true
        } else {
            self.textView.text = self.textView.text + "Failed! Will try to generate keypair...\n"
            createSecureKeyPair(kExportKeyTag + keyType) { (success, pubKeyData) -> Void in
                if success && pubKeyData != nil {
                    self.textView.text = self.textView.text + "Success!\nPublic key raw bytes:\(pubKeyData!)\n"
                    self.exportKeyFromRawBytesAndShowInTextView(pubKeyData!)
                } else {
                    self.textView.text = self.textView.text + "Oups! I was unable to generate the keypair to test the export functionality."
                }
                self.view.isUserInteractionEnabled = true
            }
        }
    }

这里生成key pair:

func createSecureKeyPair(_ keyTag: String, completion: ((_ success: Bool, _ pubKeyData: Data?) -> Void)? = nil) {
        // private key parameters
        let privateKeyParams: [String: AnyObject] = [
            kSecAttrIsPermanent as String: true as AnyObject,
            kSecAttrApplicationTag as String: keyTag as AnyObject,
        ]
        
        // private key parameters
        let publicKeyParams: [String: AnyObject] = [
            kSecAttrApplicationTag as String: keyTag as AnyObject,
            kSecAttrIsPermanent as String: true as AnyObject
        ]
        
        // global parameters for our key generation
        let parameters: [String: AnyObject] = [
            kSecAttrKeyType as String:          getKeyTypeFromSegmentedControl() as AnyObject,
            kSecAttrKeySizeInBits as String:    getKeyLengthFromSegmentedControl() as AnyObject,
            kSecPublicKeyAttrs as String:       publicKeyParams as AnyObject,
            kSecPrivateKeyAttrs as String:      privateKeyParams as AnyObject,
        ]
        
        // asynchronously generate the key pair and call the completion block
        DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { () -> Void in
            var pubKey, privKey: SecKey?
            let status = SecKeyGeneratePair(parameters as CFDictionary, &pubKey, &privKey)
            if status == errSecSuccess {
                DispatchQueue.main.async(execute: {
                    print("Successfully generated keypair!\nPrivate key: \(privKey)\nPublic key: \(pubKey)")
                    let publicKeyData = self.getPublicKeyData(kExportKeyTag + self.getKeyTypeFromSegmentedControl())
                    completion?(true, publicKeyData)
                })
            } else {
                DispatchQueue.main.async(execute: {
                    print("Error generating keypair: \(status)")
                    completion?(false, nil)
                })
            }
        }
    }

 用的也是ios的api,可以参考Apple的文档:

https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys

/generating_new_cryptographic_keys

这样重新获取public key:(根据generate时的key tag)

func getPublicKeyData(_ keyTag: String) -> Data? {
        let parameters = [
            kSecClass as String: kSecClassKey,
            kSecAttrKeyType as String: getKeyTypeFromSegmentedControl(),
            kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
            kSecAttrApplicationTag as String: keyTag,
            kSecReturnData as String: true
        ] as [String : Any]
        var data: AnyObject?
        let status = SecItemCopyMatching(parameters as CFDictionary, &data)
        if status == errSecSuccess {
            return data as? Data
        } else { print("Error getting public key data: \(status)"); return nil }
    }

这里就是转换成PEM(X509的文件格式):

func exportKeyFromRawBytesAndShowInTextView(_ rawBytes: Data) {
        let keyType = getKeyTypeFromSegmentedControl()
        let keySize = getKeyLengthFromSegmentedControl()
        let exportImportManager = CryptoExportImportManager()
        if let exportableDERKey = exportImportManager.exportPublicKeyToDER(rawBytes, keyType: keyType, keySize: keySize) {
            self.textView.text = self.textView.text + "Exportable key in DER format:\n\(exportableDERKey.hexDescription)\n\n"
            print("Exportable key in DER format:\n\(exportableDERKey.hexDescription)\n")
            let exportablePEMKey = exportImportManager.PEMKeyFromDERKey(exportableDERKey)
            self.textView.text = self.textView.text + "Exportable key in PEM format:\n\(exportablePEMKey)\n\n"
            print("Exportable key in PEM format:\n\(exportablePEMKey)\n")
        } else {
            self.textView.text = self.textView.text + "Unable to generate DER key from raw bytes."
        }
    }

经过上面的步骤,就获得一串经过base64编码的string,如下:

Exportable key in PEM format:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0CglywDQCmCdLNT8tiCJm75poZQo
f2w+zdKCtyphK5LEgbWC8RUYojgFU0AEDdBVT5U4fBZwjBkjOnzCGCQEAQ==
-----END PUBLIC KEY-----

切去"-----BEGIN PUBLIC KEY-----""-----END PUBLIC KEY-----"然后传给java,java用base64 decode,获得byte[],就可以顺利generate出public key.

**注意:这个库也有个缺憾,就是没有192的曲线头

private let kCryptoExportImportManagerSecp256r1CurveLen = 256
private let kCryptoExportImportManagerSecp256r1header: [UInt8] = [0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00]
private let kCryptoExportImportManagerSecp256r1headerLen = 26

 所以暂时转换不了192的曲线.

附加:使用openssl命令生成的PEM,取出string也可以转给java,不过ios的openssl库没有EC加密,只有RSA.所以这里不用openssl库.

补充:后来看到有同事使用cocopod来引用了openssl的库,应该是有生成EC曲线的API的,但还没尝试.

三.有可能有坑的地方:

首先,这样ios生成出来的PEM可以转换成java,但是打印发现java的key pair长度是91,但是ios的是123.后来发现第三方库生成的public key raw byte长度也是91,但是原封代码嵌入到我的项目中就不知道为什么出现123长度.而且这个长度base64 decode后也是可以转成java的public key的,十分神奇.

四.签名:

1.首先一下这篇文章介绍了如何进行非对称加密和签名和认证:

https://digitalleaves.com/blog/2015/10/asymmetric-cryptography-in-swift/

GitHub:https://github.com/DigitalLeaves/AsymmetricCrypto

我使用的也是上面这个库的签名方法.

这是Apple的签名guide:https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/signing_and_verifying?language=objc

以下是代码:

func signWithPrivate(_ text: String) -> String?{
        
        let privateKey: SecKey = self.getPrivateKeyReference("your generate tag")!
        if #available(iOS 10.0, *) {
            let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA1
            
            guard SecKeyIsAlgorithmSupported(privateKey, .sign, algorithm) else {
                print("Can't not sign")
                return nil
            }
            
            var error: Unmanaged<CFError>?
            let data = text.data(using: String.Encoding.utf8)
            guard let signature = SecKeyCreateSignature(privateKey,
                                                        algorithm,
                                                        data! as CFData,
                                                        &error) as Data? else {
                                                            print("Sign fail")
                                                            return nil
            }
            //            let sign = String(data: signature.base64EncodedData() as Data, encoding: String.Encoding.utf8)
            return signature.base64EncodedString()
            
        } else {
            let signature = self.signWithPrivateKeyUnderIOS10("Test123", privateKey)
            return signature
            
        }
    }
    
    
    private func signWithPrivateKeyUnderIOS10(_ text: String, _ key: SecKey) -> String? {
        var digest = Data(count: Int(CC_SHA1_DIGEST_LENGTH))
        let data = text.data(using: .utf8)!
        
        let _ = digest.withUnsafeMutableBytes { digestBytes in
            data.withUnsafeBytes { dataBytes in
                CC_SHA1(dataBytes, CC_LONG(data.count), digestBytes)
            }
        }
        
        var signature = Data(count: SecKeyGetBlockSize(key) * 4)
        var signatureLength = signature.count
        
        let result = signature.withUnsafeMutableBytes { signatureBytes in
            digest.withUnsafeBytes { digestBytes in
                SecKeyRawSign(key,
                              SecPadding.PKCS1SHA1,
                              digestBytes,
                              digest.count,
                              signatureBytes,
                              &signatureLength)
            }
        }
        
        let count = signature.count - signatureLength
        signature.removeLast(count)
        
        guard result == noErr else {
            return nil;
        }
        //        let str123 = String(data: signature.base64EncodedData() as Data, encoding: String.Encoding.utf8)
        //        return str123
        return signature.base64EncodedString()
    }

上面的方法需要在ios10以上使用,下面的是适配ios10以下.签名后是base64编码了的,java那边验证的话也是需要先对这个签名进行解码.

另外需要重点说明一下,如果需要加密的内容含有类等元素,直接转成string来签名,签名后是验证不上的.因为转string过程中失真了.这个时候就需要编码一下,把要签名的内容先编码,然后再签名,这样最稳妥.那么对于java那边,同样需要对content进行编码来进行签名内容核对.