feat: externalize Ed25519 keys with file-based loading

Keys are loaded from PEM files when CAMELEER_JWT_PRIVATE_KEY_PATH and
CAMELEER_JWT_PUBLIC_KEY_PATH are set. Falls back to ephemeral key
generation for development.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-04 14:48:20 +02:00
parent 24309eab94
commit 0a2d5970e4
2 changed files with 49 additions and 3 deletions

View File

@@ -1,27 +1,71 @@
package net.siegeln.cameleer.saas.config;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Component
public class JwtConfig {
private static final Logger log = LoggerFactory.getLogger(JwtConfig.class);
@Value("${cameleer.jwt.expiration:86400}")
private long expirationSeconds = 86400;
@Value("${cameleer.jwt.private-key-path:}")
private String privateKeyPath = "";
@Value("${cameleer.jwt.public-key-path:}")
private String publicKeyPath = "";
private KeyPair keyPair;
@PostConstruct
public void init() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
this.keyPair = keyGen.generateKeyPair();
public void init() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
if (privateKeyPath.isEmpty() || publicKeyPath.isEmpty()) {
log.warn("No Ed25519 key files configured — generating ephemeral keys (dev mode)");
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
this.keyPair = keyGen.generateKeyPair();
} else {
log.info("Loading Ed25519 keys from {} and {}", privateKeyPath, publicKeyPath);
PrivateKey privateKey = loadPrivateKey(Path.of(privateKeyPath));
PublicKey publicKey = loadPublicKey(Path.of(publicKeyPath));
this.keyPair = new KeyPair(publicKey, privateKey);
}
}
private PrivateKey loadPrivateKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String pem = Files.readString(path)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] decoded = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(decoded));
}
private PublicKey loadPublicKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
String pem = Files.readString(path)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
byte[] decoded = Base64.getDecoder().decode(pem);
return KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(decoded));
}
public PrivateKey getPrivateKey() {

View File

@@ -21,3 +21,5 @@ management:
cameleer:
jwt:
expiration: 86400 # 24 hours in seconds
private-key-path: ${CAMELEER_JWT_PRIVATE_KEY_PATH:}
public-key-path: ${CAMELEER_JWT_PUBLIC_KEY_PATH:}