Search code examples
javaencryptionrsa

How to encrypt data with RSA PublicKey in java?


I have a json response with parameters:

 "jwk":{
    "kty": "string",
    "n": "string",
    "e": "string",
    "alg": "string",
    "ext": bool
  },
 "pem": "--BEGIN RSA PUBLIC KEY--...SOMEKEY...--END RSA PUBLIC KEY--",
 "alg": {
    "name": "string",
    "hash": {
        "name": "string
    }
 }
}

I need to encrypt data with RSA Public key. I'm trying to get "pem", clear it and create public key

String clearKey = rsaKey.replace("--BEGIN RSA PUBLIC KEY--", "").replace("--END RSA PUBLIC KEY--", "").replace("\\s","");
byte[] prByte = Base64.getDecoder().decode(clearKey);
KeyFactory keyFactory = KeyFactory.getInstanse("RSA");
keyFactory.generatePrivate(new PKCS8EEncodedKeySpec(prByte));

but on this step I have an error

:java.security.InvalidKeyException: IOException: algid parse error not a sequence

this code followed by error too

byte[] prByte = Base64.getDecoder().decode(clearKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(prBytes);
PublicKey publicKey = keyFactory.generatePublic("keySpec"):

I know that RSA PUBLIC KEY maybe should be mapped on RSAPublicKey but for this i need "n" and "e" attributes?


Solution

  • There is no PEM format for RSA publickey that begins and ends with the strings you show: ALL PEM formats start with FIVE hyphens, uppercase 'BEGIN', a label or type description of one or more uppercase words, FIVE hyphens, AND A LINEBREAK; this is followed by one or more lines of base64, and then a similar line with 'END' instead of 'BEGIN' and optionally the linebreak after the 'END' line may be omitted. See wikipedia and as referenced there RFC 7468.

    If your 'PEM' is actually in the format I (and wikipedia and 7468) describe and not what you posted, then it is the 'traditional' format defined by OpenSSL (originally SSLeay) based on the simple and straightforward ASN.1 format defined by PKCS1. This is not the same format as the X.509 'SPKI' (SubjectPublicKeyInfo) format defined by X.509 and used by Java as X509EncodedKeySpec. (It is even more different from the PKCS8 format used by Java for privatekeys as PKCS8EncodedKeySpec, because a publickey is not a privatekey.) It is however related, because the SPKI format actually is an ASN.1 SEQUENCE containing an AlgorithmIdentifier which (surprise!) identifies the algorithm plus a BIT STRING containing algorithm-specific data and for RSA the algorithm-specific data is PKCS1 RSAPublicKey, so as a result the DER encoding of the SPKI for an RSA key contains as a suffix the PKCS1 data found in an OpenSSL-defined BEGIN/END RSA PUBLIC KEY file.

    There are two ways to directly use this 'traditional' PEM format:

    1. construct the SPKI format for the same key by building the ASN.1 'wrapper', and then feed that to KeyFactory.getInstance("RSA") .generatePublic( new X509EncodedKeySpec( spki )).

    2. use bcpkix+bcprov from https://www.bouncycastle.org whose PEMParser class combined with JcaPEMKeyConverter can directly handle traditional RSA publickey (among many other formats).

    Both of these methods have been asked and answered several times. See:
    How to load RSA public key from String for signature verification in Java?
    Reading a PKCS#1 or SPKI public key in Java without libraries
    Failed to parse RSA publicKey in Java
    RSA should I use X.509 or PKCS #1 (and more linked there)
    How can I transform between the two styles of public key format, one "BEGIN RSA PUBLIC KEY", the other is "BEGIN PUBLIC KEY" (background but not Java)

    The similar problem of converting PKCS1 privatekey to PKCS8 also comes up quite a bit. I wrote an answer for the equivalent of approach 1 (i.e. 'wrap' the PKCS1 privatekey to give PKCS8) in Java: Convert DKIM private key from RSA to DER for JavaMail and two other people did similarly in Getting RSA private key from PEM BASE64 Encoded private key file . Modifying my approach in #23709898 for publickey-to-SPKI instead of privatekey-to-PKCS8 gives:

        // input: b64 contains the base64 _data_ from a BEGIN/END RSA PUBLIC KEY file
        // (i.e. with the BEGIN/END lines and linebreaks removed)
        byte[] oldder = Base64.getDecoder().decode (b64);
    
        // concatenate the mostly-fixed prefix plus the PKCS#1 data 
        final byte[] prefix = {0x30,(byte)0x82,0,0, // SEQUENCE(lenTBD) 
                0x30,0x0d, 6,9,0x2a,(byte)0x86,0x48,(byte)0x86,(byte)0xf7,0x0d,1,1,1, 5,0, // AlgID for rsaEncryption,NULL
                3,(byte)0x82,0,0,0 }; // BITSTRING(lenTBD) + unused
        byte[] newder = new byte [prefix.length + oldder.length];
        System.arraycopy (prefix,0, newder,0, prefix.length);
        System.arraycopy (oldder,0, newder,prefix.length, oldder.length);
        // and patch the (variable) lengths to be correct
        int len = oldder.length+1, loc = prefix.length-3; 
        newder[loc] = (byte)(len>>8); newder[loc+1] = (byte)len;
        len = newder.length-4; loc = 2;
        newder[loc] = (byte)(len>>8); newder[loc+1] = (byte)len;
    
        PublicKey pubkey = KeyFactory.getInstance("RSA") .generatePublic( new X509EncodedKeySpec( newder ));
    

    But since you also have the JWK form you additionally have a third option:

    1. get the jwk n and e values (from whatever JSON classes/representation you are using), convert each to (positive) BigInteger, put them in RSAPublicKeySpec and run that through the KeyFactory:
        BigInteger n = new BigInteger(1, Base64.getUrlDecoder().decode( jwk.n ));
        BigInteger e = new BigInteger(1, Base64.getUrlDecoder().decode( jwk.e ));
        PublicKey pubkey = KeyFactory.getInstance("RSA") .generatePublic( new RSAPublicKeySpec(n,e) ));