Getting Rsa Private Key from Pem Base64 Encoded Private Key File

How to read public/private key files for encryption when they are Base64?

Great thanks to @President James K. Polk who gave me the correct answer in a comment.

Here is the Base64 related code that that worked for me ...

private static String PRIVATE_HEADER = "-----BEGIN PRIVATE KEY-----\n";
private static String PRIVATE_FOOTER = "\n-----END PRIVATE KEY-----\n";
private static String PUBLIC_HEADER = "-----BEGIN PUBLIC KEY-----\n";
private static String PUBLIC_FOOTER = "\n-----END PUBLIC KEY-----\n";

public static void saveKeysToDiskBase64(String name, KeyPair keyPair) {
try {

Base64.Encoder encoder = Base64.getEncoder();

String privateFileName = name+".key";
Writer out = new FileWriter(privateFileName);
out.write(PRIVATE_HEADER);
out.write(encoder.encodeToString(keyPair.getPrivate().getEncoded()));
out.write(PRIVATE_FOOTER);
out.close();

String publicFileName = name+".pub";
Writer out2 = new FileWriter(publicFileName);
out2.write(PUBLIC_HEADER);
out2.write(encoder.encodeToString(keyPair.getPublic().getEncoded()));
out2.write(PUBLIC_FOOTER);
out2.close();

} catch (Exception x) {
x.printStackTrace();
}
}

public static PrivateKey loadPrivateKeyBase64(String keyFile) {
PrivateKey pvt = null;
try {
/* Read all bytes from the private key file */
Path path = Paths.get(keyFile);
byte[] bytes = Files.readAllBytes(path);

String s = new String(bytes);
s = s.replace(PRIVATE_HEADER, "");
s = s.replace(PRIVATE_FOOTER, "");

bytes = Base64.getDecoder().decode(s.getBytes());

/* Generate private key. */
PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
pvt = kf.generatePrivate(ks);
} catch (Exception x) {
x.printStackTrace();
}

return pvt;
}

public static PublicKey loadPublicKeyBase64(String keyFile) {
PublicKey pub = null;
try {
/* Read all the public key bytes */
Path path = Paths.get(keyFile);
byte[] bytes = Files.readAllBytes(path);

String s = new String(bytes);
s = s.replace(PUBLIC_HEADER, "");
s = s.replace(PUBLIC_FOOTER, "");

bytes = Base64.getDecoder().decode(s.getBytes());

/* Generate public key. */
X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
pub = kf.generatePublic(ks);
} catch (Exception x) {
x.printStackTrace();
}
return pub;
}

Load RSA private key from a PEM encoded private key

The key is PEM encoded. You need to strip the PEM header and footer, then convert from Base64 back to DER/BER, and finally use Crypto++'s BERDecodePrivateKey.

There's some reading on the subject at the Crypto++ wiki under Keys and Formats. Below is the code to perform the conversion (I don't believe Stack Overflow has a working example of it in Crypto++).

string RSA_PRIV_KEY =
"-----BEGIN RSA PRIVATE KEY-----\n"
"MIIBOgIBAAJBAK8Q+ToR4tWGshaKYRHKJ3ZmMUF6jjwCS/u1A8v1tFbQiVpBlxYB\n"
"paNcT2ENEXBGdmWqr8VwSl0NBIKyq4p0rhsCAQMCQHS1+3wL7I5ZzA8G62Exb6RE\n"
"INZRtCgBh/0jV91OeDnfQUc07SE6vs31J8m7qw/rxeB3E9h6oGi9IVRebVO+9zsC\n"
"IQDWb//KAzrSOo0P0yktnY57UF9Q3Y26rulWI6LqpsxZDwIhAND/cmlg7rUz34Pf\n"
"SmM61lJEmMEjKp8RB/xgghzmCeI1AiEAjvVVMVd8jCcItTdwyRO0UjWU4JOz0cnw\n"
"5BfB8cSIO18CIQCLVPbw60nOIpUClNxCJzmMLbsrbMcUtgVS6wFomVvsIwIhAK+A\n"
"YqT6WwsMW2On5l9di+RPzhDT1QdGyTI5eFNS+GxY\n"
"-----END RSA PRIVATE KEY-----";

