Search code examples
androiddelphirsajavax.cryptolockbox-2

How to configure Android RSA key generation (or key use) so that it works like Delphi TurboPower Lockbox 2 RSA key generation/use?


I have the following code for Delphi 10.2 TurboPower LockBox 2 RSA keys generation and their represenation as some string:

//Object properties 
object LbRSA1024: TLbRSA
  PrimeTestIterations = 20
  KeySize = aks1024
  Left = 416
  Top = 248
end

//Key generation - so simple!
LbRSA1024.GenerateKeyPair;

//Getting generated key as string 
function TMainForm.GetPublicKey1024AsString: string;
var
  str1, str2: TStringStream;
begin
  Result:='';
  if (LbRSA1024.PublicKey.Exponent.Int.dwUsed = 0)
    or (LbRSA1024.PublicKey.Modulus.Int.dwUsed = 0) then
    exit;
  str1:= TStringStream.Create('');
  str2:= TStringStream.Create('');
  try
    LbRSA1024.PublicKey.StoreToStream(str1);
    str1.Position:=0;
    //LbEncodeBase64(str1,str2);
    TLbBase64.LbEncodeBase64(str1,str2);
    Result:=str2.DataString;
  finally
    str1.Free;
    str2.Free;
  end;
end;

I got public key as ~200 character string (e.g. ...dh2dMTy/ab...) and I assigned his string to Android kotlin variable publicKeyString and tried to encrypt some other string with this public key generated from Delphi. Android Kotlin code is:

    val publicKeyBytes: ByteArray = Base64.decode(publicKeyString, Base64.DEFAULT)
    val X509PublicKey: X509EncodedKeySpec = X509EncodedKeySpec(publicKeyBytes)
    val kf: KeyFactory = KeyFactory.getInstance("RSA")
    val publicKey: PublicKey = kf.generatePublic(X509PublicKey)
    val cipher: Cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
    cipher.init(Cipher.ENCRYPT_MODE, publicKey)
    val bytes = cipher.doFinal(s.toByteArray())
    val result: String = String(bytes, Charsets.UTF_8)

Generally it is not working - the key is not accepted and the error messages are (I experimented with some variations of the code above):

Exception in thread "main" java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence
 at sun.security.rsa.RSAKeyFactory.engineGeneratePublic (:-1)
...
Exception in thread "main" java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
 at sun.security.rsa.RSAKeyFactory.engineGeneratePublic (:-1) 
 at java.security.KeyFactory.generatePublic (:-1) 

So, it may be possible that Delphi LockBox and Android/javax RSA has different key formats. So - I tried to do 2 things. First - I checked the code for Delphi key generation - specificaly - LbRsa.pas class procedure TRSA.GenerateRSAKeysEx(var PrivateKey, PublicKey : TLbRSAKey; KeySize : TLbAsymKeySize; PrimeTestIterations : Byte; Callback : TLbRSACallback); But this code is completely generic - large integers are generated and stored as the member variables and the then streamed to the strings with the code I have already provided above.

