Copy-EncryptedDB.ps1

<#PSScriptInfo
  .VERSION 1.4
  .GUID 7dcce4d8-6df3-413a-9699-631b0dc915e4
  .AUTHOR Dave Farinelli
  .LICENSEURI https://www.gnu.org/licenses/gpl-3.0.en.html
  .PROJECTURI https://github.com/dfar-io/copy-encrypted-db-script
#>


<#
.DESCRIPTION
 This script allows for copying a Azure SQL Database encrypted using Always
 Encrypted.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
  "PSAvoidUsingPlainTextForPassword", "",
  Justification = "Using for existing calls.")]
Param(
  [Parameter(Mandatory = $True)]
  [string]$destDB,

  [Parameter(Mandatory = $True)]
  [string]$destServer,

  [Parameter(Mandatory = $True)]
  [string]$destRG,

  [Parameter(Mandatory = $True)]
  [string]$destKeyVaultName,

  [Parameter(Mandatory = $True)]
  [string]$destServerUsername,

  [Parameter(Mandatory = $True)]
  [string]$destServerPassword,

  [Parameter(Mandatory = $True)]
  [string]$destDbAppUsername,

  [Parameter(Mandatory = $True)]
  [string]$destDbAppPassword,

  [Parameter(Mandatory = $True)]
  [string]$sourceDB,

  [Parameter(Mandatory = $True)]
  [string]$sourceServer,

  [Parameter(Mandatory = $True)]
  [string]$sourceRG
)

function ExecuteSqlWriteConstruct {
  Param(
    [Parameter(Mandatory = $true)][string]$connectionString,
    [Parameter(Mandatory = $true)][string]$command
  )
  $gscn = New-Object System.Data.SqlClient.SqlConnection
  $gscn.ConnectionString = $connectionString

  $wrCmd = New-Object System.Data.SqlClient.SqlCommand
  $wrCmd.Connection = $gscn
  $wrCmd.CommandTimeout = 0
  $wrCmd.CommandText = $command

  try {
    $gscn.Open()
    $wrCmd.ExecuteNonQuery() | Out-Null
  }
  catch [Exception] {
    Write-Warning $_.Exception.Message
    Write-Warning $command
  }
  finally {
    $gscn.Dispose()
    $wrCmd.Dispose()
  }
}

################################################################################

#Requires -Modules SqlServer
$ErrorActionPreference = "Stop"

az sql db delete -n $destDB -g $destRG -s $destServer

Write-Output `
  "Creating DB '$destDB' on server $destServer (copying from '$sourceDB' on $sourceServer)"
az sql db copy --dest-name $destDB --dest-server $destServer `
  --dest-resource-group $destRG --name $sourceDB `
  --server $sourceServer --resource-group $sourceRG

Write-Output "Creating Key Vault Key..."
$akvKey = az keyvault key create --vault-name $destKeyVaultName `
  --name "AlwaysEncrypted" | ConvertFrom-Json
$cmkSettings = New-SqlAzureKeyVaultColumnMasterKeySettings `
  -KeyURL $akvKey.key.kid
Write-Output "Key Vault Key Created."

Write-Output "Connecting to database server $destServer..."
$connStr = "Server=" + $destServer + ".database.windows.net" +
";Database=" + $destDB +
";User ID=" + $destServerUsername +
";Password=" + $destServerPassword
$connection = New-Object Microsoft.SqlServer.Management.Common.ServerConnection
$connection.ConnectionString = $connStr
try {
  $connection.Connect()
}
catch {
  $errorMessage = $_.Exception.Message
  Write-Error $errorMessage
  exit 1;
}
$server = New-Object Microsoft.SqlServer.Management.Smo.Server($connection)
$database = $server.Databases[$destDB]
Write-Output "Connected to database server $destServer and database $database..."

$cmkName = "CMKAuto"
$existingCmkName = ""

Get-SqlColumnMasterKey -InputObject $database -Name $cmkName `
  -ErrorAction 'SilentlyContinue' | Out-Null
if ($?) {
  Write-Output "Found existing automatic CMK, using alternative automatic CMK."
  $existingCmkName = $cmkName
  $cmkName = "CMKAuto2"
}
else {
  Write-Output "Getting existing CMK..."
  $existingCmk = Get-SqlColumnMasterKey -InputObject $database
  $existingCmkName = $existingCmk.Name
  Write-Output "Found existing CMK $existingCmkName."
}



Write-Output "Creating a column master key for DB $dbName..."
try {
  New-SqlColumnMasterKey -Name $cmkName -InputObject $database `
    -ColumnMasterKeySettings $cmkSettings | Out-Null
}
catch {
  $errorMessage = $_.Exception.Message
  Write-Error $errorMessage
  exit 1;
}
Write-Output "Column master key created."

Write-Output "Rotating Keys (will require sign-in)..."
Add-SqlAzureAuthenticationContext -Interactive
Invoke-SqlColumnMasterKeyRotation `
  -SourceColumnMasterKeyName $existingCmkName `
  -TargetColumnMasterKeyName $cmkName `
  -InputObject $database
Complete-SqlColumnMasterKeyRotation `
  -SourceColumnMasterKeyName $existingCmkName `
  -InputObject $database
Write-Output "Keys rotated."

Write-Output "Deleting existing CMK..."
Remove-SqlColumnMasterKey -Name $existingCmkName -InputObject $database
Write-Output "Existing CMK deleted."

Write-Output "Setting app user/pass..."
ExecuteSqlWriteConstruct -connectionString $connStr `
  -command "DROP USER IF EXISTS $destDbAppUsername"
ExecuteSqlWriteConstruct -connectionString $connStr `
  -command "CREATE USER $destDbAppUsername WITH PASSWORD = '$destDbAppPassword'"
ExecuteSqlWriteConstruct -connectionString $connStr `
  -command "EXEC sp_addrolemember N'db_owner', N'$destDbAppUsername'"
Write-Output "App user/pass set."