feat: add SigningKeyService for Ed25519 keypair management
Entity, repository, and service for generating and storing Ed25519 signing keys. Lazy-initializes on first call — generates keypair and persists to signing_keys table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
package net.siegeln.cameleer.saas.license;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "signing_keys")
|
||||||
|
public class SigningKeyEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(name = "public_key_b64", nullable = false, columnDefinition = "text")
|
||||||
|
private String publicKeyB64;
|
||||||
|
|
||||||
|
@Column(name = "private_key_b64", nullable = false, columnDefinition = "text")
|
||||||
|
private String privateKeyB64;
|
||||||
|
|
||||||
|
@Column(name = "active", nullable = false)
|
||||||
|
private boolean active = true;
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private Instant createdAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
if (createdAt == null) createdAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() { return id; }
|
||||||
|
public String getPublicKeyB64() { return publicKeyB64; }
|
||||||
|
public void setPublicKeyB64(String publicKeyB64) { this.publicKeyB64 = publicKeyB64; }
|
||||||
|
public String getPrivateKeyB64() { return privateKeyB64; }
|
||||||
|
public void setPrivateKeyB64(String privateKeyB64) { this.privateKeyB64 = privateKeyB64; }
|
||||||
|
public boolean isActive() { return active; }
|
||||||
|
public void setActive(boolean active) { this.active = active; }
|
||||||
|
public Instant getCreatedAt() { return createdAt; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package net.siegeln.cameleer.saas.license;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface SigningKeyRepository extends JpaRepository<SigningKeyEntity, UUID> {
|
||||||
|
|
||||||
|
Optional<SigningKeyEntity> findByActiveTrue();
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package net.siegeln.cameleer.saas.license;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SigningKeyService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SigningKeyService.class);
|
||||||
|
|
||||||
|
private final SigningKeyRepository signingKeyRepository;
|
||||||
|
|
||||||
|
public SigningKeyService(SigningKeyRepository signingKeyRepository) {
|
||||||
|
this.signingKeyRepository = signingKeyRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the active signing key, generating a new Ed25519 keypair on first call.
|
||||||
|
*/
|
||||||
|
public SigningKeyEntity getOrCreateActiveKey() {
|
||||||
|
return signingKeyRepository.findByActiveTrue()
|
||||||
|
.orElseGet(this::generateAndStoreKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base64-encoded public key (X.509 SPKI format).
|
||||||
|
*/
|
||||||
|
public String getPublicKeyBase64() {
|
||||||
|
return getOrCreateActiveKey().getPublicKeyB64();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstructs the Ed25519 PrivateKey from the stored base64.
|
||||||
|
*/
|
||||||
|
public PrivateKey getPrivateKey() {
|
||||||
|
SigningKeyEntity key = getOrCreateActiveKey();
|
||||||
|
try {
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(key.getPrivateKeyB64());
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||||
|
return KeyFactory.getInstance("Ed25519").generatePrivate(spec);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to reconstruct Ed25519 private key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstructs the Ed25519 PublicKey from the stored base64.
|
||||||
|
*/
|
||||||
|
public PublicKey getPublicKey() {
|
||||||
|
SigningKeyEntity key = getOrCreateActiveKey();
|
||||||
|
try {
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(key.getPublicKeyB64());
|
||||||
|
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||||
|
return KeyFactory.getInstance("Ed25519").generatePublic(spec);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to reconstruct Ed25519 public key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SigningKeyEntity generateAndStoreKey() {
|
||||||
|
try {
|
||||||
|
KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
|
||||||
|
String pubB64 = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
|
||||||
|
String privB64 = Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded());
|
||||||
|
|
||||||
|
var entity = new SigningKeyEntity();
|
||||||
|
entity.setPublicKeyB64(pubB64);
|
||||||
|
entity.setPrivateKeyB64(privB64);
|
||||||
|
entity.setActive(true);
|
||||||
|
|
||||||
|
var saved = signingKeyRepository.save(entity);
|
||||||
|
log.info("Generated new Ed25519 signing keypair (id={})", saved.getId());
|
||||||
|
return saved;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to generate Ed25519 keypair", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user