diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 73047c4851a..c212228e256 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -314,8 +314,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati newVol.setFormat(oldVol.getFormat()); if (oldVol.getPassphraseId() != null) { - PassphraseVO passphrase =_passphraseDao.persist(new PassphraseVO()); - passphrase.clearPassphrase(); + PassphraseVO passphrase =_passphraseDao.persist(new PassphraseVO(true)); newVol.setPassphraseId(passphrase.getId()); } @@ -1689,8 +1688,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati } s_logger.debug("Creating passphrase for the volume: " + volume.getName()); long startTime = System.currentTimeMillis(); - PassphraseVO passphrase = _passphraseDao.persist(new PassphraseVO()); - passphrase.clearPassphrase(); + PassphraseVO passphrase = _passphraseDao.persist(new PassphraseVO(true)); volume.setPassphraseId(passphrase.getId()); long finishTime = System.currentTimeMillis(); s_logger.debug("Creating and persisting passphrase took: " + (finishTime - startTime) + " ms for the volume: " + volume.toString()); diff --git a/engine/schema/src/main/java/org/apache/cloudstack/secret/PassphraseVO.java b/engine/schema/src/main/java/org/apache/cloudstack/secret/PassphraseVO.java index 44557b97abb..73c839ceb8b 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/secret/PassphraseVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/secret/PassphraseVO.java @@ -2,6 +2,7 @@ package org.apache.cloudstack.secret; import com.cloud.utils.db.Encrypt; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.StringUtils; import javax.persistence.Column; import javax.persistence.Entity; @@ -9,6 +10,7 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; + import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; @@ -24,32 +26,39 @@ public class PassphraseVO { @Column(name = "passphrase") @Encrypt - private byte[] passphrase; + private String passphrase; public PassphraseVO() { - try { - SecureRandom random = SecureRandom.getInstanceStrong(); - byte[] temporary = new byte[48]; // 48 byte random passphrase buffer - this.passphrase = new byte[64]; // 48 byte random passphrase as base64 for usability - random.nextBytes(temporary); - Base64.getEncoder().encode(temporary, this.passphrase); - Arrays.fill(temporary, (byte) 0); // clear passphrase from buffer - } catch (NoSuchAlgorithmException ex ) { - throw new CloudRuntimeException("Volume encryption requested but system is missing specified algorithm to generate passphrase"); + } + + public PassphraseVO(boolean initialize) { + if (initialize) { + try { + SecureRandom random = SecureRandom.getInstanceStrong(); + byte[] temporary = new byte[48]; // 48 byte random passphrase buffer + random.nextBytes(temporary); + this.passphrase = Base64.getEncoder().encodeToString(temporary); + Arrays.fill(temporary, (byte) 0); // clear passphrase from buffer + } catch (NoSuchAlgorithmException ex ) { + throw new CloudRuntimeException("Volume encryption requested but system is missing specified algorithm to generate passphrase"); + } } } public PassphraseVO(PassphraseVO existing) { - this.passphrase = existing.getPassphrase(); + this.passphrase = existing.getPassphraseString(); } - public void clearPassphrase() { - if (this.passphrase != null) { - Arrays.fill(this.passphrase, (byte) 0); + public byte[] getPassphrase() { + if (StringUtils.isBlank(this.passphrase)) { + return new byte[]{}; } + return this.passphrase.getBytes(); } - public byte[] getPassphrase() { return this.passphrase; } + public String getPassphraseString() { + return this.passphrase; + } public Long getId() { return this.id; } } diff --git a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java index 88830b3e3f9..6017dca58c1 100644 --- a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java +++ b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java @@ -486,6 +486,8 @@ public class EncryptionSecretKeyChanger { migrateImageStoreUrlForCifs(conn); migrateStoragePoolPathForSMB(conn); + preparePassphraseTableForMigration(conn); + // migrate columns with annotation @Encrypt migrateEncryptedTableColumns(conn); @@ -665,6 +667,56 @@ public class EncryptionSecretKeyChanger { System.out.println("End migrate user vm deploy_as_is details"); } + // encrypt any unencrypted passphrases using old style encryptor before we migrate + private void preparePassphraseTableForMigration(Connection conn) throws SQLException { + System.out.println("Preparing passphrase table by checking for unencrypted passphrases"); + + try(PreparedStatement selectPstmt = conn.prepareStatement("SELECT id, passphrase FROM passphrase"); + ResultSet rs = selectPstmt.executeQuery(); + PreparedStatement updatePstmt = conn.prepareStatement("UPDATE passphrase SET passphrase=? WHERE id=?") + ) { + while(rs.next()) { + long id = rs.getLong(1); + String value = rs.getString(2); + if (StringUtils.isBlank(value)) { + continue; + } + + // passphrases are 64 bytes long when unencrypted, longer when encrypted + if (value.length() == 64) { + // just confirm it won't decrypt, to be safe, before assuming raw value and encrypting + try { + oldEncryptor.decrypt(value); + System.out.printf("Passphrase table entry db id %d was already encrypted with old encryption\n", id); + } catch(EncryptionException | CloudRuntimeException ex) { + String message = null; + if (ex instanceof CloudRuntimeException && ex.getCause() != null) { + if ((ex.getCause() instanceof EncryptionException)) { + message = ex.getCause().getMessage(); + } + } else if (ex instanceof EncryptionException) { + message = ex.getMessage(); + } + + if (message != null && message.contains("Failed to decrypt")) { + System.out.printf("Encrypting unencrypted passphrase table entry db id %d before migration using old encryption\n", id); + String encrypted = oldEncryptor.encrypt(value); + updatePstmt.setBytes(1, encrypted.getBytes(StandardCharsets.UTF_8)); + updatePstmt.setLong(2, id); + updatePstmt.executeUpdate(); + } else { + throwCloudRuntimeException("Unhandled EncryptionException", ex); + } + } + } + } + } catch (SQLException e) { + throwCloudRuntimeException("Unable to prepare passphrase table", e); + } + + System.out.println("End preparing passphrase table"); + } + private void migrateImageStoreUrlForCifs(Connection conn) { System.out.println("Begin migrate image store url if protocol is cifs"); diff --git a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/cryptsetup/CryptSetupTest.java b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/cryptsetup/CryptSetupTest.java index e031178a465..8c5446c3b38 100644 --- a/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/cryptsetup/CryptSetupTest.java +++ b/plugins/hypervisors/kvm/src/test/java/org/apache/cloudstack/utils/cryptsetup/CryptSetupTest.java @@ -33,7 +33,7 @@ public class CryptSetupTest { file.close(); String filePath = path.toAbsolutePath().toString(); - PassphraseVO passphrase = new PassphraseVO(); + PassphraseVO passphrase = new PassphraseVO(true); cryptSetup.luksFormat(passphrase.getPassphrase(), CryptSetup.LuksType.LUKS, filePath);