Add pem-api module: PEMDecoder/PEMEncoder (JEP 538 preview on JDK 27, JEP 542 final in JDK 28) for JWT keys, TLS, encrypted keys and BouncyCastle migration

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
This commit is contained in:
2026-10-26 01:50:05 +00:00
co-authored by Claude Sonnet 5.5
parent c3cc57f482
commit 2e25042a68
34 changed files with 895 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import java.security.*;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.spec.PBEParameterSpec;
/** Encrypted private keys: write one, read one, read one openssl wrote, and every way it can go wrong. */
public class EncryptedKeys {
static final char[] PW = "changeit".toCharArray();
public static void main(String[] args) throws Exception {
PEMDecoder d = PEMDecoder.of();
PEMEncoder e = PEMEncoder.of();
PrivateKey key = d.decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class);
Fx.h("write an encrypted key with the defaults");
String enc = e.withEncryption(PW).encodeToString(key);
System.out.println(enc.lines().filter(l -> l.startsWith("-----")).toList());
EncryptedPrivateKeyInfo info = (EncryptedPrivateKeyInfo) d.decode(enc);
System.out.println("default algorithm: " + info.getAlgName());
PBEParameterSpec spec = info.getAlgParameters().getParameterSpec(PBEParameterSpec.class);
System.out.println("default iteration count: " + spec.getIterationCount() + ", salt bytes: " + spec.getSalt().length);
Fx.h("read it back");
Fx.check(d.withDecryption(PW).decode(enc, PrivateKey.class).equals(key), "withDecryption(password) returns the original key");
Fx.h("choose the algorithm and cost yourself");
PBEParameterSpec custom = new PBEParameterSpec(new SecureRandom().generateSeed(16), 600_000);
EncryptedPrivateKeyInfo strong = EncryptedPrivateKeyInfo.encrypt(key, PW, "PBEWithHmacSHA512AndAES_256", custom, null);
String strongPem = e.encodeToString(strong);
EncryptedPrivateKeyInfo again = (EncryptedPrivateKeyInfo) d.decode(strongPem);
System.out.println(again.getAlgName() + ", iterations " + again.getAlgParameters().getParameterSpec(PBEParameterSpec.class).getIterationCount());
Fx.check(again.getKey(PW).equals(key), "getKey(password) on the decoded EncryptedPrivateKeyInfo works too");
Fx.h("a file written by openssl pkcs8 -topk8 -v2 aes-256-cbc");
EncryptedPrivateKeyInfo fromOpenssl = (EncryptedPrivateKeyInfo) d.decode(Fx.read("rsa-private-encrypted.pem"));
System.out.println(fromOpenssl.getAlgName());
Fx.check(d.withDecryption(PW).decode(Fx.read("rsa-private-encrypted.pem"), PrivateKey.class).equals(key), "decrypts to the same key as the unencrypted file");
Fx.h("failures");
String f = Fx.read("rsa-private-encrypted.pem");
Fx.tryIt("no password, ask for PrivateKey", () -> d.decode(f, PrivateKey.class));
Fx.tryIt("no password, no class", () -> d.decode(f));
Fx.tryIt("wrong password", () -> d.withDecryption("nope".toCharArray()).decode(f, PrivateKey.class));
Fx.tryIt("password on a key that is not encrypted", () -> d.withDecryption(PW).decode(Fx.read("rsa-private-pkcs8.pem"), PrivateKey.class));
Fx.tryIt("openssl 'traditional' encrypted (Proc-Type: 4,ENCRYPTED)", () -> d.withDecryption(PW).decode(Fx.read("rsa-private-legacy-encrypted.pem"), PrivateKey.class));
}
}