I recently started writing a version of Android for an online game written in Java. However, I am facing inconsistency with encryption. The java application works fine - it is read in a public key from a file, encrypts the text and sends it to the server, where it is decrypted correctly using the private key. Everything works on android (and works through the same code), but the server has a BadPaddingException that tries to decrypt the message. I have included all the relevant code and a step-by-step sequence of events below:
The first thing that happens when you connect to the server is the consent of the symmetric key. This is generated on the client, thus:
SecretKey symmetricKey = null; try { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); symmetricKey = keyGen.generateKey(); } catch (Throwable t) { Debug.stackTrace(t, "Failed to generate symmetric key."); } return symmetricKey;
Then it is converted to the Base64 string:
byte[] keyBytes = secretKey.getEncoded(); return base64Interface.encode(keyBytes);
And encrypted using the public key:
public static String encrypt(String messageString, Key key) { String encryptedString = null; try { byte[] messageBytes = messageString.getBytes(); String algorithm = key.getAlgorithm() Cipher cipher = Cipher.getInstance(algorithm); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] cipherData = cipher.doFinal(messageBytes); encryptedString = base64Interface.encode(cipherData); //Strip out any newline characters encryptedString = encryptedString.replaceAll("\n", ""); encryptedString = encryptedString.replaceAll("\r", ""); } catch (Throwable t) { Debug.append("Caught " + t + " trying to encrypt message: " + messageString); } return encryptedString; }
In this form, it is passed to the server, which uses the private key to decrypt the message and restore the SecretKey object:
public static String decrypt(String encryptedMessage, Key key) { String messageString = null; try { byte[] cipherData = base64Interface.decode(encryptedMessage); String algorithm = key.getAlgorithm(); Cipher cipher = Cipher.getInstance(algorithm); cipher.init(Cipher.DECRYPT_MODE, key); byte[] messageBytes = cipher.doFinal(cipherData); messageString = new String(messageBytes); } catch (Throwable t) { Debug.append("Caught " + t + " trying to decrypt message: " + encryptedMessage, failedDecryptionLogging); } return messageString; }
However, whenever I do this from a message passed from an Android application, the doFinal line throws the following exception:
11/07 12:55:55.975 javax.crypto.BadPaddingException: Decryption error at sun.security.rsa.RSAPadding.unpadV15(Unknown Source) at sun.security.rsa.RSAPadding.unpad(Unknown Source) at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:354) at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:380) at javax.crypto.Cipher.doFinal(Cipher.java:2121) at util.EncryptionUtil.decrypt(EncryptionUtil.java:85) at server.MessageHandlerRunnable.handleUnencryptedMessage(MessageHandlerRunnable.java:226) at server.MessageHandlerRunnable.getResponse(MessageHandlerRunnable.java:188) at server.MessageHandlerRunnable.run(MessageHandlerRunnable.java:85) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source)
My first thought was that the problem with Base64's encoding / decoding was that it was the only bit of code that was different from the versions of Android and desktop computers. However, I did some testing and checked that they are consistent and that my server code can restore the source text using either the encoding method.
My next thought was that the version of Android must somehow use the wrong public key. This is generated when launched from a file that is common to both platforms using the following code:
public static void generatePublicKey() { InputStream in = null; ObjectInputStream oin = null; try { in = KeyGeneratorUtil.class.getResourceAsStream("/assets/public.key"); oin = new ObjectInputStream(new BufferedInputStream(in)); BigInteger m = (BigInteger) oin.readObject(); BigInteger e = (BigInteger) oin.readObject(); RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e); KeyFactory fact = KeyFactory.getInstance("RSA"); MessageUtil.publicKey = fact.generatePublic(keySpec); } catch (Throwable e) { Debug.stackTrace(e, "Unable to read public key - won't be able to communicate with Server."); } finally { if (in != null) { try {in.close();} catch (Throwable t) {} } if (oin != null) { try {oin.close();} catch (Throwable t) {} } } }
When I look at the key on two platforms (using toString ()), I see the following (I trimmed the modules):
Desktop:
RS RSS public key, 1024 bit module: 11920225567195913955197820411061866681846853580 ... public indicator: 65537
Android:
OpenSSLRSAPublicKey {modulus = a9bfe8d8a199fc6a ..., publicExponent = 10001}
At first glance, they seem completely different, but I am now convinced that they are equivalent to one expression in decimal (Desktop) and another in hex (Android). 10001 in hexadecimal equivalent is 65537 in decimal, and including the hex module in an online converter gives a number that at least starts with the correct digits for the decimal module. Why am I seeing a BadPaddingException?
One great thing worth noting is that it seems to be the same issue that was raised on this issue about a year ago:
RSA on Android is different from PC
However, no solution was presented, and I thought it was worth asking a new question, where could I provide all the information available to me for me.