5

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

user207421
  • 305,947
  • 44
  • 307
  • 483
Ankit Thakur
  • 4,739
  • 1
  • 19
  • 35

2 Answers2

6

The exception is thrown because the ASN.1 DER encoding of an RSA public key generated on iOS is represented with the RSAPublicKey type as defined by PKCS#1, while Java (and many other languages and tools) expect the DER encoding to be represented with the SubjectPublicKeyInfo type as defined by X.509. There are of course two sides where this problem can be solved. And if you choose to convert the DER encoding of the RSA public key at the iOS side, you could use this project I recently published on GitHub. The structure you may be interested in is RSAPublicKeyExporter, which uses the SimpleASN1Writer for converting the DER encoding. The code snippet below shows how to use it:

import RSAPublicKeyExporter

let publicKeyData = ... // Get external representation of RSA public key some how

let x509EncodedKeyData = RSAPublicKeyExporter().toSubjectPublicKeyInfo(publicKeyData)

The answer I posted here contains some information that may be useful in case the exported key is fetched from the keychain.

Next Increment
  • 141
  • 3
  • 6
  • I have verified that, and so I have posted answer which include ASN1 headers. Unfortunately, iOS SDK only supports Raw encrypted data, But `SubjectPublicKeyInfo` needs ASN1 header. – Ankit Thakur Sep 03 '19 at 10:53
  • @AnkitThakur I agree, both solutions do the same thing. The raw encrypted data you are referring to is in fact the ASN.1 DER encoding of an RSA public key that is formatted in a certain way (two INTEGER values wrapped in a SEQUENCE). And this format can be referred to as the RSAPublicKey type as defined by PKCS#1. Now, if you add the encoding of a SEQUENCE, an OBJECT IDENTIFIER and a BIT STRING (aka “header”) on top of these bytes, you’ll get a new DER encoding. The format of this encoding can be referred to as the SubjectPublicKeyInfo type as defined by X.509. – Next Increment Sep 03 '19 at 16:36
3

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.

Ankit Thakur
  • 4,739
  • 1
  • 19
  • 35
  • I faced the same issue and fixed it by adding ASN1 header. However, I am not able to decrypt the encrypted message from backend. Error is "Error Domain=NSOSStatusErrorDomain Code=-50 "RSAdecrypt wrong input (err -27)" UserInfo={numberOfErrorsDeep=0, NSDescription=RSAdecrypt wrong input (err -27)". Did anyone face similar issue? – Nay Jun 26 '23 at 10:21