static string HEADER = "-----BEGIN RSA PRIVATE KEY-----";
static string FOOTER = "-----END RSA PRIVATE KEY-----";

size_t pos1, pos2;
pos1 = RSA_PRIV_KEY.find(HEADER);
if(pos1 == string::npos)
throw runtime_error("PEM header not found");

pos2 = RSA_PRIV_KEY.find(FOOTER, pos1+1);
if(pos2 == string::npos)
throw runtime_error("PEM footer not found");

// Start position and length
pos1 = pos1 + HEADER.length();
pos2 = pos2 - pos1;
string keystr = RSA_PRIV_KEY.substr(pos1, pos2);

// Base64 decode, place in a ByteQueue
ByteQueue queue;
Base64Decoder decoder;

decoder.Attach(new Redirector(queue));
decoder.Put((const byte*)keystr.data(), keystr.length());
decoder.MessageEnd();

// Write to file for inspection
FileSink fs("decoded-key.der");
queue.CopyTo(fs);
fs.MessageEnd();

try
{
CryptoPP::RSA::PrivateKey rsaPrivate;
rsaPrivate.BERDecodePrivateKey(queue, false /*paramsPresent*/, queue.MaxRetrievable());

// BERDecodePrivateKey is a void function. Here's the only check
// we have regarding the DER bytes consumed.
ASSERT(queue.IsEmpty());
}
catch (const Exception& ex)
{
cerr << ex.what() << endl;
exit (1);
}

After loading the key, you can validate it with:

AutoSeededRandomPool prng;
bool valid = rsaPrivate.Validate(prng, 3);
if(!valid)
cerr << "RSA private key is not valid" << endl;

And print it with:

cout << "N: " << rsaPrivate.GetModulus() << endl << endl;
cout << "E: " << rsaPrivate.GetPublicExponent() << endl << endl;
cout << "D: " << rsaPrivate.GetPrivateExponent() << endl << endl;

If the key is password protected, then Crypto++ cannot decode it. The library lacks the support to perform the decryption. In this case, you can convert it to BER/DER using the following OpenSSL command. Then you can use the key material with Crypto++.

openssl pkcs8 -nocrypt -in rsa-key.pem -inform PEM -topk8 -outform DER -out rsa-key.der

The sample program wrote the key to file with this:

FileSink fs("decoded-key.der");
queue.CopyTo(fs);
fs.MessageEnd();

The CopyTo leaves the bytes in the queue for use later. You can dump the file with an ASN.1 tool, like Gutmann's dumpasn1:

$ dumpasn1 decoded-key.der 
0 314: SEQUENCE {
4 1: INTEGER 0
7 65: INTEGER
: 00 AF 10 F9 3A 11 E2 D5 86 B2 16 8A 61 11 CA 27
: 76 66 31 41 7A 8E 3C 02 4B FB B5 03 CB F5 B4 56
: D0 89 5A 41 97 16 01 A5 A3 5C 4F 61 0D 11 70 46
: 76 65 AA AF C5 70 4A 5D 0D 04 82 B2 AB 8A 74 AE
: 1B
74 1: INTEGER 3
77 64: INTEGER
: 74 B5 FB 7C 0B EC 8E 59 CC 0F 06 EB 61 31 6F A4
: 44 20 D6 51 B4 28 01 87 FD 23 57 DD 4E 78 39 DF
: 41 47 34 ED 21 3A BE CD F5 27 C9 BB AB 0F EB C5
: E0 77 13 D8 7A A0 68 BD 21 54 5E 6D 53 BE F7 3B
143 33: INTEGER
: 00 D6 6F FF CA 03 3A D2 3A 8D 0F D3 29 2D 9D 8E
: 7B 50 5F 50 DD 8D BA AE E9 56 23 A2 EA A6 CC 59
: 0F
178 33: INTEGER
: 00 D0 FF 72 69 60 EE B5 33 DF 83 DF 4A 63 3A D6
: 52 44 98 C1 23 2A 9F 11 07 FC 60 82 1C E6 09 E2
: 35
213 33: INTEGER
: 00 8E F5 55 31 57 7C 8C 27 08 B5 37 70 C9 13 B4
: 52 35 94 E0 93 B3 D1 C9 F0 E4 17 C1 F1 C4 88 3B
: 5F
248 33: INTEGER
: 00 8B 54 F6 F0 EB 49 CE 22 95 02 94 DC 42 27 39
: 8C 2D BB 2B 6C C7 14 B6 05 52 EB 01 68 99 5B EC
: 23
283 33: INTEGER
: 00 AF 80 62 A4 FA 5B 0B 0C 5B 63 A7 E6 5F 5D 8B
: E4 4F CE 10 D3 D5 07 46 C9 32 39 78 53 52 F8 6C
: 58
: }

0 warnings, 0 errors.

Get a PrivateKey from a RSA .pem file

I'm using BouncyCastle 1.57 (bcprov-jdk15on, bcmail-jdk15on and bcpkix-jdk15on) and Java 7.

You can read the private key using the JcaPEMKeyConverter class.
The code below works for keys with and without a password:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;

// don't forget to add the provider
Security.addProvider(new BouncyCastleProvider());
String password = "your password";

// reads your key file
PEMParser pemParser = new PEMParser(new FileReader(keyFile));
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

KeyPair kp;
if (object instanceof PEMEncryptedKeyPair) {
// Encrypted key - we will use provided password
PEMEncryptedKeyPair ckp = (PEMEncryptedKeyPair) object;
// uses the password to decrypt the key
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
kp = converter.getKeyPair(ckp.decryptKeyPair(decProv));
} else {
// Unencrypted key - no password needed
PEMKeyPair ukp = (PEMKeyPair) object;
kp = converter.getKeyPair(ukp);
}

// RSA
KeyFactory keyFac = KeyFactory.getInstance("RSA");
RSAPrivateCrtKeySpec privateKey = keyFac.getKeySpec(kp.getPrivate(), RSAPrivateCrtKeySpec.class);

System.out.println(privateKey.getClass());

The privateKey's class will be java.security.spec.RSAPrivateCrtKeySpec (which extends RSAPrivateKeySpec).

Decrypt Base64 string with RSA private key

Here is the exact equivalent code in node.js for the above Java code.

var crypto = require("crypto");
var path = require("path");
var fs = require("fs");

var encryptStringWithRsaPublicKey = function(toEncrypt, relativeOrAbsolutePathToPublicKey) {
var absolutePath = path.resolve(relativeOrAbsolutePathToPublicKey);
var publicKey = fs.readFileSync(absolutePath, "utf8");
var buffer = Buffer.from(toEncrypt);
var encrypted = crypto.publicEncrypt(publicKey, buffer);
return encrypted.toString("base64");
};

