0

Objective

Writing an Encryption and Decryption-Test in a Java17 SpringBoot Service using RSA-keys generated on an iOS mobile device.

Hence:

  • Target: RSA-4096 is used throughout
  • This is about an iOS mobile device (Swift) <-> SpringBoot Service (Java17) context

Why is this question important?

The aim of the process:

  1. On the iOS mobile divice a KeyPair is created and the public key (only) is sent to the SpringBoot Service.
  2. Somewhen payload is encrypted in the SpringBoot Service and held for collection.
  3. Later on the iOS mobile device pulls the encrypted data and decrypts it locally with its private key to retreive the payload.

Sidenote

This had to be done with an Android mobile device as well and it works fine. Android serves PKCS-Formatted Keys. Luckily regarding Android, both sides may use the standard javax.crypto-Library (only).

Hurdle along the way

Key format: ASN.1

The iOS-device does not export keys along the plain PKCS-Format. It uses ASN.1-Format from the same 2003 RFC: https://www.rfc-editor.org/rfc/rfc3447#appendix-A Nevertheless, the keys can be extracted and exported from the keyChain (not the Secure Enclave).

SampleCode to extract the Keys from the KeyPair (Swift):

// Keypair attributes
let tag = "my_personal_keystore_keypair_alias".data(using: .utf8)!
let attributes: [String: Any] =
    [kSecAttrKeyType as String:            kSecAttrKeyTypeRSA,
     kSecAttrKeySizeInBits as String:      "4096",
     kSecPrivateKeyAttrs as String:
        [kSecAttrIsPermanent as String:    true,
         kSecAttrApplicationTag as String: tag]
]
var error: Unmanaged<CFError>?

// Generate PrivateKey / Keypair
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue() as Error
}

// Extract PrivateKey
guard
    let privateKeyOptional = SecKeyCopyExternalRepresentation(privateKey, nil) else {
    return "Key generation did not succeed."
}
let privateKeyData = privateKeyOptional as Data
let base64PrivateKey = privateKeyData.base64EncodedString()
print("PrivateKey, base64:   ", base64PrivateKey)

// Extract PublicKey
guard let publicKey = SecKeyCopyPublicKey(privateKey),
      let publicKeyOptional = SecKeyCopyExternalRepresentation(publicKey, nil) else {
    return "PublicKey could not be accessed."
}
let publicKeyData = publicKeyOptional as Data
let base64PublicKey = publicKeyData.base64EncodedString()
print("PublicKey, base64:   ", base64PublicKey)

Sample Base64-encoded Keys (Java):

