2

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?

2 Answers2

4

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);
    }
}
Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
3

Michael Fehr's answer shows how to load a public key in PKCS#1 format without third party libraries. If the latter is a requirement, then that's the way you have to go.
Otherwise, if you use BouncyCastle, there are much less elaborate solutions that might also be considered and are therefore worth mentioning (although the given answer is already accepted):

The following method expects a public key in PKCS#1 format, PEM encoded and loads it into a java.security.PublicKey instance:

import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import java.security.PublicKey;
import java.io.StringReader;
...

private static PublicKey getPublicKey(String publicKeyc) throws Exception {
    PEMParser pemParser = new PEMParser(new StringReader(publicKeyc));
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter();
    SubjectPublicKeyInfo subjectPublicKeyInfo = (SubjectPublicKeyInfo)pemParser.readObject();
    PublicKey publicKey = jcaPEMKeyConverter.getPublicKey(subjectPublicKeyInfo);
    return publicKey;
}

Another similarly compact implementation can be found here.

To use BouncyCastle in Android, a dependency to BouncyCastle corresponding to the API level used must be referenced in the dependencies section of the build.gradle file. I used API Level 28 (Pie) and referenced the following dependency (assuming Android Studio):

implementation 'org.bouncycastle:bcpkix-jdk15on:1.65'
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • The Bouncy Castle implementation is much more elegant, fully agreed. Personally I try to solve problems with "build in" sources, but sometimes I cannot avoid it, and then I'm using e.g. BC. @Topaco & Board: have a nice x-mas time and a better 2021. – Michael Fehr Dec 21 '20 at 21:58
  • @MichaelFehr - When does the advantage offered by a third-party library outweigh an additional dependency (assuming the requirements allow for one in the first place)? Sometimes it's not so easy to decide. In this particular case, I would prefer BouncyCastle, simply because of the much more compact implementation. But that's a matter of opinion, another developer may decide otherwise. Merry Xmas as well and a happy new year. – Topaco Dec 22 '20 at 04:34