var decryptStringWithRsaPrivateKey = function(toDecrypt, relativeOrAbsolutePathtoPrivateKey) {
var absolutePath = path.resolve(relativeOrAbsolutePathtoPrivateKey);
var privateKey = fs.readFileSync(absolutePath, "utf8");
var buffer = Buffer.from(toDecrypt, "base64");
console.log("PRIVATE_KEY:",privateKey);
var decrypted = crypto.privateDecrypt({
key: privateKey,
padding:crypto.constants.RSA_PKCS1_OAEP_PADDING
}, buffer);

return decrypted.toString("utf8");
};`

cipher= '/**Encrypted TEXT/*'

/*private.pem*/
-----BEGIN PRIVATE KEY-----
............................
-----ENDPRIVATE KEY-----
/**/
const decryptedText = decryptStringWithRsaPrivateKey(cipher,'private.pem')
````````

PEM to PrivateKey

Thanks to comments by Greggz and James I was able to get it to work. There were two problems:

  • X509EncodedKeySpec had to be replaced by PKCS8EncodedKeySpec
  • The provided pemkey was PKCS#1 (to be recognized by 'BEGIN RSA PRIVATE KEY') but needs to be PKCS#8 (to be recognized by 'BEGIN PRIVATE KEY').

Create PrivateKey and PublicKey from a String base64 encoding with DER format

To test your scenario, I've created an RSA private key with openssl.

openssl genrsa -out private.pem 1024

Then I've converted this key to PKCS#8 DER format.

openssl pkcs8 -topk8 -inform PEM -in private.pem -outform DER -out private.der -nocrypt

The manual of openssl refers to PKCS#8 and DER both as formats, so as far as I'm concerned the following happens:

  • pkcs8 tells openssl that I want to work with private keys in PKCS#8 format.
  • -topk8 tells it that the private key I'm going to specify with -in is not in PKCS#8 (otherwise it'll assume it is).
  • -inform and -in specify that I want to convert the (PEM) private key to PKCS#8 (without -topk8 it'll try to convert a key already in PKCS#8 format to a standard key format).
  • -outform and -out tells it I want a DER formatted key as output.
  • -nocrypt tells it that I don't want to encrypt the key.

Then, with my RSA key (in standard format) I've created a certificate.

openssl req -new -x509 -keyform PEM -key private.pem -outform DER -out public.der

The certificate contains the public key corresponding to my private key.

After all of these, I've encoded both the private key and the certificate with Base64.

base64 private.der > private.der.b64
base64 public.der > public.der.b64

The following files were generated.

private.pem      # standard
private.der # pkcs8/DER
private.der.b64
public.der # x509/DER
public.der.b64
public static void main(String[] args) throws IOException, GeneralSecurityException {
// get a handle on the base64 encoded key and certificate
File privateKeyFile = new File("private.der.b64");
File publicKeyFile = new File("public.der.b64");

// pull them into arrays
byte[] privateKeyBytes = toByteArray(privateKeyFile);
byte[] publicKeyBytes = toByteArray(publicKeyFile);

// decode them
privateKeyBytes = toDecodedBase64ByteArray(privateKeyBytes);
publicKeyBytes = toDecodedBase64ByteArray(publicKeyBytes);

// get the private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
KeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

// get the public key
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
Certificate certificate = certificateFactory.generateCertificate(new ByteArrayInputStream(publicKeyBytes));
PublicKey publicKey = certificate.getPublicKey();
}

private static byte[] toByteArray(File file) throws IOException {
// java 7's try-with-resources statement
try (FileInputStream in = new FileInputStream(file);
FileChannel channel = in.getChannel()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
channel.transferTo(0, channel.size(), Channels.newChannel(out));
return out.toByteArray();
}
}

private static byte[] toDecodedBase64ByteArray(byte[] base64EncodedByteArray) {
return DatatypeConverter.parseBase64Binary(
new String(base64EncodedByteArray, Charset.forName("UTF-8")));
}

The main problem was that you had a certificate instead of a public key. The certificate contains the public key, but it cannot be loaded with X509EncodedKeySpec(...), this is why the CertificateFactory has to be used instead.

(By the way here is a great article/tutorial on openssl and Java cryptography usage. I've got my info partly from there.)

Differences between BEGIN RSA PRIVATE KEY and BEGIN PRIVATE KEY

See https://polarssl.org/kb/cryptography/asn1-key-structures-in-der-and-pem (search the page for "BEGIN RSA PRIVATE KEY") (archive link for posterity, just in case).

BEGIN RSA PRIVATE KEY is PKCS#1 and is just an RSA key. It is essentially just the key object from PKCS#8, but without the version or algorithm identifier in front. BEGIN PRIVATE KEY is PKCS#8 and indicates that the key type is included in the key data itself. From the link:

The unencrypted PKCS#8 encoded data starts and ends with the tags:

-----BEGIN PRIVATE KEY-----
BASE64 ENCODED DATA
-----END PRIVATE KEY-----

Within the base64 encoded data the following DER structure is present:

PrivateKeyInfo ::= SEQUENCE {
version Version,
algorithm AlgorithmIdentifier,
PrivateKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}

So for an RSA private key, the OID is 1.2.840.113549.1.1.1 and there is a RSAPrivateKey as the PrivateKey key data bitstring.

As opposed to BEGIN RSA PRIVATE KEY, which always specifies an RSA key and therefore doesn't include a key type OID. BEGIN RSA PRIVATE KEY is PKCS#1:

RSA Private Key file (PKCS#1)

The RSA private key PEM file is specific for RSA keys.

It starts and ends with the tags:

-----BEGIN RSA PRIVATE KEY-----
BASE64 ENCODED DATA
-----END RSA PRIVATE KEY-----

Within the base64 encoded data the following DER structure is present:

RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

RSA decryption according to the private key reports an error

1. You are decoding wrong. PEM format has a dash-BEGIN line identifying the type of data, a block of base64 encoding the data, and a dash-END line. The BEGIN and END lines are part of the format, but they do not contain base64-encoded data; only the lines in between contain the base64-encoded data. You are apparently passing the whole thing, including the BEGIN and END lines, to commons.codec.Base64, which results in decoding a bunch of garbage before and after the actual data. That garbage isn't valid ASN.1 DER, so when Java tries to parse it as DER it fails.

2. Plus your data is not a PKCS8-clear privatekey. The PEM type 'RSA PRIVATE KEY' is an OpenSSL-defined format that contains a 'traditional' or 'legacy' format, namely the PKCS1 representation of the private key. This is not PKCS8, which is the only key format Java supports natively; that's why the spec class is named PKCS8EncodedKeySpec, because it is a key spec encoded as PKCS8 and more specifically PKCS8-clear. If you fix the above problem by removing the BEGIN and END lines before base64-decoding, Java can parse the result as DER, but not as a PKCS8-clear key; you get a different exception about 'algid parse error, not a sequence'. To fix this there are 5 approaches:

  • change whatever process you use to initially generate the keypair so it generates PKCS8, not OpenSSL-legacy PKCS1. Especially since you need anyway to replace the keypair you compromised by publishing it, as 207421 said. You give no clue what that process is or was, so I can't give any details.

  • convert your generated privatekey, or a copy, to PKCS8-clear. This is not programming or development and offtopic, but if you have or get OpenSSL (on the same or any accessible and secure system), you can do

    openssl pkey -in oldfile -out newfile   # 1.0.0 up only, but older is now rare
    # or
    openssl pkcs8 -topk8 -nocrypt -in oldfile -out newfile # even ancient versions

Once you have a PKCS8-clear file, just remove the BEGIN and END lines and base64-decode what is left, and pass that to KeyFactory as PKCS8EncodedKeySpec as you already do.

  • use https://www.bouncycastle.org . The 'bcpkix' jar has (Java) code to read a large range of OpenSSL-supported PEM formats, including the RSA-PKCS1 private key format you have. There are lots of existing Qs about this; just search for PEMParser and JcaPEMKeyConverter.

  • convert it yourself. Decode the body of the file you have, after removing the BEGIN and END lines, to get the PKCS1 key, then build the PKCS8 format for that key, and then pass it to KeyFactory as PKCS8EncodedKeySpec. See answers by Noa Resare and Jean-Alexis Aufauvre on Getting RSA private key from PEM BASE64 Encoded private key file or mine in Java: Convert DKIM private key from RSA to DER for JavaMail .

  • do it entirely yourself. Decode the file you have without BEGIN/END to get PCKS1, parse that as DER following e.g. RFC8447, and build RSAPrivateCrtKeySpec. Some other As on the Q I linked just above do this. However, this requires either: using undocumented internal sun.* classes, which used to work in Java (hence the existing answers) but which 'modular' Java versions (9 up) since 2017 have steadily made more difficult or impossible; using BouncyCastle which has documented (and good) support for ASN.1 -- but then it's easier to use bcpkix for the whole job as above; or writing your own ASN.1 parsing, which is a good deal of work.

PS: encrypting text with RSA is usually a bad design; it's not suited for that. But that's not really a programming issue and doesn't belong here.



Related Topics



Leave a reply



Submit