// === Sample KeyPair START ===
String publicKey = "MIICCgKCAgEAu2QLxnFmEMeZTtiX4DRUmeqsocOFtqyP2cKAiWCjbr75D+Ymzem0T3/oJov4G68yPMMAB/scdrxlpu3D7UmOUjeQ4aNUW3u/ZnHoDRdph7Jc/2ed6jAmPV/867BV78XZXL0O9Ofru6W+jCwuFunJN3IWhiVJhwCmYgn3wQDl6TeIYKysZ2XKWauFFlLYY9RSZAoCCyEr5PPS9irJhhiQiP2bv2cqh0eW/8N1DXBDSx80bsgtrhAFaG9UU/P5ApoCr+EmwqM6wEjYbTV/fWi0b74UGxiE2UtvEMHfp+XJ+JkbXH4GT7v+Gwhtkuq0+3wrSSD3bCZ5by7ti1+MK6bugsQ96+teY3Bd3jKHaGVLvYN2mIGwRW7Jmbf93B9s8z5nmvNJibF67Ru+0M86VJBIjmhbmoEJwPda6FDTqJObRmQwqGj6JSIwTeSnEqKA/8PZXn+qID0HaDly4uoVb5q2yDaykvqmA8k5IGV5UOjQvqf6saD4UL8nYmubUcQxLm5m9AC9J4sAmJwWGs5tuYr7auyH+GFIOmn7irtC+YX/lNAypgvnklJEWNmz/M/nBq1nDD4eygpY0E/k/cWMloLTwwvRbjj0ApUbGz1RUB2UP9ZI4Avb17UKhXXylLra9tPz1QtNIhwhIOX3tQzIQMiyK7aMJJUryBJajMzHowYSYQMCAwEAAQ==";
String privateKey = "MIIJKAIBAAKCAgEAu2QLxnFmEMeZTtiX4DRUmeqsocOFtqyP2cKAiWCjbr75D+Ymzem0T3/oJov4G68yPMMAB/scdrxlpu3D7UmOUjeQ4aNUW3u/ZnHoDRdph7Jc/2ed6jAmPV/867BV78XZXL0O9Ofru6W+jCwuFunJN3IWhiVJhwCmYgn3wQDl6TeIYKysZ2XKWauFFlLYY9RSZAoCCyEr5PPS9irJhhiQiP2bv2cqh0eW/8N1DXBDSx80bsgtrhAFaG9UU/P5ApoCr+EmwqM6wEjYbTV/fWi0b74UGxiE2UtvEMHfp+XJ+JkbXH4GT7v+Gwhtkuq0+3wrSSD3bCZ5by7ti1+MK6bugsQ96+teY3Bd3jKHaGVLvYN2mIGwRW7Jmbf93B9s8z5nmvNJibF67Ru+0M86VJBIjmhbmoEJwPda6FDTqJObRmQwqGj6JSIwTeSnEqKA/8PZXn+qID0HaDly4uoVb5q2yDaykvqmA8k5IGV5UOjQvqf6saD4UL8nYmubUcQxLm5m9AC9J4sAmJwWGs5tuYr7auyH+GFIOmn7irtC+YX/lNAypgvnklJEWNmz/M/nBq1nDD4eygpY0E/k/cWMloLTwwvRbjj0ApUbGz1RUB2UP9ZI4Avb17UKhXXylLra9tPz1QtNIhwhIOX3tQzIQMiyK7aMJJUryBJajMzHowYSYQMCAwEAAQKCAgAfJd6VMjU6dcsEYZlBIcGsQedHDjZ0KlPQ6PUvoJoZ5vGEVIe/s2iOzF58xchMZb8ufWVMbk+JZwBokl3+W7sl7GmPL/RuLnAeqbFeN7WJYjr2EzWa/zzj98gVLx7ht5vNP/mz+Lbk3oSBTTiuA1c4eaTH0Hvbzl5Zrnl5odoVfW8UTq9rkm5joFCDaOriESFO0qELU4y1xlebJnqP6RZhRvJ0CsR1bw9o3QbgYHg3DO1MusZpB+22McctG0EZTxtCO+US9knmO1WKNZnG8TgI2OoDpPw0GEdSXD9+a4I6acyz/5ix+TggKzL3eD70DGwvgCTQW8bUldLTV2L3wIwldFOnwNWDHwOXbQ/SmFkj56/TazRBL9DjAxrG4DDVsJ2OglOhpY70F32SgaQjpAVgV7lDYsGFD35Cm4nQV3h7lYWY56ErNnXZs+4epngQLRtFx2dYvxmWRqX11xHNDy9pdgodW3BZBvEMyWEZbLXhjbu/n4Vv4m8zQGtC/0JKZ7y4KvLcZwaAihAmRz8g0q4gOKdA8QygtFJ5yY04myvREKg9Z5v/P/Kxj4yOf2kFfy3Zmvyb2X4G3YcH1UUkDsJOqd0+qHowOQgTQrAcQOvJ0hmmy7yeQzR491kY0OWhcIpS2ldV1FmdS/PukfI5r12N0NiuqGSzBc0g0rp5rGGYLQKCAQEA612qiMHur8R4YvvHWrvHKYj6otqh0yTR0WrbnrICbRoaOITlRjcxTcnFDX1W9AjWUDjCw426W44WenAhlctR5+3M+XaXDmeW587iG++/zyaV344LBsg1cn3VrI8ViJKTRC7UFZclo3YCT6GDGGuHMLCpeJW67Tpfmgpz5SvIaDSQQaNxy6qXhnZ7itebLXt+HYm11IcIg5XqfWwbkPMMrfYObphIMxBdedUkr47Tlw0As2IQ4F97Q1N1Jst4NldxlrA6OHL2cK2HH0IKK8uq6jBu/7gELDFbGEhVrLxmgs4ojFQJc5Gdplcz/ST347munFftpzYx2rRJXKo9OfwkPQKCAQEAy9Gr74MuvpW+rF8Xp57qDY8Gkzrqfq2J60F34oiJE69m59CXv3awvLpImw2sEox8Y7jgpXUrBZ06s0f/5MDg2c+4KwtO0sTkZTOXhnHipOSJUsd3d7v2z42Ch4YXHnTMKSyn+xKv9nRQ2Pff6ejS2NHj4/l1hZ5W7FgXkFXiDtGJG6HKqlE1awHbXkvwPR4YhjOmej8gdsc+ULu7dh05IGJ/J35XIWzXMK3hX92Bxe/+VrDcCTSseACZYWhWpJpddk6O30pDffH74GjnkRQ5Lgyxo/cg+9pfKdVvmFZecgeSj0EY2pULJh+y32uNAfzkBiiF6X9k7PNLWWIRVDSuPwKCAQAO1uT9olyOMHD2rLExA03XTI+g3O//A/9GmNon80k7371vetGJz8kIAoSuCQ0Gbdg1Tp7Y/YKWayr3pUI404zidpfJ0rRLcDSPgPe4kzEgumoQokAHuW/FPDHQo2TUK4mlbt5oThNNbw9OPfyp+X2YkErfE3Gpq1iDucz80fncuBOwT8HI+YR8MdQwOM/L2lFlQ113fNwIj9cs+Tfzt59BCJZ4WpmSvqFmIQ4jE3o7t9InfTNbMinvYL+uJn35zyGWQp4pGPZ4vDgcvGkvwbOQ+GTHMq7wqlv39/eO4IIGFUFxN4sxAilSZ4UbnM0USoy7xr9xH3WdOGi3svQRR9hxAoIBABx6ZmCn3q8ocyTYgJCeJqvQUSXfNIaQrtWdJygS1bxXZLR9M8a/ycAE80Ie7e0FjhfM7C6SKXm2V05XgAyxWnl0iZISGWhftF3jkIdrgDRz7jAPyMSFEd48MoHHHZHW1fPm1m3BVa7E38sBD1s6ecNryEDBSUdrMVACmwBCz7wsUND4kT2s7R7Pepw5Vg7kFp8htmAcU+fkvPNA19eQC7xXptaY04nLEGIv2W6wn4JNnybzvTrYDkUSKFww3PJQ00BFh7bxRG7jkcLwRXLC9Z5WjbeQPx6Ri3xn4xjQ8I9UOYkkmlloO8+O3EpVV7VwZVfq75MJhsuIzv1lM3Clj7cCggEBALMy2/G6rcK1f8I21utDAD/VOWRswvK5pahwwRLqYOadt4n7qTik/TdwXCSD8BrHyKEX+o28tNoq+MZpazRyORLt7jn5Xs3Npf2xu31rc39SNhyXj8NMdpbycI1lB/j8K39QjnQse4iM/up/zZfWyiE+Qn8aEjEd3V594SbETgjlZqPnaZ2Hp/cet5O2POp9XuihZd1RgRqHsd1AwnBAG1JTzb5Gc0ZXK+iOZQrXKlSm71bXQMKc856s6OXeFEpPjcstDbO5ySnPmL4jWde9vZZ+MLWgfQAB3WkmNTIQoybWFPHSt8vpXsrq2Snxet+3JisS5nKND0arwFLRw5u06Kk=";
// === Sample KeyPair END ===