Then I tried to generate RSA keys in Android/javax and check whether they are the same as LockBox generated keys. I used the following code:

    val REG_KEY: String = "REG_KEY"
    val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA /*, ANDROID_KEYSTORE */)
    val builder = KeyGenParameterSpec.Builder(REG_KEY,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setKeySize(1024)
        .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
        //.setUserAuthenticationRequired(true)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
    generator.initialize(builder.build())
    val keys = generator.generateKeyPair()

    Log.i( "Cryptic", keys.private.toString())
    Log.i( "Cryptic", keys.public.toString())

    Log.i( "Cryptic", keys.private.encoded.contentToString())
    Log.i( "Cryptic", keys.public.encoded.contentToString())

    Log.i( "Cryptic", keys.private.encoded.toString(Charsets.UTF_8))
    Log.i( "Cryptic", keys.public.encoded.toString(Charsets.UTF_8))

I em experiencing error message, because the private key is null, but the public key is generated:

java.lang.NullPointerException: keys.private.encoded must not be null
        at com.batsoft.stockmobile.service.Cryptography.encryptString(Cryptography.kt:35)

I am still seeking to convert it to the string to look like LockBox generated key - just for comparison.

But at the present stage I am already confused - why I have to provide chaining mode and padding scheme for the key generation? My understanding is that keys are just encoded large integer. And the chaining mode, padding scheme are used for the encryption/decryption only? Of course, I need to provide key size, this is understandable.

So - my goal is to configure Android/javax RSA key generation and RSA key use so that it conform exactly to the RSA key generation and use in Delphi 10.2 LockBox 2. My aim is to use LockBox generated keys in the Android program. I have described the number of paths I have already taken but still I have not managed to generate javax keys in the same format as Delphi keys. As I don't know the exact configuration of they keys in LockBox (I guess - there is none, except for the key size) I can not configure my encryption/decryption on Android/javax as well.

How to achieve this conformity by changing Android/javax code?

Additional info: Delphi LbAsym.pas procedure TLbAsymmetricKey.StoreToStream(aStream : TStream); is very important, because it saves the key-integer into stream. In the case when passphrase is not used, the code is very straingtforward:

aStream.Write(KeyBuf, Len);

and the comment in the Pascal code reads as:

save key to ASN.1 format stream (encrypt if necessary)

So, maybe my question my be reformulated - how to save Android/javax generated keys in ASN.1 format or to read keys from ASN.1 format?

Additional Info 2: I am looking for the way to save Java-generated keys as PEM strings, and this can be done by the Android Kotlin code:

    val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA /*, ANDROID_KEYSTORE */)
    val builder = KeyGenParameterSpec.Builder(REG_KEY,
        KeyProperties.PURPOSE_ENCRYPT and KeyProperties.PURPOSE_DECRYPT)
        .setKeySize(1024)
        .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
        //.setUserAuthenticationRequired(true)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
    generator.initialize(builder.build())
    val keys = generator.generateKeyPair()

    //https://stackoverflow.com/questions/25129822/export-rsa-public-key-to-pem-string-using-java
    val writer = StringWriter()
    val pemWriter = PemWriter(writer)
    pemWriter.writeObject(PemObject("PUBLIC KEY", keys.public.encoded))
    pemWriter.flush()
    pemWriter.close()

    Log.i( "Cryptic", writer.toString())

Note, this requires to add gradle dependencies:

implementation 'org.bouncycastle:bcpkix-jdk15to18:1.68'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.68'

In order to use Delphi-generated keys (apparently LockBox 2 generates and saves them as PEM strings) I need to parse PEM strings and assign them as keys javax Cipher.

Additional Info 3: This is strange. Java generated string (from Additional Info 2) is a about 25 characters longer gan Delphi generated (based64 encoded) string (of course, I have removed initial and trailing strings that somes with PEM file and are just constants like '===PUBLIC KEY===') and it can be used perfectly as publicKeyString in my initial code - the encrytion with such public key works perfectly. Delphi public key string is some 25 characters shorter and does not work.

So - I have figure out what happens inside every step and tried to establish parallel steps, but ultimately I have failed to find solution.

Additional Info 4: Here are base64 encoded public keys:

Generated from Delphi:

MIGIAoGBALtEMVXxHBWzBx/AzO/aOHrYEQZB3VlqYBvqX/SHES7ehERXaCbUO5aEwyZcDrdh2dMTy/abNDaFJK4bEqghpC6yvCNvnTqjAz+bsD9UqS0w5CUh3KHwqhPv+HFGcF7rAuU9uoJcWXbTC9tUBEG7rdmdmMatIgL1Y4ebOACQHn1xAgIlKg==

Generated from Android Kotlin/java:

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuQi7gMZwWL1iEhNVgdu23S/rYYhtntXQlfVVBjcGiSE8EXzjjnZHxcYHcIszV0F6F20msGK8MFernJpWg8k7J3GLH4TYkQwEEy6jWnRdEB3uqQWFCNQ/CflCHtq1o1iSS0qmXcHQuI7zZ0cHd5FNDg4Bl/DveftEje9yTgUXN3wIDAQAB

