Search code examples
swiftrsajava-security

Swift RSA Encryption Public Key to Java Server is failing


I am trying to create public base64 key from RSA Private key using Security framework. Here is snippet.

let tag = "com.example.keys.mykey"
public extension SecKey {
    static func generateBase64Encoded2048BitRSAKey() throws -> (private: String, public: String) {
        let type = kSecAttrKeyTypeRSA
        let attributes: [String: Any] =
            [kSecAttrKeyType as String: type,
             kSecAttrKeySizeInBits as String: 2048
        ]

        var error: Unmanaged<CFError>?
        guard let key = SecKeyCreateRandomKey(attributes as CFDictionary, &error),
            let data = SecKeyCopyExternalRepresentation(key, &error) as Data?,
            let publicKey = SecKeyCopyPublicKey(key),
            let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
                throw error!.takeRetainedValue() as Error
        }
        return (private: data.base64EncodedString(), public: publicKeyData.base64EncodedString())
    }
}

do {
    let (pvtKey, pubKey) = try SecKey.generateBase64Encoded2048BitRSAKey()
    print(pubKey)
} catch let error {
    print(error)
}

This is the output

MIIBCgKCAQEA1ZafTYboquQbCTZMEb1IqHKIr8wiDjdn6e0toRajZCQo9W5zuTlEuctrjJJQ08HcOuK3BPFRaFTUP1RBFvnba/T2S1Mc6WVX81b0DmKS8aPJ83TvvQlH3bZjVqFzndXJHJatcXRkZKlbidNQYxV9OYFCRLwgR5PBoJ1P5tp8f8735vIADOBL/93nFywODSjAWLXcyG5tUyRlRGX7eDodL7jqVOFxVMB7K9UOJehPuJQiheykyPSbBSLE6raZbpCHlranTLdihWYFs2tYbxzNrVbXzgKIxDDjrhDLVFvo3beudKQcLQkSO+m2LJIDT91zAnxVQ075AIn80ZHh5kdyQQIDAQAB

But this public key is not getting accepted by our Java server. It is throwing exception for the same.

Here is java snippet