Solving the hurdle

Afaik the javax.crypto-Library does not support ASN.1-Formatted RSA-Keys. Therefore the bouncycastle-dependency was used to parse the PublicKey (SpringBoot pom.xml):

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
</dependency>

Converting the PublicKey-String into a PublicKey-Class (Java):

public Optional<PublicKey> createIOSPublicKeyFromString(String publicKeyASN1) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyASN1);

    ASN1Sequence sequence = ASN1Sequence.getInstance(publicKeyBytes);
    ASN1Integer modulus = ASN1Integer.getInstance(sequence.getObjectAt(0));
    ASN1Integer exponent = ASN1Integer.getInstance(sequence.getObjectAt(1));
    RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus.getPositiveValue(),
            exponent.getPositiveValue());

    KeyFactory factory = KeyFactory.getInstance("RSA");

    return Optional.ofNullable(factory.generatePublic(keySpec));
}

The encryption using the PublicKey works without further interruption (Java):

public String encryptPayload(String payload, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
    Cipher encryptCipher = Cipher.getInstance(encryptionProperties.getAlgorithm());
    encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);

    byte[] secretMessageBytes = payload.getBytes(StandardCharsets.UTF_8);
    byte[] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);
    return Base64.getEncoder().encodeToString(encryptedMessageBytes);
}

The Problem

Please note - this is still in a testing scenario.

  • The aim is to decrypt within a unit-test of the SpringBoot Service.

The PrivateKey

Although the PublicKey could be parsed the described way, the parsing of the PrivateKey is refused.

java.security.InvalidKeyException: RSA keys must be at least 512 bits long

As the keys had been initialized using a KeySize of 4096, the error message is irritating. Nevertheless, I cannot find the mistake so far.

Solution space

Multiple approaches may be suitable to solve this issue:

  1. Execute the PrivateKey creation using the bouncycastle dependency.
  2. Conduct the PrivateKey creation using the javax-crypto dependency.
  3. Exporting the PublicKey generated in iOS (Swift) in a PKCS-Format
  • Solution (3) would erase the need of the bouncycastel-dependency and makes the need of THIS additional test obsolete :)

All approaches are fine ... .. . ;-)

Finally

Thanks for reading & sharing - any help is appreciated :)

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
d.braun1991
  • 151
  • 1
  • 13

0 Answers0