简体   繁体   English

在 iOS/Swift 中创建并导出为 base64 的 RSA 公钥在 Java 中无法识别

[英]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. TL;DR:无法识别在 iOS 中生成并存储在钥匙串中、导出为 base64 并发送到 java 后端的 RSA 公钥。

I'm implementing a chat encryption feature in an iOS app, and I'm using symmetric + asymmetric keys to handle it.我正在 iOS 应用程序中实现聊天加密功能,我正在使用对称 + 非对称密钥来处理它。

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.我分别在 Swift 和 Java(后端)中创建了两个框架来处理密钥生成、加密、解密等。我也对它们进行了测试,所以我 100% 一切都按预期工作。

However, it looks like the backend is unable to recognize the format of the public key passed from iOS.但是,后端似乎无法识别从 iOS 传递的公钥格式。 Using RSA both sides, this is the code I use in Swift to generate the key:双方都使用 RSA,这是我在 Swift 中用来生成密钥的代码:

// 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:这是我用来将公钥导出为 base64 字符串的一段代码:

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:在后端级别,我使用此 Java 代码将 base64 字符串转换为公钥:

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:在进行一些手动调试时,我注意到公钥的格式不同——当我在 iOS 中生成一个密钥然后导出为 base 64 时,它看起来像这样:

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:总共 360 个字符,而在 Java 中做同样的事情(仍然使用 RSA)就像:

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

with a length of 216 characters.长度为 216 个字符。

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.我无法弄清楚出了什么问题 - 显然,如果 iOS 以不同的密钥处理密钥,并且需要特殊处理才能与其他人交谈,我不会感到惊讶。

Any idea?任何的想法?

We ran into the exact same problem when connecting an iOS app to a Java backend.在将 iOS 应用程序连接到 Java 后端时,我们遇到了完全相同的问题。 And the CryptoExportImportManager mentioned by pedrofb helped us out too, which is awesome. pedrofb提到的CryptoExportImportManager也帮助了我们,这太棒了。 However, the code in the CryptoExportImportManager class is a bit elaborated and might be hard to maintain.但是, CryptoExportImportManager类中的代码有点复杂,可能难以维护。 This is because a top-down approach is used when adding new components to the DER encoding.这是因为在向 DER 编码添加新组件时使用了自上而下的方法。 As a result, numbers contained by length fields must be calculated ahead (ie 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:因此,我创建了一个新类,我们现在使用它来转换 RSA 公钥的 DER 编码:

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:您可以在函数asBase64()中使用此类,如下所示:

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).此行为与问题中描述的发现相匹配(尽管在我们的例子中,Base64 编码密钥的大小已增长到 392 个字符而不是 360 个字符)。 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:第一个解决方案是在定义查询时指定kSecAttrKeySizeInBitskSecAttrEffectiveKeySize ,如以下代码片段所示:

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.我在 GitHub 上发布了这个项目,可以作为上述课程的替代品。

References参考

A Layman's Guide to a Subset of ASN.1, BER, and DER ASN.1、BER 和 DER 子集的外行指南

RFC 5280 (X.509 v3) RFC 5280 (X.509 v3)

RFC 8017 (PKCS #1 v2.2) RFC 8017 (PKCS #1 v2.2)

Some code I found here inspired me when creating the lengthField(...) function.在创建lengthField(...)函数时,我在这里找到的一些代码启发了我。

Java requires a public key encoded in DER format. Java 需要一个以 DER 格式编码的公钥。 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)不幸的是 iOS 不支持这种标准格式,需要额外的转换(我不知道这是否会在最新版本的 swift 中得到改进)

See my answer here You can convert the key using CryptoExportImportManager在这里查看我的答案您可以使用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
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM