+ * Implementations provide the cryptographic backend (HSM via PKCS#11, database, cloud KMS, etc.) + * for secure key wrapping/unwrapping using envelope encryption. + *
+ * Design principles: + * - KEKs (Key Encryption Keys) never leave the secure backend + * - DEKs (Data Encryption Keys) are wrapped by KEKs for storage + * - Plaintext DEKs exist only transiently in memory during wrap/unwrap + * - All operations are purpose-scoped to prevent key reuse + *
+ * Thread-safety: Implementations must be thread-safe for concurrent operations. + */ +public interface KMSProvider extends Configurable, Adapter { + + /** + * Returns {@code true} if the given HSM profile configuration key name refers + * to a + * sensitive value (PIN, password, secret, or private key) that must be + * encrypted at + * rest and masked in API responses. + * + *
+ * This is a shared naming-convention helper used by both KMS providers (when
+ * loading/storing profile details) and the KMS manager (when building API
+ * responses).
+ *
+ * @param key configuration key name (case-insensitive); null returns false
+ * @return true if the key is considered sensitive
+ */
+ static boolean isSensitiveKey(String key) {
+ if (key == null) {
+ return false;
+ }
+ return key.equalsIgnoreCase("pin") ||
+ key.equalsIgnoreCase("password") ||
+ key.toLowerCase().contains("secret") ||
+ key.equalsIgnoreCase("private_key");
+ }
+
+ /**
+ * Get the unique name of this provider
+ *
+ * @return provider name (e.g., "database", "pkcs11")
+ */
+ String getProviderName();
+
+ /**
+ * Create a new Key Encryption Key (KEK) in the secure backend.
+ * Delegates to {@link #createKek(KeyPurpose, String, int, Long)} with null profile ID.
+ *
+ * @param purpose the purpose/scope for this KEK
+ * @param label human-readable label for the KEK (must be unique within purpose)
+ * @param keyBits key size in bits (typically 128, 192, or 256)
+ * @return the KEK identifier (label or handle) for later reference
+ * @throws KMSException if KEK creation fails
+ */
+ default String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException {
+ return createKek(purpose, label, keyBits, null);
+ }
+
+ /**
+ * Create a new Key Encryption Key (KEK) in the secure backend with explicit HSM profile.
+ *
+ * @param purpose the purpose/scope for this KEK
+ * @param label human-readable label for the KEK (must be unique within purpose)
+ * @param keyBits key size in bits (typically 128, 192, or 256)
+ * @param hsmProfileId optional HSM profile ID to create the KEK in (null for auto-resolution/default)
+ * @return the KEK identifier (label or handle) for later reference
+ * @throws KMSException if KEK creation fails
+ */
+ String createKek(KeyPurpose purpose, String label, int keyBits, Long hsmProfileId) throws KMSException;
+
+ /**
+ * Delete a KEK from the secure backend.
+ * WARNING: This will make all DEKs wrapped by this KEK unrecoverable.
+ *
+ * @param kekId the KEK identifier to delete
+ * @throws KMSException if deletion fails or KEK not found
+ */
+ void deleteKek(String kekId) throws KMSException;
+
+ /**
+ * Validates the configuration details for this provider before saving an HSM
+ * profile.
+ * Implementations should override this to perform provider-specific validation.
+ *
+ * @param details the configuration details to validate
+ * @throws KMSException if validation fails
+ */
+ default void validateProfileConfig(java.util.Map
+ * SECURITY: Caller MUST zeroize the returned byte array after use
+ *
+ * @param wrappedKey the wrapped key to decrypt
+ * @return plaintext DEK (caller must zeroize!)
+ * @throws KMSException if unwrapping fails or KEK not found
+ */
+ default byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException {
+ return unwrapKey(wrappedKey, null);
+ }
+
+ /**
+ * Unwrap (decrypt) a wrapped DEK to obtain the plaintext key using explicit HSM profile.
+ *
+ * SECURITY: Caller MUST zeroize the returned byte array after use
+ *
+ * @param wrappedKey the wrapped key to decrypt
+ * @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default)
+ * @return plaintext DEK (caller must zeroize!)
+ * @throws KMSException if unwrapping fails or KEK not found
+ */
+ byte[] unwrapKey(WrappedKey wrappedKey, Long hsmProfileId) throws KMSException;
+
+ /**
+ * Generate a new random DEK and immediately wrap it with a KEK.
+ * Delegates to {@link #generateAndWrapDek(KeyPurpose, String, int, Long)} with null profile ID.
+ * (convenience method combining generation + wrapping)
+ *
+ * @param purpose the intended purpose of the new DEK
+ * @param kekLabel the label of the KEK to use for wrapping
+ * @param keyBits DEK size in bits (typically 128, 192, or 256)
+ * @return WrappedKey containing the newly generated and wrapped DEK
+ * @throws KMSException if generation or wrapping fails
+ */
+ default WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException {
+ return generateAndWrapDek(purpose, kekLabel, keyBits, null);
+ }
+
+ /**
+ * Generate a new random DEK and immediately wrap it with a KEK using explicit HSM profile.
+ * (convenience method combining generation + wrapping)
+ *
+ * @param purpose the intended purpose of the new DEK
+ * @param kekLabel the label of the KEK to use for wrapping
+ * @param keyBits DEK size in bits (typically 128, 192, or 256)
+ * @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default)
+ * @return WrappedKey containing the newly generated and wrapped DEK
+ * @throws KMSException if generation or wrapping fails
+ */
+ WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits,
+ Long hsmProfileId) throws KMSException;
+
+ /**
+ * Rewrap a DEK with a different KEK (used during key rotation).
+ * Delegates to {@link #rewrapKey(WrappedKey, String, Long)} with null profile ID.
+ * This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
+ *
+ * @param oldWrappedKey the currently wrapped key
+ * @param newKekLabel the label of the new KEK to wrap with
+ * @return new WrappedKey encrypted with the new KEK
+ * @throws KMSException if rewrapping fails
+ */
+ default WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
+ return rewrapKey(oldWrappedKey, newKekLabel, null);
+ }
+
+ /**
+ * Rewrap a DEK with a different KEK (used during key rotation) using explicit target HSM profile.
+ * This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
+ *
+ * @param oldWrappedKey the currently wrapped key
+ * @param newKekLabel the label of the new KEK to wrap with
+ * @param targetHsmProfileId optional target HSM profile ID to wrap with (null for auto-resolution/default)
+ * @return new WrappedKey encrypted with the new KEK
+ * @throws KMSException if rewrapping fails
+ */
+ WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel, Long targetHsmProfileId) throws KMSException;
+
+ /**
+ * Perform health check on the provider backend
+ *
+ * @return true if provider is healthy and operational
+ * @throws KMSException if health check fails with critical error
+ */
+ boolean healthCheck() throws KMSException;
+
+ /**
+ * Invalidates any cached state (config, sessions) associated with the given HSM profile.
+ * Must be called after an HSM profile is updated or deleted so that the next operation
+ * re-reads the profile details from the database instead of using stale cached values.
+ *
+ * Providers that do not cache per-profile state (e.g. the database provider) can
+ * leave this as a no-op.
+ *
+ * @param profileId the HSM profile ID whose cache should be evicted
+ */
+ default void invalidateProfileCache(Long profileId) {
+ // no-op for providers that don't cache per-profile state
+ }
+}
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java
new file mode 100644
index 000000000000..41f5cf461fcc
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+/**
+ * Defines the purpose/usage scope for cryptographic keys in the KMS system.
+ * This enables proper key segregation and prevents key reuse across different contexts.
+ */
+public enum KeyPurpose {
+ /**
+ * Keys used for encrypting VM disk volumes (LUKS, encrypted storage)
+ */
+ VOLUME_ENCRYPTION("volume", "Volume disk encryption keys"),
+
+ /**
+ * Keys used for protecting TLS certificate private keys
+ */
+ TLS_CERT("tls", "TLS certificate private keys");
+
+ private final String name;
+ private final String description;
+
+ KeyPurpose(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ /**
+ * Convert string name to KeyPurpose enum
+ *
+ * @param name the string representation of the purpose
+ * @return matching KeyPurpose
+ * @throws IllegalArgumentException if no matching purpose found
+ */
+ public static KeyPurpose fromString(String name) {
+ for (KeyPurpose purpose : KeyPurpose.values()) {
+ if (purpose.getName().equalsIgnoreCase(name)) {
+ return purpose;
+ }
+ }
+ throw new IllegalArgumentException("Unknown KeyPurpose: " + name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Generate a globally unique, collision-resistant KEK label with context
+ *
+ * @param domainId the domain ID associated with this key
+ * @param accountId the account ID associated with this key
+ * @param uuid the unique identifier of the key entity
+ * @param version the version number of the key
+ * @return formatted KEK label (e.g., "volume-kek-1-2-a8054d8f-...-1")
+ */
+ public String generateKekLabel(long domainId, long accountId, String uuid, int version) {
+ return name + "-kek-" + domainId + "-" + accountId + "-" + uuid.replace("-", "") + "-v" + version;
+ }
+}
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java
new file mode 100644
index 000000000000..e70c5e32c46a
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java
@@ -0,0 +1,131 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Immutable Data Transfer Object representing an encrypted (wrapped) Data Encryption Key.
+ * The wrapped key material contains the DEK encrypted by a Key Encryption Key (KEK)
+ * stored in a secure backend (HSM, database, etc.).
+ *
+ * This follows the envelope encryption pattern:
+ * - DEK: encrypts actual data (e.g., disk volume)
+ * - KEK: encrypts the DEK (never leaves secure storage)
+ * - Wrapped Key: DEK encrypted by KEK, safe to store in database
+ */
+public class WrappedKey {
+ private final String uuid;
+ private final String kekId;
+ private final KeyPurpose purpose;
+ private final String algorithm;
+ private final byte[] wrappedKeyMaterial;
+ private final String providerName;
+ private final Date created;
+ private final Long zoneId;
+
+ /**
+ * Create a new WrappedKey instance
+ *
+ * @param kekId ID/label of the KEK used to wrap this key
+ * @param purpose the intended use of this key
+ * @param algorithm encryption algorithm (e.g., "AES/GCM/NoPadding")
+ * @param wrappedKeyMaterial the encrypted DEK blob
+ * @param providerName name of the KMS provider that created this key
+ * @param created timestamp when key was wrapped
+ * @param zoneId optional zone ID for zone-scoped keys
+ */
+ public WrappedKey(String kekId, KeyPurpose purpose, String algorithm,
+ byte[] wrappedKeyMaterial, String providerName,
+ Date created, Long zoneId) {
+ this(null, kekId, purpose, algorithm, wrappedKeyMaterial, providerName, created, zoneId);
+ }
+
+ /**
+ * Constructor for database-loaded keys with ID
+ */
+ public WrappedKey(String uuid, String kekId, KeyPurpose purpose, String algorithm,
+ byte[] wrappedKeyMaterial, String providerName,
+ Date created, Long zoneId) {
+ this.uuid = uuid;
+ this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
+ this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
+ this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
+ this.providerName = providerName;
+
+ if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
+ throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
+ }
+ this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
+
+ this.created = created != null ? new Date(created.getTime()) : new Date();
+ this.zoneId = zoneId;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getKekId() {
+ return kekId;
+ }
+
+ public KeyPurpose getPurpose() {
+ return purpose;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ /**
+ * Get wrapped key material. Returns a defensive copy to prevent modification.
+ * Caller is responsible for zeroizing the returned array after use.
+ */
+ public byte[] getWrappedKeyMaterial() {
+ return Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
+ }
+
+ public String getProviderName() {
+ return providerName;
+ }
+
+ public Date getCreated() {
+ return created != null ? new Date(created.getTime()) : null;
+ }
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ @Override
+ public String toString() {
+ return "WrappedKey{" +
+ "uuid='" + uuid + '\'' +
+ ", kekId='" + kekId + '\'' +
+ ", purpose=" + purpose +
+ ", algorithm='" + algorithm + '\'' +
+ ", providerName='" + providerName + '\'' +
+ ", materialLength=" + (wrappedKeyMaterial != null ? wrappedKeyMaterial.length : 0) +
+ ", created=" + created +
+ ", zoneId=" + zoneId +
+ '}';
+ }
+}
diff --git a/framework/pom.xml b/framework/pom.xml
index 337e5b0268b2..95d0bd0694c6 100644
--- a/framework/pom.xml
+++ b/framework/pom.xml
@@ -54,6 +54,7 @@
+ * This provider is suitable for deployments that don't have access to HSM hardware.
+ * The master KEKs are stored encrypted in the kms_database_kek_objects table using
+ * CloudStack's existing DBEncryptionUtil, with PKCS#11-compatible attributes.
+ */
+public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
+ private static final Logger logger = LogManager.getLogger(DatabaseKMSProvider.class);
+ private static final String PROVIDER_NAME = "database";
+ private static final int GCM_IV_LENGTH = 12; // 96 bits recommended for GCM
+ private static final int GCM_TAG_LENGTH = 16; // 128 bits
+ private static final String ALGORITHM = "AES/GCM/NoPadding";
+ private static final String CKO_SECRET_KEY = "CKO_SECRET_KEY";
+ private static final String CKK_AES = "CKK_AES";
+
+ private static final String DEFAULT_PROFILE_NAME = "HSM Database Provider";
+ private static final long SYSTEM_ACCOUNT_ID = 1L;
+ private static final long ROOT_DOMAIN_ID = 1L;
+
+ private final SecureRandom secureRandom = new SecureRandom();
+ @Inject
+ private KMSDatabaseKekObjectDao kekObjectDao;
+ @Inject
+ private HSMProfileDao hsmProfileDao;
+
+ @Override
+ public boolean start() {
+ super.start();
+ ensureDefaultHSMProfile();
+ return true;
+ }
+
+ @Override
+ public String getProviderName() {
+ return PROVIDER_NAME;
+ }
+
+ @Override
+ public String createKek(KeyPurpose purpose, String label, int keyBits, Long hsmProfileId) throws KMSException {
+ // Database provider ignores hsmProfileId
+ return createKek(purpose, label, keyBits);
+ }
+
+ @Override
+ public String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException {
+ if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
+ throw KMSException.invalidParameter("Key size must be 128, 192, or 256 bits");
+ }
+
+ if (StringUtils.isEmpty(label)) {
+ throw KMSException.invalidParameter("KEK label cannot be empty");
+ }
+
+ if (kekObjectDao.existsByLabel(label)) {
+ throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
+ }
+
+ byte[] kekBytes = new byte[keyBits / 8];
+ try {
+ secureRandom.nextBytes(kekBytes);
+
+ // Base64 encode then encrypt the KEK material using DBEncryptionUtil
+ String kekBase64 = Base64.getEncoder().encodeToString(kekBytes);
+ String encryptedKek = DBEncryptionUtil.encrypt(kekBase64);
+ byte[] encryptedKekBytes = encryptedKek.getBytes(StandardCharsets.UTF_8);
+
+ KMSDatabaseKekObjectVO kekObject = new KMSDatabaseKekObjectVO(label, purpose, keyBits, encryptedKekBytes);
+ kekObject.setObjectClass(CKO_SECRET_KEY);
+ kekObject.setKeyType(CKK_AES);
+ kekObject.setObjectId(label.getBytes(StandardCharsets.UTF_8));
+ kekObject.setAlgorithm(ALGORITHM);
+ kekObject.setIsSensitive(true);
+ kekObject.setIsExtractable(false);
+ kekObject.setIsToken(true);
+ kekObject.setIsPrivate(true);
+ kekObject.setIsModifiable(false);
+ kekObject.setIsCopyable(false);
+ kekObject.setIsDestroyable(true);
+ kekObject.setAlwaysSensitive(true);
+ kekObject.setNeverExtractable(true);
+
+ kekObjectDao.persist(kekObject);
+
+ logger.info("Created KEK with label {} for purpose {} (PKCS#11 object ID: {})", label, purpose,
+ kekObject.getId());
+ return label;
+
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to create KEK: " + e.getMessage(), e);
+ } finally {
+ Arrays.fill(kekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public void deleteKek(String kekId) throws KMSException {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekId);
+ if (kekObject == null) {
+ throw KMSException.kekNotFound("KEK with label " + kekId + " not found");
+ }
+
+ try {
+ kekObjectDao.remove(kekObject.getId());
+
+ if (kekObject.getKeyMaterial() != null) {
+ Arrays.fill(kekObject.getKeyMaterial(), (byte) 0);
+ }
+
+ logger.warn("Deleted KEK with label {}. All DEKs wrapped with this KEK are now unrecoverable!", kekId);
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to delete KEK: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isKekAvailable(String kekId) throws KMSException {
+ try {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekId);
+ return kekObject != null && kekObject.getRemoved() == null && kekObject.getKeyMaterial() != null;
+ } catch (Exception e) {
+ logger.warn("Error checking KEK availability: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel,
+ Long hsmProfileId) throws KMSException {
+ // Database provider ignores hsmProfileId
+ return wrapKey(plainKey, purpose, kekLabel);
+ }
+
+ @Override
+ public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel) throws KMSException {
+ if (plainKey == null || plainKey.length == 0) {
+ throw KMSException.invalidParameter("Plain key cannot be null or empty");
+ }
+
+ byte[] kekBytes = loadKek(kekLabel);
+
+ try {
+ // Tink's AesGcmJce automatically generates a random IV and prepends it to the ciphertext
+ AesGcmJce aesgcm = new AesGcmJce(kekBytes);
+ byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]);
+
+ WrappedKey wrapped = new WrappedKey(kekLabel, purpose, ALGORITHM, wrappedBlob, PROVIDER_NAME, new Date(),
+ null);
+
+ logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
+ return wrapped;
+ } catch (Exception e) {
+ throw KMSException.wrapUnwrapFailed("Failed to wrap key: " + e.getMessage(), e);
+ } finally {
+ // Zeroize KEK
+ Arrays.fill(kekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public byte[] unwrapKey(WrappedKey wrappedKey, Long hsmProfileId) throws KMSException {
+ // Database provider ignores hsmProfileId
+ return unwrapKey(wrappedKey);
+ }
+
+ @Override
+ public byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException {
+ if (wrappedKey == null) {
+ throw KMSException.invalidParameter("Wrapped key cannot be null");
+ }
+
+ byte[] kekBytes = loadKek(wrappedKey.getKekId());
+
+ try {
+ AesGcmJce aesgcm = new AesGcmJce(kekBytes);
+ // Tink's decrypt expects [IV][ciphertext+tag] format (same as encrypt returns)
+ byte[] blob = wrappedKey.getWrappedKeyMaterial();
+ if (blob.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
+ throw new KMSException(KMSException.ErrorType.WRAP_UNWRAP_FAILED,
+ "Invalid wrapped key format: too short");
+ }
+
+ byte[] plainKey = aesgcm.decrypt(blob, new byte[0]);
+
+ logger.debug("Unwrapped {} key with KEK {}", wrappedKey.getPurpose(), wrappedKey.getKekId());
+ return plainKey;
+
+ } catch (KMSException e) {
+ throw e;
+ } catch (Exception e) {
+ throw KMSException.wrapUnwrapFailed("Failed to unwrap key: " + e.getMessage(), e);
+ } finally {
+ // Zeroize KEK
+ Arrays.fill(kekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits,
+ Long hsmProfileId) throws KMSException {
+ // Database provider ignores hsmProfileId
+ return generateAndWrapDek(purpose, kekLabel, keyBits);
+ }
+
+ @Override
+ public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException {
+ if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
+ throw KMSException.invalidParameter("DEK size must be 128, 192, or 256 bits");
+ }
+
+ byte[] dekBytes = new byte[keyBits / 8];
+ secureRandom.nextBytes(dekBytes);
+
+ try {
+ return wrapKey(dekBytes, purpose, kekLabel);
+ } finally {
+ // Zeroize DEK (wrapped version is in WrappedKey)
+ Arrays.fill(dekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel,
+ Long targetHsmProfileId) throws KMSException {
+ // Database provider ignores targetHsmProfileId
+ return rewrapKey(oldWrappedKey, newKekLabel);
+ }
+
+ @Override
+ public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
+ byte[] plainKey = unwrapKey(oldWrappedKey);
+ try {
+ return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel);
+ } finally {
+ // Zeroize plaintext DEK
+ Arrays.fill(plainKey, (byte) 0);
+ }
+ }
+
+ @Override
+ public boolean healthCheck() throws KMSException {
+ try {
+ if (kekObjectDao == null) {
+ logger.error("KMSDatabaseKekObjectDao is not initialized");
+ return false;
+ }
+ return true;
+
+ } catch (Exception e) {
+ throw KMSException.healthCheckFailed("Health check failed: " + e.getMessage(), e);
+ }
+ }
+
+ private byte[] loadKek(String kekLabel) throws KMSException {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekLabel);
+
+ if (kekObject == null || kekObject.getRemoved() != null) {
+ throw KMSException.kekNotFound("KEK with label " + kekLabel + " not found");
+ }
+
+ try {
+ byte[] encryptedKekBytes = kekObject.getKeyMaterial();
+ if (encryptedKekBytes == null || encryptedKekBytes.length == 0) {
+ throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
+ }
+
+ String encryptedKek = new String(encryptedKekBytes, StandardCharsets.UTF_8);
+ String kekBase64 = DBEncryptionUtil.decrypt(encryptedKek);
+ byte[] kekBytes = Base64.getDecoder().decode(kekBase64);
+
+ updateLastUsed(kekLabel);
+
+ return kekBytes;
+
+ } catch (IllegalArgumentException e) {
+ throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to decrypt KEK for label " + kekLabel + ": " + e.getMessage(),
+ e);
+ }
+ }
+
+ private void updateLastUsed(String kekLabel) {
+ try {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekLabel);
+ if (kekObject != null && kekObject.getRemoved() == null) {
+ kekObject.setLastUsed(new Date());
+ kekObjectDao.update(kekObject.getId(), kekObject);
+ }
+ } catch (Exception e) {
+ logger.debug("Failed to update last used timestamp for KEK {}: {}", kekLabel, e.getMessage());
+ }
+ }
+
+ /**
+ * Seeds the default database HSM profile if it does not already exist.
+ * This runs at provider startup to avoid FK constraint issues that occur
+ * when the INSERT is placed in the schema upgrade SQL script (the account
+ * table may not yet be populated when the upgrade script executes on a
+ * fresh install).
+ */
+ private void ensureDefaultHSMProfile() {
+ try {
+ SearchBuilder
+ * This table stores KEKs (Key Encryption Keys) in a PKCS#11-compatible format,
+ * allowing the database provider to mock PKCS#11 interface behavior.
+ */
+@Entity
+@Table(name = "kms_database_kek_objects")
+public class KMSDatabaseKekObjectVO {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "uuid", nullable = false)
+ private String uuid;
+
+ // PKCS#11 Object Class (CKA_CLASS)
+ @Column(name = "object_class", nullable = false, length = 32)
+ private String objectClass = "CKO_SECRET_KEY";
+
+ // PKCS#11 Label (CKA_LABEL) - human-readable identifier
+ @Column(name = "label", nullable = false, length = 255)
+ private String label;
+
+ // PKCS#11 ID (CKA_ID) - application-defined identifier
+ @Column(name = "object_id", length = 64)
+ private byte[] objectId;
+
+ // PKCS#11 Key Type (CKA_KEY_TYPE)
+ @Column(name = "key_type", nullable = false, length = 32)
+ private String keyType = "CKK_AES";
+
+ // PKCS#11 Key Value (CKA_VALUE) - encrypted KEK material
+ @Column(name = "key_material", nullable = false, length = 512)
+ private byte[] keyMaterial;
+
+ // PKCS#11 Boolean Attributes
+ @Column(name = "is_sensitive", nullable = false)
+ private Boolean isSensitive = true;
+
+ @Column(name = "is_extractable", nullable = false)
+ private Boolean isExtractable = false;
+
+ @Column(name = "is_token", nullable = false)
+ private Boolean isToken = true;
+
+ @Column(name = "is_private", nullable = false)
+ private Boolean isPrivate = true;
+
+ @Column(name = "is_modifiable", nullable = false)
+ private Boolean isModifiable = false;
+
+ @Column(name = "is_copyable", nullable = false)
+ private Boolean isCopyable = false;
+
+ @Column(name = "is_destroyable", nullable = false)
+ private Boolean isDestroyable = true;
+
+ @Column(name = "always_sensitive", nullable = false)
+ private Boolean alwaysSensitive = true;
+
+ @Column(name = "never_extractable", nullable = false)
+ private Boolean neverExtractable = true;
+
+ // Key Metadata
+ @Column(name = "purpose", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private KeyPurpose purpose;
+
+ @Column(name = "key_bits", nullable = false)
+ private Integer keyBits;
+
+ @Column(name = "algorithm", nullable = false, length = 64)
+ private String algorithm = "AES/GCM/NoPadding";
+
+ // PKCS#11 Validity Dates
+ @Column(name = "start_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date startDate;
+
+ @Column(name = "end_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date endDate;
+
+ // Lifecycle
+ @Column(name = GenericDao.CREATED_COLUMN, nullable = false)
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date created;
+
+ @Column(name = "last_used")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date lastUsed;
+
+ @Column(name = GenericDao.REMOVED_COLUMN)
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date removed;
+
+ /**
+ * Constructor for creating a new KEK object
+ *
+ * @param label PKCS#11 label (CKA_LABEL)
+ * @param purpose key purpose
+ * @param keyBits key size in bits
+ * @param keyMaterial encrypted key material (CKA_VALUE)
+ */
+ public KMSDatabaseKekObjectVO(String label, KeyPurpose purpose, Integer keyBits, byte[] keyMaterial) {
+ this();
+ this.label = label;
+ this.purpose = purpose;
+ this.keyBits = keyBits;
+ this.keyMaterial = keyMaterial;
+ this.objectId = label.getBytes(); // Use label as object ID by default
+ this.startDate = new Date();
+ }
+
+ public KMSDatabaseKekObjectVO() {
+ this.uuid = UUID.randomUUID().toString();
+ this.created = new Date();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getObjectClass() {
+ return objectClass;
+ }
+
+ public void setObjectClass(String objectClass) {
+ this.objectClass = objectClass;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public byte[] getObjectId() {
+ return objectId;
+ }
+
+ public void setObjectId(byte[] objectId) {
+ this.objectId = objectId;
+ }
+
+ public String getKeyType() {
+ return keyType;
+ }
+
+ public void setKeyType(String keyType) {
+ this.keyType = keyType;
+ }
+
+ public byte[] getKeyMaterial() {
+ return keyMaterial;
+ }
+
+ public void setKeyMaterial(byte[] keyMaterial) {
+ this.keyMaterial = keyMaterial;
+ }
+
+ public Boolean getIsSensitive() {
+ return isSensitive;
+ }
+
+ public void setIsSensitive(Boolean isSensitive) {
+ this.isSensitive = isSensitive;
+ }
+
+ public Boolean getIsExtractable() {
+ return isExtractable;
+ }
+
+ public void setIsExtractable(Boolean isExtractable) {
+ this.isExtractable = isExtractable;
+ }
+
+ public Boolean getIsToken() {
+ return isToken;
+ }
+
+ public void setIsToken(Boolean isToken) {
+ this.isToken = isToken;
+ }
+
+ public Boolean getIsPrivate() {
+ return isPrivate;
+ }
+
+ public void setIsPrivate(Boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ }
+
+ public Boolean getIsModifiable() {
+ return isModifiable;
+ }
+
+ public void setIsModifiable(Boolean isModifiable) {
+ this.isModifiable = isModifiable;
+ }
+
+ public Boolean getIsCopyable() {
+ return isCopyable;
+ }
+
+ public void setIsCopyable(Boolean isCopyable) {
+ this.isCopyable = isCopyable;
+ }
+
+ public Boolean getIsDestroyable() {
+ return isDestroyable;
+ }
+
+ public void setIsDestroyable(Boolean isDestroyable) {
+ this.isDestroyable = isDestroyable;
+ }
+
+ public Boolean getAlwaysSensitive() {
+ return alwaysSensitive;
+ }
+
+ public void setAlwaysSensitive(Boolean alwaysSensitive) {
+ this.alwaysSensitive = alwaysSensitive;
+ }
+
+ public Boolean getNeverExtractable() {
+ return neverExtractable;
+ }
+
+ public void setNeverExtractable(Boolean neverExtractable) {
+ this.neverExtractable = neverExtractable;
+ }
+
+ public KeyPurpose getPurpose() {
+ return purpose;
+ }
+
+ public void setPurpose(KeyPurpose purpose) {
+ this.purpose = purpose;
+ }
+
+ public Integer getKeyBits() {
+ return keyBits;
+ }
+
+ public void setKeyBits(Integer keyBits) {
+ this.keyBits = keyBits;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(Date startDate) {
+ this.startDate = startDate;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(Date endDate) {
+ this.endDate = endDate;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastUsed() {
+ return lastUsed;
+ }
+
+ public void setLastUsed(Date lastUsed) {
+ this.lastUsed = lastUsed;
+ }
+
+ public Date getRemoved() {
+ return removed;
+ }
+
+ public void setRemoved(Date removed) {
+ this.removed = removed;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("KMSDatabaseKekObject %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
+ this, "id", "uuid", "label", "purpose", "keyBits", "objectClass", "keyType", "algorithm"));
+ }
+}
diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java
new file mode 100644
index 000000000000..582c1179ec43
--- /dev/null
+++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.kms.provider.database.dao;
+
+import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.framework.kms.KeyPurpose;
+import org.apache.cloudstack.kms.provider.database.KMSDatabaseKekObjectVO;
+
+import java.util.List;
+
+/**
+ * DAO for KMSDatabaseKekObject entities
+ * Provides PKCS#11-like object storage operations for KEKs
+ */
+public interface KMSDatabaseKekObjectDao extends GenericDao