rsa public key interoperability between ruby and android

Jan 16, 2012

I recently worked on a project where sensitive information needed to be sent from an Android app to a Ruby-based server. Our solution was to use asymmetric encryption (the actual implementation was more involved, but for the sake of simplicity, let’s just focus on asymmetric encryption, namely RSA). Using an RSA key pair generated by the Ruby-based server, the Android app encrypted sensitive information using the RSA public key and the server decrypted the sensitive data using the RSA private key. This sounds straightforward, but what I’m going to talk about here is an issue when using an RSA public key generated from pre-Ruby 1.9.3 in an Android app and several solutions to work around the issue.

TL;DR

Solutions to this problem:

RSA Public Key Encoded Format Problem in pre-Ruby 1.9.3

The Ruby-based server generates the RSA key pair and stores the RSA private key in its database. If you know RSA, you know if you have the private key, you can easily generate its public key counterpart, but not vice versa. Using the public key given by the server, I attempted to load it in the Android app.

In Ruby, you can simply load up a public key by passing it to OpenSSL::PKey::RSA::new.


require 'openssl'
require 'base64'

public_key = "MIIBCgKCAQEA20O377QEiZvPsj14LKl2xO23iirJB5WDTVjeab1cIOJu1vbV+Pdwl1Bov8m896ZG4K0S/qvfJcdHLovr2WJ+o2maK1XZCNy8lA" +
  "zIPzZrj/yDZAB2GSjR3in1lQRQPtWjIOdB8Cy2FGybEstIkpf8MD3XMWp5g8BtdOv43ekjBuTiGGLlPRG0+IiazjHlWjyl6DU9x9m2Jxks0H6YZud6zf4s9Q6" +
  "9vPUYgOZXWs7IghxqrVGE5mWxoRudsDFhLYP706+IrSxGOf5fE0/8fjtzj/eJayCLmkUWq/xsts5tBAbwsX5xKdk8iD0OU2qOEbVuiYmehEiJnvO2vyd+t76C" +
  "xwIDAQAB"

rsa_public_key = OpenSSL::PKey::RSA.new(Base64.decode64(public_key))

Unfortunately, in Android, it’s a bit more complex (oh, Java!). You have to first get an instance of KeyFactory, create an X509EncodedKeySpec, pass in the public key string, then generate the public key using that key spec.


public PublicKey getPublicKey() {
    try {
        String publicKey = "MIIBCgKCAQEA20O377QEiZvPsj14LKl2xO23iirJB5WDTVjeab1cIOJu1vbV+Pdwl1Bov8m896ZG4K0S/qvfJcdHLovr2WJ+" +
            "o2maK1XZCNy8lAzIPzZrj/yDZAB2GSjR3in1lQRQPtWjIOdB8Cy2FGybEstIkpf8MD3XMWp5g8BtdOv43ekjBuTiGGLlPRG0+IiazjHlWjyl6DU" +
            "9x9m2Jxks0H6YZud6zf4s9Q69vPUYgOZXWs7IghxqrVGE5mWxoRudsDFhLYP706+IrSxGOf5fE0/8fjtzj/eJayCLmkUWq/xsts5tBAbwsX5xKd" +
            "k8iD0OU2qOEbVuiYmehEiJnvO2vyd+t76CxwIDAQAB";
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec encodedPubKeySpec = new X509EncodedKeySpec(Base64.decode(publicKey, Base64.NO_WRAP));
        return keyFactory.generatePublic(encodedPubKeySpec);
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    } catch (InvalidKeySpecException e) {
      e.printStackTrace();
    }
    return null;
}

Unfortunately, this raises an InvalidKeySpecException. Countless Google queries resulting in many hours of yak shaving, led me to dead ends. It was when I kicked off a Ruby 1.9.3 irb instance that showed me the light.

RSA Encoded Format in Ruby 1.9.3

In a Ruby 1.9.3 irb instance, I loaded the same public key I generated from Ruby 1.8.7 and to my surprise, noticed the public key changed in the front of my eyes.


require 'openssl'
require 'base64'

public_key = "MIIBCgKCAQEA20O377QEiZvPsj14LKl2xO23iirJB5WDTVjeab1cIOJu1vbV+Pdwl1Bov8m896ZG4K0S/qvfJcdHLovr2WJ+o2maK1XZCNy8lA" +
  "zIPzZrj/yDZAB2GSjR3in1lQRQPtWjIOdB8Cy2FGybEstIkpf8MD3XMWp5g8BtdOv43ekjBuTiGGLlPRG0+IiazjHlWjyl6DU9x9m2Jxks0H6YZud6zf4s9Q6" +
  "9vPUYgOZXWs7IghxqrVGE5mWxoRudsDFhLYP706+IrSxGOf5fE0/8fjtzj/eJayCLmkUWq/xsts5tBAbwsX5xKdk8iD0OU2qOEbVuiYmehEiJnvO2vyd+t76C" +
  "xwIDAQAB"

rsa_public_key = OpenSSL::PKey::RSA.new(Base64.decode64(public_key))

