Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSA public key created in iOS/Swift and exported as base64 not recognized in Java

TL;DR: RSA public key generated in iOS and stored in the keychain, exported as base64 and sent to a java backend, is not recognized.

I'm implementing a chat encryption feature in an iOS app, and I'm using symmetric + asymmetric keys to handle it.

Without going too much into details, at backend I use the user's public key to encrypt a symmetric key used to encrypt and decrypt messages.

I created two frameworks, respectively in Swift and in Java (backend) to handle key generation, encryption, decryption, etc. I also have tests for them, so I'm 100% everything works as expected.

However, it looks like the backend is unable to recognize the format of the public key passed from iOS. Using RSA both sides, this is the code I use in Swift to generate the key:

// private key parameters
static let privateKeyParams: [String : Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]

// public  key parameters
static let publicKeyParams: [String : Any] = [
        kSecAttrIsPermanent as String: true,
        kSecAttrApplicationTag as String: "..." // I have a proper unique tag here
]

// global parameters for our key generation
static let keyCreationParameters: [String : Any] = [
        kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
        kSecAttrKeySizeInBits as String: 2048,
        kSecPublicKeyAttrs as String: publicKeyParams,
        kSecPrivateKeyAttrs as String: privateKeyParams
]

...

var publicKey, privateKey: SecKey?
let status = SecKeyGeneratePair(Constants.keyCreationParameters as CFDictionary, &publicKey, &privateKey)

I use specular code to read the keys from the keychain.

This is the piece of code I use to export the public key as a base64 string:

extension SecKey {
  func asBase64() throws -> String {
    var dataPtr: CFTypeRef?
    let query: [String:Any] = [
      kSecClass as String: kSecClassKey,
      kSecAttrApplicationTag as String: "...", // Same unique tag here
      kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
      kSecReturnData as String: kCFBooleanTrue
    ]
    let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)

    switch (result, dataPtr) {
    case (errSecSuccess, .some(let data)):
      // convert to Base64 string
      let base64PublicKey = data.base64EncodedString(options: [])
      return base64PublicKey
    default:
      throw CryptoError.keyConversionError
    }
  }
}

At backend level I use this Java code to convert the base64 string to a public key:

public PublicKey publicKeyFrom(String data) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] publicBytes = Base64.decodeBase64(data);
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    return keyFactory.generatePublic(keySpec);
}

But this fails at the last line, with this exception:

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence

Doing some manual debugging, I noticed that the format of the public key is different - when I generate a key in iOS and then export as base 64, it looks like this:

MIIBCgKCAQEA4M/bRDdH0f6qFIXxOg13RHka+g4Yv8u9PpPp1IR6pSwrM1aq8B6cyKRwnLe/MOkvODvDfJzvGXGQ01zSTxYWAW1B4uc/NCEemCmZqMosSB/VUJdNxxWtt2hJxpz06hAawqV+6HmweAB2dUn9tDEsQLsNHdwYouOKpyRZGimcF9qRFn1RjR0Q54sUh1tQAj/EwmgY2S2bI5TqtZnZw7X7Waji7wWi6Gz88IkuzLAzB9VBNDeV1cfJFiWsZ/MIixSvhpW3dMNCrJShvBouIG8nS+vykBlbFVRGy3gJr8+OcmIq5vuHVhqrWwHNOs+WR87K/qTFO/CB7MiyiIV1b1x5DQIDAQAB

for a total of 360 characters, whereas doing the same in Java (still using RSA) it's like:

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCAAnWO4BXUGP0qM3Op36YXkWNxb4I2pPZuZ7jJtfUO7v+IO1mq43WzNaxLqqLPkTnMrv2ACRDK55vin+leQlL1z0LzVxjtZ9F6pajQo1r7PqBlL5N8bzBFKpagEf0QfyHPw0/0kG9DMnvQ+Im881QyN2zdl33wp5Fi+jRT7cunFQIDAQAB

with a length of 216 characters.

I'm unable to figure out what's wrong - apparently I wouldn't be surprised if iOS handles keys in a different key, and require special processing in order to talk with other folks.

Any idea?

like image 939
Antonio Avatar asked Dec 23 '18 18:12

Antonio


2 Answers

We ran into the exact same problem when connecting an iOS app to a Java backend. And the CryptoExportImportManager mentioned by pedrofb helped us out too, which is awesome. However, the code in the CryptoExportImportManager class is a bit elaborated and might be hard to maintain. This is because a top-down approach is used when adding new components to the DER encoding. As a result, numbers contained by length fields must be calculated ahead (i.e. before the contents to which the length applies has been defined). I therefore created a new class that we now use to convert the DER encoding of an RSA public key:

class RSAKeyEncoding: NSObject {

  // ASN.1 identifiers
  private let bitStringIdentifier: UInt8 = 0x03
  private let sequenceIdentifier: UInt8 = 0x30

  // ASN.1 AlgorithmIdentfier for RSA encryption: OID 1 2 840 113549 1 1 1 and NULL
  private let algorithmIdentifierForRSAEncryption: [UInt8] = [0x30, 0x0d, 0x06,
    0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00]

