Search code examples
javaandroidencryptionrsapem

Android Java - Encrypting String using a RSA public key .PEM


I have an RSA public key certificate. I can use the file that has a .PEM extension or simply use it as a String which has the following format:

-----BEGIN RSA PUBLIC KEY-----

{KEY}

-----END RSA PUBLIC KEY-----

I am trying to use this key in order to send an encrypted JSON to the server. I tried numerous solutions from other related stack overflow questions, but none of the answers didn't for me. This answer seems to makes sense https://stackoverflow.com/a/43534042, but something doesn't work properly, maybe it's because X509EncodedKeySpec expects DER encoded data not PEM according to one of the comments. But in this case what should i use for PEM encoded data?


Solution

  • As already commented by @Topaco your RSA Public Key is in PEM encoding but in PKCS#1 format and not in PKCS#8 format that is readable by Java "out of the box".

    The following solution was provided by @Maarten Bodewes here on SO (https://stackoverflow.com/a/54246646/8166854) will do the job to read and transform it to a (Java) usable RSAPublicKey.

    The solution is running on my OpenJdk11, if your'e using "Android Java" you may have to change the Base64-calls. There are no external libraries like Bouncy Castle necessary. Please obey the notes from Maarten regarding key lengths.

    The simple output:

    Load RSA PKCS#1 Public Keys
    pkcs1PublicKey: Sun RSA public key, 2048 bits
      params: null
      modulus: 30333480050529072539152474433261825229175303911986187056546130987160889422922632165228273249976997833741424393377152058709551313162877595353675051556949998681388601725684016724167050111037861889500002806879899578986908702627237884089998121288607696752162223715667435607286689842713475938751449494999920670300421827737208147069624343973533326291094315256948284968840679921633097541211738122424891429452073949806872319418453594822983237338545978675594260211082913078702997218079517998196340177653632261614031770091082266225991043014081642881957716572923856737534043425399435601282335538921977379429228634484095086075971
      public exponent: 65537
    

    code:

    import java.io.IOException;
    import java.security.GeneralSecurityException;
    import java.security.KeyFactory;
    import java.security.NoSuchAlgorithmException;
    import java.security.interfaces.RSAPublicKey;
    import java.security.spec.InvalidKeySpecException;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.Base64;
    
    public class LoadPkcs1PublicKeyPemSo {
        // solution from https://stackoverflow.com/a/54246646/8166854  answered Jan 18 '19 at 1:36 Maarten Bodewes
        private static final int SEQUENCE_TAG = 0x30;
        private static final int BIT_STRING_TAG = 0x03;
        private static final byte[] NO_UNUSED_BITS = new byte[] { 0x00 };
        private static final byte[] RSA_ALGORITHM_IDENTIFIER_SEQUENCE =
                {(byte) 0x30, (byte) 0x0d,
                        (byte) 0x06, (byte) 0x09, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, (byte) 0x01,
                        (byte) 0x05, (byte) 0x00};
    
        public static void main(String[] args) throws GeneralSecurityException, IOException {
            System.out.println("Load RSA PKCS#1 Public Keys");
    
            String rsaPublicKeyPem = "-----BEGIN RSA PUBLIC KEY-----\n" +
                    "MIIBCgKCAQEA8EmWJUZ/Osz4vXtUU2S+0M4BP9+s423gjMjoX+qP1iCnlcRcFWxt\n" +
                    "hQGN2CWSMZwR/vY9V0un/nsIxhZSWOH9iKzqUtZD4jt35jqOTeJ3PCSr48JirVDN\n" +
                    "Let7hRT37Ovfu5iieMN7ZNpkjeIG/CfT/QQl7R+kO/EnTmL3QjLKQNV/HhEbHS2/\n" +
                    "44x7PPoHqSqkOvl8GW0qtL39gTLWgAe801/w5PmcQ38CKG0oT2gdJmJqIxNmAEHk\n" +
                    "atYGHcMDtXRBpOhOSdraFj6SmPyHEmLBishaq7Jm8NPPNK9QcEQ3q+ERa5M6eM72\n" +
                    "PpF93g2p5cjKgyzzfoIV09Zb/LJ2aW2gQwIDAQAB\n" +
                    "-----END RSA PUBLIC KEY-----";
    
            RSAPublicKey pkcs1PublicKey = getPkcs1PublicKeyFromString(rsaPublicKeyPem);
            System.out.println("pkcs1PublicKey: " + pkcs1PublicKey);
        }
    
        public static RSAPublicKey getPkcs1PublicKeyFromString(String key) throws GeneralSecurityException {
            String publicKeyPEM = key;
            publicKeyPEM = publicKeyPEM.replace("-----BEGIN RSA PUBLIC KEY-----", "");
            publicKeyPEM = publicKeyPEM.replace("-----END RSA PUBLIC KEY-----", "");
            publicKeyPEM = publicKeyPEM.replaceAll("[\\r\\n]+", "");
            byte[] pkcs1PublicKeyEncoding = Base64.getDecoder().decode(publicKeyPEM);
            return decodePKCS1PublicKey(pkcs1PublicKeyEncoding);
        }
    
    /*
    solution from https://stackoverflow.com/a/54246646/8166854  answered Jan 18 '19 at 1:36 Maarten Bodewes
    The following code turns a PKCS#1 encoded public key into a SubjectPublicKeyInfo encoded public key,
    which is the public key encoding accepted by the RSA KeyFactory using X509EncodedKeySpec -
    as SubjectPublicKeyInfo is defined in the X.509 specifications.
    
    Basically it is a low level DER encoding scheme which
        wraps the PKCS#1 encoded key into a bit string (tag 0x03, and a encoding for the number of unused
        bits, a byte valued 0x00);
        adds the RSA algorithm identifier sequence (the RSA OID + a null parameter) in front -
        pre-encoded as byte array constant;
        and finally puts both of those into a sequence (tag 0x30).
    
    No libraries are used. Actually, for createSubjectPublicKeyInfoEncoding, no import statements are even required.
    
    Notes:
    
        NoSuchAlgorithmException should probably be caught and put into a RuntimeException;
        the private method createDERLengthEncoding should probably not accept negative sizes.
        Larger keys have not been tested, please validate createDERLengthEncoding for those -
        I presume it works, but better be safe than sorry.
    */
    
        public static RSAPublicKey decodePKCS1PublicKey(byte[] pkcs1PublicKeyEncoding)
                throws NoSuchAlgorithmException, InvalidKeySpecException
        {
            byte[] subjectPublicKeyInfo2 = createSubjectPublicKeyInfoEncoding(pkcs1PublicKeyEncoding);
            KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA");
            RSAPublicKey generatePublic = (RSAPublicKey) rsaKeyFactory.generatePublic(new X509EncodedKeySpec(subjectPublicKeyInfo2));
            return generatePublic;
        }
    
        public static byte[] createSubjectPublicKeyInfoEncoding(byte[] pkcs1PublicKeyEncoding)
        {
            byte[] subjectPublicKeyBitString = createDEREncoding(BIT_STRING_TAG, concat(NO_UNUSED_BITS, pkcs1PublicKeyEncoding));
            byte[] subjectPublicKeyInfoValue = concat(RSA_ALGORITHM_IDENTIFIER_SEQUENCE, subjectPublicKeyBitString);
            byte[] subjectPublicKeyInfoSequence = createDEREncoding(SEQUENCE_TAG, subjectPublicKeyInfoValue);
            return subjectPublicKeyInfoSequence;
        }
    
        private static byte[] concat(byte[] ... bas)
        {
            int len = 0;
            for (int i = 0; i < bas.length; i++)
            {
                len += bas[i].length;
            }
            byte[] buf = new byte[len];
            int off = 0;
            for (int i = 0; i < bas.length; i++)
            {
                System.arraycopy(bas[i], 0, buf, off, bas[i].length);
                off += bas[i].length;
            }
            return buf;
        }
    
        private static byte[] createDEREncoding(int tag, byte[] value)
        {
            if (tag < 0 || tag >= 0xFF)
            {
                throw new IllegalArgumentException("Currently only single byte tags supported");
            }
            byte[] lengthEncoding = createDERLengthEncoding(value.length);
            int size = 1 + lengthEncoding.length + value.length;
            byte[] derEncodingBuf = new byte[size];
            int off = 0;
            derEncodingBuf[off++] = (byte) tag;
            System.arraycopy(lengthEncoding, 0, derEncodingBuf, off, lengthEncoding.length);
            off += lengthEncoding.length;
            System.arraycopy(value, 0, derEncodingBuf, off, value.length);
            return derEncodingBuf;
        }
    
        private static byte[] createDERLengthEncoding(int size)
        {
            if (size <= 0x7F)
            {
                // single byte length encoding
                return new byte[] { (byte) size };
            }
            else if (size <= 0xFF)
            {
                // double byte length encoding
                return new byte[] { (byte) 0x81, (byte) size };
            }
            else if (size <= 0xFFFF)
            {
                // triple byte length encoding
                return new byte[] { (byte) 0x82, (byte) (size >> Byte.SIZE), (byte) size };
            }
            throw new IllegalArgumentException("size too large, only up to 64KiB length encoding supported: " + size);
        }
    }