I’d like to generate an RSA key pair in Java 21 using plain Java API (without using BouncyCastle or similar libraries). The private key should be processable by both OpenSSL and Java. Based on Java limitations, this means that I’d like to generate an AES-encrypted private key.
The code below works fine for generating unencrypted private keys, but despite many Google & StackOverflow searches, I haven’t been able to successfully implement the pemEncrypt
method. I’m getting somewhat lost in the many algorithms and formats; maybe I need to do extra work for generating a proper ASN.1 data structure?
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class KPGenerator {
private final char[] passPhrase;
@SneakyThrows
public final KeyPair generate() {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
return kpg.generateKeyPair();
}
@SneakyThrows
public final void writePem(Path privateKeyPath, Path publicKeyPath) {
var kp = generate();
writePem("PRIVATE KEY", kp.getPrivate().getEncoded(), privateKeyPath);
writePem("PUBLIC KEY", kp.getPublic().getEncoded(), publicKeyPath);
}
@SneakyThrows
private final void writePem(String type, byte[] key, Path path) {
var pemString = asPem(type, key);
Files.writeString(path, pemString, StandardOpenOption.CREATE_NEW);
}
private final String asPem(String type, byte[] key) {
if ( "PRIVATE KEY".equals(type) && passPhrase!=null ) {
return asPem("ENCRYPTED PRIVATE KEY", pemEncrypt(key, passPhrase));
}
return "-----BEGIN "+type+"-----n"
+ Base64.getMimeEncoder().encodeToString(key)
+ "n-----END "+type+"-----";
}
@SneakyThrows
private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
???
}
}
Below are some examples of pemEncrypt
implementations that I’ve tried.
Method 1
Based on https://medium.com/@patc888/decrypt-openssl-encrypted-data-in-java-4c31983afe19
Problem: openssl asn1parse -in privatekey.pem
throws an error: 4047F272597F0000:error:0680009B:asn1 encoding routines:ASN1_get_object:too long:../crypto/asn1/asn1_lib.c:95
@SneakyThrows
private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
var salt = createSalt();
byte[] passAndSalt = ArrayUtils.addAll(toBytes(passPhrase), salt);
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] key = md.digest(passAndSalt);
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
md.reset();
byte[] iv = Arrays.copyOfRange(md.digest(ArrayUtils.addAll(key, passAndSalt)), 0, 16);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
byte[] encryptedSecretKey = cipher.doFinal(privateKey);
try ( var bos = new ByteArrayOutputStream(); ) {
bos.writeBytes("Salted__".getBytes(StandardCharsets.US_ASCII));
bos.writeBytes(salt);
bos.writeBytes(encryptedSecretKey);
return bos.toByteArray();
}
}
private static final byte[] toBytes(char[] chars) {
CharBuffer charBuffer = CharBuffer.wrap(chars);
ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
byte[] bytes = Arrays.copyOfRange(byteBuffer.array(),
byteBuffer.position(), byteBuffer.limit());
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
return bytes;
}
private static final byte[] createSalt() {
final byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return salt;
}
Sample output:
-----BEGIN ENCRYPTED PRIVATE KEY-----
U2FsdGVkX193omAgeegjjYcN8h3riBDrFrFVpOSxkaEIo1sFg9ZT89RkN+fCBYpzFROB4UOmfBVs
0t/3mfLIID8eDjRH59KU9BCXwXt9unpYGWjFL4Vv5OQsRefiVM57FpbQ5YS6xPhfq0qQT39uTI7p
wG9XSHEBogoYiHTSx3+IIj5/iPNWuE3rMxRJUX/tERFfl6SSY1SlWR3zzy2LwlCddklzIqyZkLSk
cUE+yHTFsFnLuWdkaVMVrWY/isArukuc/gAWA9LThdF+il87s1D6tVtZOpFAmhqYUXhX6Si4scDK
910xEhqZhxqsybBih51+y0GqvcxgVpkadoEaz2WAcAq9aOykjvWGEhtGrCJ9JqhiiIVnY7bcHZxv
cPAAGAOoP95BHdDFR0VYnX1I+OzRxl0XDTVQEsYIWFV0t3u5/dUy/x2yOgkhFer7feAiY8yo7fnU
E1YOsSW2PnF8E2pXBeiJoemw5BimSfuqbuYr140gyDvPvjcfQBwr4vUuKXRxPnTwzjzvqYY5vNXG
p3+TTTL++DMOX/Us0kRVHERdsznn/E20QqBie4f2xa06Vkpf8SZMyO+aSQXRJeVeGH1LtaBLPX9R
hflr6SaX94E9Du91r9Qif1dI451P4M0+Zo3oQTmfy6RwKwckiP+L7LRyfZKLdRk2OTMcP2PX20DZ
SpeYzEzY6LxsaGAiQnHQ+jX9173455PJ1YCRANfCVCjVFr4enAIHqbxId8XD2HdEp3wB76Bj/uFF
NPIzA/gY6ezrUeikGrPYdbgXZ0meKmEJkSxl32ojCmDmhGShxVXRI8c8Subdj+mFS6/BPCwQ2W5V
UeS3fbblUbtpii0XdJoeh+ek/+AGwnkVI7BFse1sZqG4KRr/beH4Z7P2oms1a9ZWBihVveI8xRL1
HcLVi/lgrD25Rw9c+BVDlpCNpr9C42AJgD8XiLQsfd3kj+S5nOTjf61+onz7VQR3LtFDSex2biBL
4DlAbqQzFVM9F2LO0BZeaCLcVKsQ1vmmLYahvklhAcMBco8rZ/8aw0dzVt6L5pWNytH/fYRBRbSI
F1rjnEWmGjF+aXFbjg/SMAV+YSNg03OM/BrU9HpEXH40PB7sJrbY4nqtX/4igwDkMjEVsz0ThZt/
BODzNhxND+HxcXIKjnr5W+3FKUmtkhAMHYgWShzmYCUlt4rdUWjZSL9I6YW0TpG69NBpSw0qihCD
31iUL3uDLe+1QiCoa3/MvPo5CrXheHwV2RNGwIaZRYS6Fzz5VG5j4iI2r8eB8nysiZpTC9hgrlXj
1HdZiKqQDAm3N27eDMLDOfvtJz1x8GfMI+agoj7KB9W48rMNtk0+Vmv5THf6EkeM+KhEWjff4Ju7
cShPd9tJKDwDHPKSUQyCZegIHJfEZeS5gZ6TPeE/L6mEvHBmW3YQpsDkI7lBo/qQYe7UyrAHVTIY
sYNdIA74LvrXSPHlIumjT4lPl9aTc7vaoLfrJc1EnTatujBqrVucdB0LdFWQ34s9YdLICZUV2X25
xUfIpIWZ6G6S8THqGICZ61tAXvRGRwVT2JlsiG6967G84fda0CpT/huJEiTrZRlETfAOEyfQJFJp
shxD+6gTYaIwMSmx2Tq7KyC9emddILIFu1wZgY2H6096n/MM9RuLIxgWOT9MscYWNzlGF+L9XSJH
98c=
Method 2
Problem: Similar OpenSSL error as before: 40B70A48E47F0000:error:0680007B:asn1 encoding routines:ASN1_get_object:header too long:../crypto/asn1/asn1_lib.c:105:
@SneakyThrows
private static final byte[] pemEncrypt(byte[] key, char[] passPhrase) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, createKey(passPhrase), createIv());
return cipher.doFinal(key);
}
@SneakyThrows
private static final SecretKey createKey(char[] passPhrase) {
//PBE results in invalid key length exception
//SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(passPhrase, createSalt(), 65536, 256);
SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
return secret;
}
private static final IvParameterSpec createIv() {
final byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
return new IvParameterSpec(iv);
}
private static final byte[] createSalt() {
final byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return salt;
}
Sample output:
-----BEGIN ENCRYPTED PRIVATE KEY-----
iBGmburj5hml/segy4KnBXwIm1kYGADhB0KAhfgB1yFsyyoRXC5BlwT6Dq4OYHROtkhsttwV1+5V
cqCarwq1WJmBtWlnohh3oU4YRAkmHj6nMh8fFCRutaySBuN75bqI9tSJ0+1nRsg+saU2F1rGxHma
WhcHB3KZqZe8sNlsTBvDzR6NYU1+XNbxskbOiniA6eJAXGuYpfVtoLlY3BX3AtbK8d+e2XBMF0SA
ddMhUtZblIAsj36fnxmGIql/sHfCLEsOUBDK8fdpA1YryLpg5HhE4Fwht++hamtZUgvW0vNw1q7e
PrhSGY5pOgIdtb1EPdswnHNd77C2/qDC/it3on9mU6U9lvGbP7TyqO0vkXWdDY8hzwowNauy6pTv
YneTQCX1rLwTkuMJMq9ObvoPd+Z0ojmS25eSFlAxIin1DZ3wUKcox2u0DIVA5iF5M4wQRn3GiIJU
tjX4B45t935gxPlNMB7EGRySBCidCmWngTWlUF4KhRD6bcf/BlD70LLRxD37SBT/kpExhaewvUx8
Obrj5hXEHxhyf7h28Rb/LXj5C5MwbG3mAlWWlk1cLW6vezvVi2xpVsjIuoCVmN2iv3RklQtL018I
yUEEUeJZObfdZ13c9ha/Pf4a/+mIx6HJUkQzsH0BxkXifxSYBw7/CtH+0nRZRRswo/AbxTVDEVyV
A0UcWfCZa9PW52mWqZPp7aggI7Zy/IjMYQ6q4YMLWQbzCPGl3aQZ/b7VFO7kYrPsyZwyj9IMQIWt
AUI2gvtz5DysTekrMU0aiU/BS1rrl6i9C7FnmZHEBCf6DjtgKl3f0ZwPo5r2UejvdV4rw4sFqrR4
DYdva5McjmFRpiqpvDNXnRUliLXqXorsJgTCga+kX4YfHDhjjAdU+WvFMnC14FqbiJ6bb6dnGZ93
AdTPdOFV3tAX/M9mEMGqcVKazkVSzC0ZV7bxYO0IKpKpufRganb9emHu+A7EZPiBZKm7/IEnI8vq
NISt8raICwPfrl7lkg+VLfYC/8ubhpMY8itsDi/LHzOcAj4Tw0Hyayymy7chUSPfKJu4homVbeMY
vvSpBVJwUMa4rs+NQH6ocpUk3o/OuEdbY29KceqH8SQQtshZ7NQN6NTDJl40l3A7jdDK62PEJksc
BeHrKRiT3wMLKnhbEHbyXeL47j17Bbq0SWzUQuS7pBasIf+UsiQVKNLxYepIJdneRCsqW32zu7n3
DOx/ZftThUCU7NjmbVgWOmj0NVAEa5zfkwMKuqNyDRbTTkiw4tSxnbuqQGOZOp3G0e3pJ+kPui+p
qLsIyCY9l/wk2zy1WafD6ddP7jtvVB89SFVMN61DiPmJVQoq6hrFLHlv+lQE970sWGsmZ3b7BxBw
QLA0c3sKF9DEFIpv9B/VU6YtyYjTi81hmWZ/MuzEhj6ZW5VNqgUAAODb5Tgk8cLELYrxgoWyxMuX
yF6WZ7XIdFx5TKYstzhsDSBkpfaQxqzSmg8OvVQL9FLCjs+CoBjTUxzixHz5OSysapf4aMiWZS3t
UfNfYoJFjRCIvZBP1T9Q12kdLxM4kvcC6Ony4ZmIWLBYCOE7aBWW3+0AGH4NO2eQkJDTIvOQkC1n
YSL/k9O3eYi+eZ4d21ZcWJRmOMjftR3qVUSG8YzyNXAIBps=
-----END ENCRYPTED PRIVATE KEY-----
Method 3
Based on ChatGPT.
Problem: Java exception java.security.InvalidKeyException: Wrong algorithm: AES or Rijndael required
on Cipher.init()
private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey);
byte[] salt = createSalt();
int iterationCount = 65536; // You can adjust this value
int keyLength = 128; // You can adjust this value
PBEKeySpec pbeKeySpec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedKey = cipher.doFinal(pkcs8EncodedKeySpec.getEncoded());
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(cipher.getParameters(), encryptedKey);
return encryptedPrivateKeyInfo.getEncoded();
}