  /// Converts the DER encoding of an RSA public key that is either fetched from the
  /// keychain (e.g. by using `SecItemCopyMatching(_:_:)`) or retrieved in another way
  /// (e.g. by using `SecKeyCopyExternalRepresentation(_:_:)`), to a format typically
  /// used by tools and programming languages outside the Apple ecosystem (such as
  /// OpenSSL, Java, PHP and Perl). The DER encoding of an RSA public key created by
  /// iOS is represented with the ASN.1 RSAPublicKey type as defined by PKCS #1.
  /// However, many systems outside the Apple ecosystem expect the DER encoding of a
  /// key to be represented with the ASN.1 SubjectPublicKeyInfo type as defined by
  /// X.509. The two types are related in a way that if the SubjectPublicKeyInfo’s
  /// algorithm field contains the rsaEncryption object identifier as defined by
  /// PKCS #1, the subjectPublicKey field shall contain the DER encoding of an
  /// RSAPublicKey type.
  ///
  /// - Parameter rsaPublicKeyData: A data object containing the DER encoding of an
  ///     RSA public key, which is represented with the ASN.1 RSAPublicKey type.
  /// - Returns: A data object containing the DER encoding of an RSA public key, which
  ///     is represented with the ASN.1 SubjectPublicKeyInfo type.
  func convertToX509EncodedKey(_ rsaPublicKeyData: Data) -> Data {
    var derEncodedKeyBytes = [UInt8](rsaPublicKeyData)

    // Insert ASN.1 BIT STRING bytes at the beginning of the array
    derEncodedKeyBytes.insert(0x00, at: 0)
    derEncodedKeyBytes.insert(contentsOf: lengthField(of: derEncodedKeyBytes), at: 0)
    derEncodedKeyBytes.insert(bitStringIdentifier, at: 0)

    // Insert ASN.1 AlgorithmIdentifier bytes at the beginning of the array
    derEncodedKeyBytes.insert(contentsOf: algorithmIdentifierForRSAEncryption, at: 0)

    // Insert ASN.1 SEQUENCE bytes at the beginning of the array
    derEncodedKeyBytes.insert(contentsOf: lengthField(of: derEncodedKeyBytes), at: 0)
    derEncodedKeyBytes.insert(sequenceIdentifier, at: 0)

    return Data(derEncodedKeyBytes)
  }

  private func lengthField(of valueField: [UInt8]) -> [UInt8] {
    var length = valueField.count

    if length < 128 {
      return [ UInt8(length) ]
    }

    // Number of bytes needed to encode the length
    let lengthBytesCount = Int((log2(Double(length)) / 8) + 1)

    // First byte encodes the number of remaining bytes in this field
    let firstLengthFieldByte = UInt8(128 + lengthBytesCount)

    var lengthField: [UInt8] = []
    for _ in 0..<lengthBytesCount {
      // Take the last 8 bits of length
      let lengthByte = UInt8(length & 0xff)
      // Insert them at the beginning of the array
      lengthField.insert(lengthByte, at: 0)
      // Delete the last 8 bits of length
      length = length >> 8
    }

    // Insert firstLengthFieldByte at the beginning of the array
    lengthField.insert(firstLengthFieldByte, at: 0)

    return lengthField
  }
}

Usage

You could use this class in the function asBase64() like this:

extension SecKey {
  func asBase64() throws -> String {
    var dataPtr: CFTypeRef?
    let query: [String:Any] = [
      kSecClass as String: kSecClassKey,
      kSecAttrApplicationTag as String: "...", // Same unique tag here
      kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
      kSecReturnData as String: kCFBooleanTrue
    ]
    let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)

    switch (result, dataPtr) {
    case (errSecSuccess, .some(let data)):

      // convert to X509 encoded key
      let convertedData = RSAKeyEncoding().convertToX509EncodedKey(data)

      // convert to Base64 string
      let base64PublicKey = convertedData.base64EncodedString(options: [])
      return base64PublicKey
    default:
      throw CryptoError.keyConversionError
    }
  }
}

UPDATE - Other Issue

After using the above class for a while, we stumbled upon another issue. Occasionally, the public key that is fetched from the keychain seems to be invalid because, for some reason, it has grown in size. This behavior matches with findings described in the question (although in our case the Base64 encoded key has grown to a size of 392 characters instead of 360 characters). Unfortunately, we didn’t find the exact cause of this strange behavior, but we found two solutions. The first solution is to specify kSecAttrKeySizeInBits along with kSecAttrEffectiveKeySize when defining the query, like in the below code snippet:

let keySize = ... // Key size specified when storing the key, for example: 2048

let query: [String: Any] = [
    kSecAttrKeySizeInBits as String: keySize,
    kSecAttrEffectiveKeySize as String: keySize,
    ... // More attributes
]

var dataPtr: CFTypeRef?

let result = SecItemCopyMatching(query as CFDictionary, &dataPtr)

The second solution is to always delete the old key from the keychain (if any) before adding a new key with the same tag.

UPDATE - Alternative Solution

I published this project on GitHub that can be used as an alternative to the above class.

References

A Layman’s Guide to a Subset of ASN.1, BER, and DER

RFC 5280 (X.509 v3)

RFC 8017 (PKCS #1 v2.2)

Some code I found here inspired me when creating the lengthField(...) function.

like image 87
Next Increment Avatar answered Sep 22 '22 07:09

Next Increment


Java requires a public key encoded in DER format. Unfortunately iOS does not support this standard format and it is needed an additional conversion (I do not know if this will have improved in the latest versions of swift)

See my answer here You can convert the key using CryptoExportImportManager

func exportPublicKeyToDER(keyId:String) -> NSData?{

    let publicKey = loadKeyStringFromKeyChainAsNSData(PUBLIC_KEY + keyId)
    let keyType = kSecAttrKeyTypeRSA
    let keySize = 2048
    let exportImportManager = CryptoExportImportManager()
    if let exportableDERKey = exportImportManager.exportPublicKeyToDER(publicKey, keyType: keyType as String, keySize: keySize) {
        return exportableDERKey
    } else {
        return nil
    }
}
like image 44
pedrofb Avatar answered Sep 20 '22 07:09

pedrofb