I am not sure, but maybe there is some online service that can base64 decode those strings and then extract big integer from them according to some scheme and detecte the format.


Solution

  • As stated in the comments, the Delphi code generates a public key in PKCS#1 format, while the Kotlin code expects a key in X.509/SPKI format.
    With BouncyCastle it is possible for the Kotlin code to import a PKCS#1 public key. This requires the classes PEMParser and JcaPEMKeyConverter.

    Example:

    import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
    import org.bouncycastle.jce.provider.BouncyCastleProvider
    import org.bouncycastle.openssl.PEMParser
    import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
    import javax.crypto.Cipher
    import java.security.PublicKey
    import java.io.FileReader
    import java.util.Base64
    
    ...
    
    val inputFile: String = "<path to PKCS#1 PEM file>"
    
    // Key import
    var publicKey: PublicKey? = null
    FileReader(inputFile).use { fileReader ->
        PEMParser(fileReader).use { pemParser ->
            val spki: SubjectPublicKeyInfo = pemParser.readObject() as SubjectPublicKeyInfo
            val converter = JcaPEMKeyConverter()
            converter.setProvider(BouncyCastleProvider())
            publicKey = converter.getPublicKey(spki) 
        }
    }
    
    // Encryption
    val cipher: Cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
    cipher.init(Cipher.ENCRYPT_MODE, publicKey)
    val ciphertext: ByteArray = cipher.doFinal("The quick brown fox jumps over the lazy dog".toByteArray())
    val ciphertextB64: String = Base64.getEncoder().encodeToString(ciphertext);
    
    println(ciphertextB64)
    

    The code imports a PEM encoded public key in PKCS#1 format. The PEM encoding includes a format-specific header and footer, and in the body, there are line breaks after every 64 characters. The PEM encoded Delphi key is:

    -----BEGIN RSA PUBLIC KEY-----
    MIGIAoGBALtEMVXxHBWzBx/AzO/aOHrYEQZB3VlqYBvqX/SHES7ehERXaCbUO5aE
    wyZcDrdh2dMTy/abNDaFJK4bEqghpC6yvCNvnTqjAz+bsD9UqS0w5CUh3KHwqhPv
    +HFGcF7rAuU9uoJcWXbTC9tUBEG7rdmdmMatIgL1Y4ebOACQHn1xAgIlKg==
    -----END RSA PUBLIC KEY-----
    

    Unfortunately the import of your Delphi key does not work. There is a provider dependent error message displayed, e.g. in the case of the BouncyCastleProvider:

    PEMException: unable to convert key pair: encoded key spec not recognized: RSA publicExponent is even
    

    Indeed, the key generated by the Delphi code has an even public exponent with the value 9514 (0x252A):

    0:d=0  hl=3 l= 136 cons: SEQUENCE
        3:d=1  hl=3 l= 129 prim: INTEGER  :BB443155F11C15B3071FC0CCEFDA387AD8110641DD596A601BEA5FF487112EDE8444576826D43B9684C3265C0EB761D9D313CBF69B34368524AE1B12A821A42EB2BC236F9D3AA3033F9BB03F54A92D30E42521DCA1F0AA13EFF87146705EEB02E53DBA825C5976D30BDB540441BBADD99D98C6AD2202F563879B3800901E7D71
      135:d=1  hl=2 l=   2 prim: INTEGER  :252A
    

    This should not be the case (φ(n) or λ(n) and e would then not be coprime), s. here. You should therefore check the key generation in the Delphi code.


    Another issue is in the line:

    val result: String = String(bytes, Charsets.UTF_8)
    

    The UTF-8 decoding of a ciphertext (which generally contains non-UTF-8 compliant byte sequences) corrupts this ciphertext.
    If a ciphertext is to be converted to a string, a binary-to-text encoding such as Base64 must be applied.