public static void main(String[] args) {
        String pubKey = "MIIBCgKCAQEA1ZafTYboquQbCTZMEb1IqHKIr8wiDjdn6e0toRajZCQo9W5zuTlEuctrjJJQ08HcOuK3BPFRaFTUP1RBFvnba/T2S1Mc6WVX81b0DmKS8aPJ83TvvQlH3bZjVqFzndXJHJatcXRkZKlbidNQYxV9OYFCRLwgR5PBoJ1P5tp8f8735vIADOBL/93nFywODSjAWLXcyG5tUyRlRGX7eDodL7jqVOFxVMB7K9UOJehPuJQiheykyPSbBSLE6raZbpCHlranTLdihWYFs2tYbxzNrVbXzgKIxDDjrhDLVFvo3beudKQcLQkSO+m2LJIDT91zAnxVQ075AIn80ZHh5kdyQQIDAQAB";
        PublicKey key = getPublic(pubKey);
    }

    public static PublicKey getPublic(String key)  {
        PublicKey pbKey = null; 
        try {
            byte[] keyBytes = Base64.getDecoder().decode(key);
            System.out.println(keyBytes.length);
            X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
            KeyFactory factory = KeyFactory.getInstance("RSA");
            pbKey = factory.generatePublic(spec);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return pbKey;
    }

Here is the exception

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence
    at sun.security.rsa.RSAKeyFactory.engineGeneratePublic(RSAKeyFactory.java:205)
    at java.security.KeyFactory.generatePublic(KeyFactory.java:334)
    at Main.getPublic(Main.java:40)
    at Main.main(Main.java:28)

But the online PEM parser website - https://8gwifi.org/PemParserFunctions.jsp is accepting this public key, which is using bouncycastle library in the background to validate this base64 encoded public key.

enter image description here


Solution

  • Thanks guys. Due to some issues with bouncycastle library, we did not used it in backend service. So in iOS, we are including ASN1 header.

    struct ASN1 {
        let type: UInt8
        let length: Int
        let data: Data
    
        init?(type: UInt8, arbitraryData data: Data) {
            guard data.count > 4 else {
                return nil
            }
    
            var result = data
    
            let byteArray = [UInt8](result)
    
            for (_, v) in byteArray.enumerated() {
                if v == type { // ASN1 SEQUENCE Type
                    break
                }
                result = Data(result.dropFirst())
            }
            guard result.count > 4 else {
                return nil
            }
            guard
                let first = result.advanced(by: 0).first, // advanced start from 7.0
                let second = result.advanced(by: 1).first,
                let third = result.advanced(by: 2).first,
                let fourth = result.advanced(by: 3).first
                else {
                    return nil
            }
    
            var length = 0
            switch second {
            case 0x82:
                length = ((Int(third) << 8) | Int(fourth)) + 4
                break
            case 0x81:
                length = Int(third) + 3
                break
            default:
                length = Int(second) + 2
                break
            }
    
            guard result.startIndex + length <= result.endIndex else { // startIndex, endIndex start from 7.0
                return nil
            }
            result = result[result.startIndex..<result.startIndex + length]
            self.data = result
            self.length = length
            self.type = first
        }
    
        var last: ASN1? {
            get {
                var result: Data?
                var dataToFetch = self.data
                while let fetched = ASN1(type: self.type, arbitraryData: dataToFetch) {
    
                    if let range = data.range(of: fetched.data) {
                        if range.upperBound == data.count {
                            result = fetched.data
                            dataToFetch = Data(fetched.data.dropFirst())
                        } else {
                            dataToFetch = Data(data.dropFirst(range.upperBound))
                        }
                    } else {
                        break
                    }
                }
    
                return ASN1(type: type, arbitraryData: result!)
            }
        }
    
        static func wrap(type: UInt8, followingData: Data) -> Data {
            var adjustedFollowingData = followingData
            if type == 0x03 {
                adjustedFollowingData = Data([0]) + followingData // add prefix 0
            }
            let lengthOfAdjustedFollowingData = adjustedFollowingData.count
            let first: UInt8 = type
            var bytes = [UInt8]()
            if lengthOfAdjustedFollowingData <= 0x80 {
                let second: UInt8 = UInt8(lengthOfAdjustedFollowingData)
                bytes = [first, second]
            } else if lengthOfAdjustedFollowingData > 0x80 && lengthOfAdjustedFollowingData <= 0xFF {
                let second: UInt8 = UInt8(0x81)
                let third: UInt8 = UInt8(lengthOfAdjustedFollowingData)
                bytes = [first, second, third]
            } else {
                let second: UInt8 = UInt8(0x82)
                let third: UInt8 = UInt8(lengthOfAdjustedFollowingData >> 8)
                let fourth: UInt8 = UInt8(lengthOfAdjustedFollowingData & 0xFF)
                bytes = [first, second, third, fourth]
            }
            return Data(bytes) + adjustedFollowingData
        }
    
        static func rsaOID() -> Data {
            var bytes = [UInt8]()
            bytes = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00]
            return Data(bytes)
        }
    }
    

    Then called this during generating public key of RSA in swift.

    class func RSAPublicKeyBitsFromKey(_ secKey:SecKey) -> Data? {
    
        var queryPublicKey:[String:AnyObject] = [:]
        queryPublicKey[kSecClass as String] = kSecClassKey as NSString
        queryPublicKey[kSecAttrKeyType as String] = kSecAttrKeyTypeRSA as NSString
    
        if let publicKeyData = SwiftCrypto.publicKeyInData(queryPublicKey, secKey: secKey) {
            let bitstringSequence = ASN1.wrap(type: 0x03, followingData: publicKeyData)
            let oidData = ASN1.rsaOID()
            let oidSequence = ASN1.wrap(type: 0x30, followingData: oidData)
            let X509Sequence = ASN1.wrap(type: 0x30, followingData: oidSequence + bitstringSequence)
            return X509Sequence
        }
        return nil
    }
    

    So, in this way, I had fixed this issue.