/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for * license information. */ package com.microsoft.azure.management.compute.implementation; import com.microsoft.azure.SubResource; import com.microsoft.azure.management.compute.DiskEncryptionSettings; import com.microsoft.azure.management.compute.DiskVolumeEncryptionMonitor; import com.microsoft.azure.management.compute.DiskVolumeType; import com.microsoft.azure.management.compute.EncryptionStatus; import com.microsoft.azure.management.compute.KeyVaultKeyReference; import com.microsoft.azure.management.compute.KeyVaultSecretReference; import com.microsoft.azure.management.compute.OperatingSystemTypes; import com.microsoft.azure.management.compute.VirtualMachine; import com.microsoft.azure.management.compute.VirtualMachineEncryptionConfiguration; import com.microsoft.azure.management.compute.VirtualMachineExtension; import com.microsoft.azure.management.compute.VirtualMachineExtensionInstanceView; import rx.Observable; import rx.functions.Func0; import rx.functions.Func1; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.UUID; /** * Helper type to enable or disable virtual machine disk (OS, Data) encryption. */ class VirtualMachineEncryptionHelper { private final String encryptionExtensionPublisher = "Microsoft.Azure.Security"; private final OperatingSystemTypes osType; private final VirtualMachine virtualMachine; // Error messages private static final String ERROR_ENCRYPTION_EXTENSION_NOT_FOUND = "Expected encryption extension not found in the VM"; private static final String ERROR_NON_SUCCESS_PROVISIONING_STATE = "Extension needed for disk encryption was not provisioned correctly, found ProvisioningState as '%s'"; private static final String ERROR_EXPECTED_KEY_VAULT_URL_NOT_FOUND = "Could not found URL pointing to the secret for disk encryption"; private static final String ERROR_EXPECTED_ENCRYPTION_EXTENSION_STATUS_NOT_FOUND = "Encryption extension with successful status not found in the VM"; private static final String ERROR_ENCRYPTION_EXTENSION_STATUS_IS_EMPTY = "Encryption extension status is empty"; private static final String ERROR_ON_LINUX_DECRYPTING_NON_DATA_DISK_IS_NOT_SUPPORTED = "Only data disk is supported to disable encryption on Linux VM"; private static final String ERROR_ON_LINUX_DATA_DISK_DECRYPT_NOT_ALLOWED_IF_OS_DISK_IS_ENCRYPTED = "On Linux VM disabling data disk encryption is allowed only if OS disk is not encrypted"; /** * Creates VirtualMachineEncryptionHelper. * * @param virtualMachine the virtual machine to enable or disable encryption */ VirtualMachineEncryptionHelper(final VirtualMachine virtualMachine) { this.virtualMachine = virtualMachine; this.osType = this.virtualMachine.osType(); } /** * Enables encryption. * * @param encryptionSettings the settings to be used for encryption extension * @param <T> the Windows or Linux encryption settings * @return an observable that emits the encryption status */ <T extends VirtualMachineEncryptionConfiguration<T>> Observable<DiskVolumeEncryptionMonitor> enableEncryptionAsync(final VirtualMachineEncryptionConfiguration<T> encryptionSettings) { final EnableDisableEncryptConfig encryptConfig = new EnableEncryptConfig(encryptionSettings); // Update the encryption extension if already installed return updateEncryptionExtensionAsync(encryptConfig) // If encryption extension is not installed then install it .switchIfEmpty(installEncryptionExtensionAsync(encryptConfig)) // Retrieve the encryption key URL after extension install or update .flatMap(new Func1<VirtualMachine, Observable<String>>() { @Override public Observable<String> call(VirtualMachine virtualMachine) { return retrieveEncryptionExtensionStatusStringAsync(ERROR_EXPECTED_KEY_VAULT_URL_NOT_FOUND); } }) // Update the VM's OS Disk (in storage profile) with the encryption metadata .flatMap(new Func1<String, Observable<VirtualMachine>>() { @Override public Observable<VirtualMachine> call(String keyVaultSecretUrl) { return updateVMStorageProfileAsync(encryptConfig, keyVaultSecretUrl); } }) // Gets the encryption status .flatMap(new Func1<VirtualMachine, Observable<DiskVolumeEncryptionMonitor>>() { @Override public Observable<DiskVolumeEncryptionMonitor> call(VirtualMachine virtualMachine) { return getDiskVolumeEncryptDecryptStatusAsync(virtualMachine); } }); } /** * Disables encryption on the given disk volume. * * @param volumeType the disk volume * @return an observable that emits the decryption status */ Observable<DiskVolumeEncryptionMonitor> disableEncryptionAsync(final DiskVolumeType volumeType) { final EnableDisableEncryptConfig encryptConfig = new DisableEncryptConfig(volumeType); return validateBeforeDecryptAsync(volumeType) // Update the encryption extension if already installed .flatMap(new Func1<Boolean, Observable<VirtualMachine>>() { @Override public Observable<VirtualMachine> call(Boolean aBoolean) { return updateEncryptionExtensionAsync(encryptConfig); } }) // If encryption extension is not then install it .switchIfEmpty(installEncryptionExtensionAsync(encryptConfig)) // Validate and retrieve the encryption extension status .flatMap(new Func1<VirtualMachine, Observable<String>>() { @Override public Observable<String> call(VirtualMachine virtualMachine) { return retrieveEncryptionExtensionStatusStringAsync(ERROR_ENCRYPTION_EXTENSION_STATUS_IS_EMPTY); } }) // Update the VM's OS profile by marking encryption disabled .flatMap(new Func1<String, Observable<VirtualMachine>>() { @Override public Observable<VirtualMachine> call(String status) { return updateVMStorageProfileAsync(encryptConfig); } }) // Gets the encryption status .flatMap(new Func1<VirtualMachine, Observable<DiskVolumeEncryptionMonitor>>() { @Override public Observable<DiskVolumeEncryptionMonitor> call(VirtualMachine virtualMachine) { return getDiskVolumeEncryptDecryptStatusAsync(virtualMachine); } }); } /** * @return OS specific encryption extension type */ private String encryptionExtensionType() { if (this.osType == OperatingSystemTypes.LINUX) { return "AzureDiskEncryptionForLinux"; } else { return "AzureDiskEncryption"; } } /** * @return OS specific encryption extension version */ private String encryptionExtensionVersion() { if (this.osType == OperatingSystemTypes.LINUX) { return "0.1"; } else { return "1.1"; } } /** * Checks the given volume type in the virtual machine can be decrypted. * * @param volumeType the volume type to decrypt * @return observable that emit true if no validation error otherwise error observable */ private Observable<Boolean> validateBeforeDecryptAsync(final DiskVolumeType volumeType) { if (osType == OperatingSystemTypes.LINUX) { if (volumeType != DiskVolumeType.DATA) { return toErrorObservable(ERROR_ON_LINUX_DECRYPTING_NON_DATA_DISK_IS_NOT_SUPPORTED); } return getDiskVolumeEncryptDecryptStatusAsync(virtualMachine) .flatMap(new Func1<DiskVolumeEncryptionMonitor, Observable<Boolean>>() { @Override public Observable<Boolean> call(DiskVolumeEncryptionMonitor status) { if (status.osDiskStatus().equals(EncryptionStatus.ENCRYPTED)) { return toErrorObservable(ERROR_ON_LINUX_DATA_DISK_DECRYPT_NOT_ALLOWED_IF_OS_DISK_IS_ENCRYPTED); } return Observable.just(true); } }); } return Observable.just(true); } /** * Retrieves encryption extension installed in the virtual machine, if the extension is * not installed then return an empty observable. * * @return an observable that emits the encryption extension installed in the virtual machine */ private Observable<VirtualMachineExtension> getEncryptionExtensionInstalledInVMAsync() { return virtualMachine.listExtensionsAsync() // firstOrDefault() is used intentionally here instead of first() to ensure // this method return empty observable if matching extension is not found. // .firstOrDefault(null, new Func1<VirtualMachineExtension, Boolean>() { @Override public Boolean call(final VirtualMachineExtension extension) { return extension.publisherName().equalsIgnoreCase(encryptionExtensionPublisher) && extension.typeName().equalsIgnoreCase(encryptionExtensionType()); } }).flatMap(new Func1<VirtualMachineExtension, Observable<VirtualMachineExtension>>() { @Override public Observable<VirtualMachineExtension> call(VirtualMachineExtension extension) { if (extension == null) { return Observable.empty(); } return Observable.just(extension); } }); } /** * Updates the encryption extension in the virtual machine using provided configuration. * If extension is not installed then this method return empty observable. * * @param encryptConfig the volume encryption configuration * @return an observable that emits updated virtual machine */ private Observable<VirtualMachine> updateEncryptionExtensionAsync(final EnableDisableEncryptConfig encryptConfig) { return getEncryptionExtensionInstalledInVMAsync() .flatMap(new Func1<VirtualMachineExtension, Observable<VirtualMachine>>() { @Override public Observable<VirtualMachine> call(final VirtualMachineExtension encryptionExtension) { final HashMap<String, Object> publicSettings = encryptConfig.extensionPublicSettings(); return virtualMachine.update() .updateExtension(encryptionExtension.name()) .withPublicSettings(publicSettings) .withProtectedSettings(encryptConfig.extensionProtectedSettings()) .parent() .applyAsync(); } }); } /** * Prepare encryption extension using provided configuration and install it in the virtual machine. * * @param encryptConfig the volume encryption configuration * @return an observable that emits updated virtual machine */ private Observable<VirtualMachine> installEncryptionExtensionAsync(final EnableDisableEncryptConfig encryptConfig) { return Observable.defer(new Func0<Observable<VirtualMachine>>() { @Override public Observable<VirtualMachine> call() { final String extensionName = encryptionExtensionType(); return virtualMachine.update() .defineNewExtension(extensionName) .withPublisher(encryptionExtensionPublisher) .withType(encryptionExtensionType()) .withVersion(encryptionExtensionVersion()) .withPublicSettings(encryptConfig.extensionPublicSettings()) .withProtectedSettings(encryptConfig.extensionProtectedSettings()) .withMinorVersionAutoUpgrade() .attach() .applyAsync(); } }); } /** * Retrieves the encryption extension status from the extension instance view. * An error observable will be returned if * 1. extension is not installed * 2. extension is not provisioned successfully * 2. extension status could be retrieved (either not found or empty) * * @param statusEmptyErrorMessage the error message to emit if unable to locate the status * @return an observable that emits status message */ private Observable<String> retrieveEncryptionExtensionStatusStringAsync(final String statusEmptyErrorMessage) { final VirtualMachineEncryptionHelper self = this; return getEncryptionExtensionInstalledInVMAsync() .switchIfEmpty(self.<VirtualMachineExtension>toErrorObservable(ERROR_ENCRYPTION_EXTENSION_NOT_FOUND)) .flatMap(new Func1<VirtualMachineExtension, Observable<VirtualMachineExtensionInstanceView>>() { @Override public Observable<VirtualMachineExtensionInstanceView> call(VirtualMachineExtension extension) { if (!extension.provisioningState().equalsIgnoreCase("Succeeded")) { return self.toErrorObservable((String.format(ERROR_NON_SUCCESS_PROVISIONING_STATE, extension.provisioningState()))); } return extension.getInstanceViewAsync(); } }) .flatMap(new Func1<VirtualMachineExtensionInstanceView, Observable<String>>() { @Override public Observable<String> call(VirtualMachineExtensionInstanceView instanceView) { if (instanceView == null || instanceView.statuses() == null || instanceView.statuses().size() == 0) { return self.toErrorObservable(ERROR_EXPECTED_ENCRYPTION_EXTENSION_STATUS_NOT_FOUND); } String extensionStatus = instanceView.statuses().get(0).message(); if (extensionStatus == null) { return self.toErrorObservable(statusEmptyErrorMessage); } return Observable.just(extensionStatus); } }); } /** * Updates the virtual machine's OS Disk model with the encryption specific details so that platform can * use it while booting the virtual machine. * * @param encryptConfig the configuration specific to enabling the encryption * @param encryptionSecretKeyVaultUrl the keyVault URL pointing to secret holding disk encryption key * @return an observable that emits updated virtual machine */ private Observable<VirtualMachine> updateVMStorageProfileAsync(final EnableDisableEncryptConfig encryptConfig, final String encryptionSecretKeyVaultUrl) { DiskEncryptionSettings diskEncryptionSettings = encryptConfig.storageProfileEncryptionSettings(); diskEncryptionSettings.diskEncryptionKey() .withSecretUrl(encryptionSecretKeyVaultUrl); return virtualMachine.update() .withOSDiskEncryptionSettings(diskEncryptionSettings) .applyAsync(); } /** * Updates the virtual machine's OS Disk model with the encryption specific details. * * @param encryptConfig the configuration specific to disabling the encryption * @return an observable that emits updated virtual machine */ private Observable<VirtualMachine> updateVMStorageProfileAsync(final EnableDisableEncryptConfig encryptConfig) { DiskEncryptionSettings diskEncryptionSettings = encryptConfig.storageProfileEncryptionSettings(); return virtualMachine.update() .withOSDiskEncryptionSettings(diskEncryptionSettings) .applyAsync(); } /** * Gets status object that describes the current status of the volume encryption or decryption process. * * @param virtualMachine the virtual machine on which encryption or decryption is running * @return an observable that emits current encrypt or decrypt status */ private Observable<DiskVolumeEncryptionMonitor> getDiskVolumeEncryptDecryptStatusAsync(VirtualMachine virtualMachine) { if (osType == OperatingSystemTypes.LINUX) { return new LinuxDiskVolumeEncryptionMonitorImpl(virtualMachine.id(), virtualMachine.manager()).refreshAsync(); } else { return new WindowsVolumeEncryptionMonitorImpl(virtualMachine.id(), virtualMachine.manager()).refreshAsync(); } } /** * Wraps the given message in an error observable. * * @param message the error message * @param <ResultT> observable type * @return error observable with message wrapped */ private <ResultT> Observable<ResultT> toErrorObservable(String message) { return Observable.error(new Exception(message)); } /** * Base type representing configuration for enabling and disabling disk encryption. */ private abstract class EnableDisableEncryptConfig { /** * @return encryption specific settings to be set on virtual machine storage profile */ public abstract DiskEncryptionSettings storageProfileEncryptionSettings(); /** * @return encryption extension public settings */ public abstract HashMap<String, Object> extensionPublicSettings(); /** * @return encryption extension protected settings */ public abstract HashMap<String, Object> extensionProtectedSettings(); } /** * Base type representing configuration for enabling disk encryption. * * @param <T> */ private class EnableEncryptConfig<T extends VirtualMachineEncryptionConfiguration<T>> extends EnableDisableEncryptConfig { private final VirtualMachineEncryptionConfiguration<T> settings; EnableEncryptConfig(final VirtualMachineEncryptionConfiguration<T> settings) { this.settings = settings; } @Override public DiskEncryptionSettings storageProfileEncryptionSettings() { KeyVaultKeyReference keyEncryptionKey = null; if (settings.keyEncryptionKeyURL() != null) { keyEncryptionKey = new KeyVaultKeyReference(); keyEncryptionKey.withKeyUrl(settings.keyEncryptionKeyURL()); if (settings.keyEncryptionKeyVaultId() != null) { keyEncryptionKey.withSourceVault(new SubResource().withId(settings.keyEncryptionKeyVaultId())); } } DiskEncryptionSettings diskEncryptionSettings = new DiskEncryptionSettings(); diskEncryptionSettings .withEnabled(true) .withKeyEncryptionKey(keyEncryptionKey) .withDiskEncryptionKey(new KeyVaultSecretReference()) .diskEncryptionKey() .withSourceVault(new SubResource().withId(settings.keyVaultId())); return diskEncryptionSettings; } @Override public HashMap<String, Object> extensionPublicSettings() { HashMap<String, Object> publicSettings = new LinkedHashMap<>(); publicSettings.put("EncryptionOperation", "EnableEncryption"); publicSettings.put("AADClientID", settings.aadClientId()); publicSettings.put("KeyEncryptionAlgorithm", settings.volumeEncryptionKeyEncryptAlgorithm()); publicSettings.put("KeyVaultURL", settings.keyVaultUrl()); publicSettings.put("VolumeType", settings.volumeType().toString()); publicSettings.put("SequenceVersion", UUID.randomUUID()); if (settings.keyEncryptionKeyURL() != null) { publicSettings.put("KeyEncryptionKeyURL", settings.keyEncryptionKeyURL()); } return publicSettings; } @Override public HashMap<String, Object> extensionProtectedSettings() { HashMap<String, Object> protectedSettings = new LinkedHashMap<>(); protectedSettings.put("AADClientSecret", settings.aadSecret()); if (settings.osType() == OperatingSystemTypes.LINUX && settings.linuxPassPhrase() != null) { protectedSettings.put("Passphrase", settings.linuxPassPhrase()); } return protectedSettings; } } /** * Base type representing configuration for disabling disk encryption. */ private class DisableEncryptConfig extends EnableDisableEncryptConfig { private final DiskVolumeType volumeType; DisableEncryptConfig(final DiskVolumeType volumeType) { this.volumeType = volumeType; } @Override public DiskEncryptionSettings storageProfileEncryptionSettings() { DiskEncryptionSettings diskEncryptionSettings = new DiskEncryptionSettings(); diskEncryptionSettings .withEnabled(false); return diskEncryptionSettings; } @Override public HashMap<String, Object> extensionPublicSettings() { HashMap<String, Object> publicSettings = new LinkedHashMap<>(); publicSettings.put("EncryptionOperation", "DisableEncryption"); publicSettings.put("SequenceVersion", UUID.randomUUID()); publicSettings.put("VolumeType", this.volumeType); return publicSettings; } @Override public HashMap<String, Object> extensionProtectedSettings() { return new LinkedHashMap<>(); } } }