# strip newlines and begin/end markers
new_public_key = rsa_public_key.to_s.gsub("\n", "").gsub(/-----(BEGIN|END) PUBLIC KEY-----/, "")
# => "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA20O377QEiZvPsj14LKl2xO23iirJB5WDTVjeab1cIOJu1vbV+Pdwl1Bov8m896ZG4K0S/qvfJcdHLovr2WJ+o2maK1XZCNy8lAzIPzZrj/yDZAB2GSjR3in1lQRQPtWjIOdB8Cy2FGybEstIkpf8MD3XMWp5g8BtdOv43ekjBuTiGGLlPRG0+IiazjHlWjyl6DU9x9m2Jxks0H6YZud6zf4s9Q69vPUYgOZXWs7IghxqrVGE5mWxoRudsDFhLYP706+IrSxGOf5fE0/8fjtzj/eJayCLmkUWq/xsts5tBAbwsX5xKdk8iD0OU2qOEbVuiYmehEiJnvO2vyd+t76CxwIDAQAB"

new_public_key == public_key
# => false

I tried to see if Ruby 1.9.2 produced the same results; the public key was the same as 1.8.7. I then tried generating a new public key in Ruby 1.9.3, then initialize that key in another OpenSSL::PKey::RSA instance in 1.9.3, but the key was identical. This led me to believe that something changed in Ruby 1.9.3 that is not in Ruby 1.8.7/Ruby 1.9.2. I searched on Google again for clues and fortunately found something relevant. Martin Boßlet’s filed a bug report for Ruby 1.9.3; in the report, he describes how the encoded format for RSA public keys was not the default format used by OpenSSL, but rather the encoding format specified by PKCS#1. The fix shows Martin changing PEM_read_bio_RSAPublicKey to PEM_read_bio_RSA_PUBKEY. Perusing in the OpenSSL documentation shows what Martin was describing in the bug report: RSAPublicKey encodes the public key using the PKCS#1 RSAPublicKey structure rather than the SubjectPublicKeyInfo structure. The former structure is not compatible with the crypto library available in Android.

The fix has been applied to Ruby-1.9.3-p0, but the problem is still present for all Ruby versions prior. This means the Ruby-based server is still affected and the path to upgrading to 1.9.3 for us was not an option. I needed to approach the problem differently and reached out to Martin for some assistance. He was extremely helpful and came up with this solution.

Solution #1: Encode pre-Ruby 1.9.3 RSA public keys using the X.509 format (gist)


require 'openssl'
require 'base64'

rsa = OpenSSL::PKey::RSA.new(2048)
modulus = rsa.n
exponent = rsa.e

oid = OpenSSL::ASN1::ObjectId.new("rsaEncryption")
alg_id = OpenSSL::ASN1::Sequence.new([oid, OpenSSL::ASN1::Null.new(nil)])
ary = [OpenSSL::ASN1::Integer.new(modulus), OpenSSL::ASN1::Integer.new(exponent)]
pub_key = OpenSSL::ASN1::Sequence.new(ary)
enc_pk = OpenSSL::ASN1::BitString.new(pub_key.to_der)
subject_pk_info = OpenSSL::ASN1::Sequence.new([alg_id, enc_pk])

base64 = Base64.encode64(subject_pk_info.to_der)

#This is the equivalent to the X.509 encoding used in >= 1.9.3
pem = "-----BEGIN PUBLIC KEY-----\n#{base64}-----END PUBLIC KEY-----"

Martin basically wrote the X.509 encoding format from scratch. This would have worked for us, but unfortunately, we have already issued RSA public keys to customers and needed to support the PKCS#1 encoded format public keys.

I googled some more and found a solution that extracts the exponent and modulus from the public key. With the exponent and modulus, I can create a PublicKey object.

Solution #2: Full Version of BouncyCastle and RSAPublicKeyStructure (gist)

This solution requires classes that are not available in the Android Standard Library. A Google result provides details about the problem with the Android Standard Library’s crippled version of BouncyCastle. To remedy this problem, grab the full version of BouncyCastle and bring in only the required classes to extract the exponent and modulus of the public key string.

After adding BouncyCastle classes, I created a static method that reads pre-Ruby 1.9.3 public keys, extracts the exponent and modulus, and returns a PublicKey object:


import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.x509.RSAPublicKeyStructure;
import org.bouncycastle.util.encoders.Base64;

public class RSAUtil {
    static public PublicKey publicKey(String publicKeyString) {
        try {
            byte[] decodedPublicKey = Base64.decode(publicKeyString);
            ASN1InputStream in = new ASN1InputStream(decodedPublicKey);
            DERObject obj = in.readObject();
            RSAPublicKeyStructure keyStruct = RSAPublicKeyStructure.getInstance(obj);
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(keyStruct.getModulus(), keyStruct.getPublicExponent());
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(keySpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Conclusion

This is definitely a unique cryptography issue between Ruby and Android. As far as I know, the Ruby team has no plans to backport the Ruby 1.9.3 fix into prior Ruby versions, but the solutions I have presented should be sufficient for your needs to make public keys interoperable between Ruby and Android. Feel free to comment if you have any questions or different solutions to this problem.