Skip to content

Commit 70cbf4c

Browse files
authored
Merge pull request #1261 from Pietervanhove/AlwaysEncryptedDemos
Sample application using AE with VBS enclaves
2 parents 771b7e2 + d542802 commit 70cbf4c

File tree

8 files changed

+429
-0
lines changed

8 files changed

+429
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.8.34408.163
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlwaysEncryptedConsole", "AlwaysEncryptedConsole\AlwaysEncryptedConsole.csproj", "{66F6F7D3-3B6E-4460-921F-8E6127C73F33}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{66F6F7D3-3B6E-4460-921F-8E6127C73F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{66F6F7D3-3B6E-4460-921F-8E6127C73F33}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{66F6F7D3-3B6E-4460-921F-8E6127C73F33}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{66F6F7D3-3B6E-4460-921F-8E6127C73F33}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {B18CFE54-1A5E-4299-96CE-11F0DCB01A9D}
24+
EndGlobalSection
25+
EndGlobal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<EnforceCodeStyleInBuild>False</EnforceCodeStyleInBuild>
9+
<AnalysisLevel>none</AnalysisLevel>
10+
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Azure.Identity" Version="1.10.4" />
15+
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.5.0" />
16+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.4" />
17+
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" Version="3.0.0" />
18+
</ItemGroup>
19+
20+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.5.002.0
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlwaysEncryptedConsole", "AlwaysEncryptedConsole.csproj", "{D51BF679-04F7-40F7-8652-2921AA73593F}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{D51BF679-04F7-40F7-8652-2921AA73593F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{D51BF679-04F7-40F7-8652-2921AA73593F}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{D51BF679-04F7-40F7-8652-2921AA73593F}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{D51BF679-04F7-40F7-8652-2921AA73593F}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {30E31893-7EC2-4FC1-91E4-C3781F5FF081}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//*********************************************************
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// This code is licensed under the MIT License (MIT).
4+
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
5+
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
6+
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
7+
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
8+
//
9+
// Author: Michael Howard, Azure Data Security
10+
//*********************************************************
11+
12+
using Microsoft.Data.SqlClient;
13+
14+
partial class Program
15+
{
16+
// Displays rows and cols from a SqlDataReader query result
17+
public static void DumpData(SqlDataReader? data)
18+
{
19+
if (data is null)
20+
{
21+
Console.WriteLine("No data");
22+
return;
23+
}
24+
25+
// get column headers
26+
Console.WriteLine("Fetching Data");
27+
for (int i = 0; i < data.FieldCount; i++)
28+
Console.Write(data.GetName(i) + ", ");
29+
30+
Console.WriteLine();
31+
32+
// get data
33+
while (data.Read())
34+
{
35+
for (int i = 0; i < data.FieldCount; i++)
36+
{
37+
var value = data.GetValue(i);
38+
39+
if (value is not null)
40+
{
41+
var type = data.GetFieldType(i);
42+
43+
// if the data is a byte array (ie; ciphertext)
44+
// dump the first 16 bytes of hex string
45+
if (type == typeof(byte[]))
46+
// Possible null reference argument. There *IS* a check two lines up!
47+
#pragma warning disable CS8604
48+
value = ByteArrayToHexString(value as byte[], 16);
49+
#pragma warning restore CS8604
50+
}
51+
else
52+
{
53+
value = "?";
54+
}
55+
56+
Console.Write(value + ", ");
57+
}
58+
Console.WriteLine();
59+
}
60+
}
61+
}
62+
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//*********************************************************
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// This code is licensed under the MIT License (MIT).
4+
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
5+
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
6+
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
7+
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
8+
//
9+
// Author: Michael Howard, Azure Data Security
10+
//*********************************************************
11+
12+
//*********************************************************
13+
// Demo Steps
14+
// Step 1
15+
// Run, as-is, AE is set to false,
16+
// and the code will return ciphertext
17+
//
18+
// Step 2
19+
// Set useAlwaysEncrypted to true (line 47)
20+
// Re-run code, will fail because of no params
21+
//
22+
// Step 3
23+
// Set testWithParams to true (line 50)
24+
// Re-run code, but will fail because of no AKV
25+
//
26+
// Step 4
27+
// Set registerAkv4Ae to true (line 53)
28+
// Re-run. At this point everything should work.
29+
// Two queries, the first is slow because intial authn/authz/column metadata
30+
// Second is much faster
31+
32+
using Azure.Core;
33+
using Microsoft.Data.SqlClient;
34+
using System.Data;
35+
using System.Diagnostics;
36+
37+
partial class Program
38+
{
39+
static void Main()
40+
{
41+
// START all these flags should be false
42+
43+
// Demo step 1 will not use AE,
44+
// and you will only see the SSN and Salary columns as ciphertext
45+
46+
// Demo step 2 set this to true
47+
bool useAlwaysEncrypted = true;
48+
49+
// Demo step 3 set this to true
50+
bool testWithParams = true;
51+
52+
// Demo step 4 set this to true
53+
bool registerAkv4Ae = true;
54+
55+
Console.WriteLine($"Cold Start\nUse Always Encrypted with Enclaves? {(useAlwaysEncrypted ? "Yes" : "No")}");
56+
57+
// Login to Azure and get Azure SQL DB OAuth2 token
58+
Console.WriteLine("Connecting to Azure");
59+
(TokenCredential? credential, string? oauth2TokenSql) = LoginToAure();
60+
if (credential is null || oauth2TokenSql is null)
61+
throw new ArgumentNullException("Unable to login to Azure");
62+
63+
Console.WriteLine("Connecting to Azure SQL DB");
64+
65+
// Connect to Azure SQL DB using EntraID AuthN rather than Windows or SQL AuthN
66+
var connectionString = GetSQLConnectionString(useAlwaysEncrypted);
67+
using SqlConnection conn = new(connectionString)
68+
{
69+
AccessToken = oauth2TokenSql
70+
};
71+
conn.Open();
72+
73+
// Register the enclave attestation URL, do this once on app startup
74+
if (useAlwaysEncrypted && registerAkv4Ae)
75+
RegisterAkvForAe(credential);
76+
77+
// From here on is real database work
78+
SqlCommand sqlCommand;
79+
80+
if (useAlwaysEncrypted == false)
81+
{
82+
string query =
83+
"SELECT Top 10 SSN, Salary, LastName, FirstName " +
84+
"FROM Employees";
85+
86+
sqlCommand = new(query, conn);
87+
DoQuery(sqlCommand);
88+
}
89+
else
90+
{
91+
///////////////////////////////////////////////////
92+
// QUERY #1: Get count based on employee salary
93+
// Demo step 4 - keep as is, but after demo set to false
94+
if (testWithParams == false)
95+
{
96+
string query1 = "SELECT count(*) FROM Employees where [Salary] > 50000";
97+
sqlCommand = new(query1, conn);
98+
DoQuery(sqlCommand);
99+
}
100+
101+
///////////////////////////////////////////////////
102+
// QUERY #2: Find minimum salary with specific SSN
103+
string query2 =
104+
"SELECT [SSN], [Salary], [LastName], [FirstName] " +
105+
"FROM Employees WHERE [Salary] > @MinSalary AND [SSN] LIKE @SSN " +
106+
"ORDER by [Salary] DESC";
107+
108+
sqlCommand = new(query2, conn);
109+
110+
// MUST use parameters
111+
SqlParameter minSalaryParam = new("@MinSalary", SqlDbType.Money) {
112+
Value = 50_000
113+
};
114+
sqlCommand.Parameters.Add(minSalaryParam);
115+
116+
SqlParameter ssnParam = new("@SSN", SqlDbType.Char) {
117+
Value = "6%"
118+
};
119+
sqlCommand.Parameters.Add(ssnParam);
120+
121+
DoQuery(sqlCommand);
122+
123+
///////////////////////////////////////////////////
124+
// QUERY #2: sproc to find salary range
125+
string query3 = "EXEC usp_GetSalary @MinSalary = @MinSalaryRange, @MaxSalary = @MaxSalaryRange";
126+
127+
sqlCommand = new(query3, conn);
128+
129+
SqlParameter minSalaryRange = new("@MinSalaryRange", SqlDbType.Money) {
130+
Value = 40_000
131+
};
132+
sqlCommand.Parameters.Add(minSalaryRange);
133+
134+
SqlParameter maxSalaryRange = new("@MaxSalaryRange", SqlDbType.Money) {
135+
Value = 42_000
136+
};
137+
sqlCommand.Parameters.Add(maxSalaryRange);
138+
139+
DoQuery(sqlCommand);
140+
}
141+
}
142+
143+
// Perform the actual query and gather stats
144+
// The time is the round trip time to and from the database
145+
// This will be higher than the actual query time due to network latency
146+
// IMPORTANT: the first query is slower due to lots of moving parts
147+
// getting loaded, authN, AuthZ, etc.
148+
static void DoQuery(SqlCommand sqlCommand)
149+
{
150+
var stopwatch = Stopwatch.StartNew();
151+
152+
Console.WriteLine($"\nPerforming Query\n{sqlCommand.CommandText}");
153+
154+
SqlDataReader? data = null;
155+
try
156+
{
157+
data = sqlCommand.ExecuteReader();
158+
}
159+
catch (SqlException ex)
160+
{
161+
Console.WriteLine(ex.Message);
162+
Environment.Exit(-1);
163+
}
164+
catch (System.InvalidOperationException ex)
165+
{
166+
Console.WriteLine(ex.Message);
167+
Environment.Exit(-1);
168+
}
169+
170+
stopwatch.Stop();
171+
Console.WriteLine($"Network Roundtrip + Query took [{stopwatch.ElapsedMilliseconds}ms]");
172+
173+
DumpData(data);
174+
data.Close();
175+
}
176+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//*********************************************************
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// This code is licensed under the MIT License (MIT).
4+
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
5+
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
6+
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
7+
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
8+
//
9+
// Author: Michael Howard, Azure Data Security
10+
//*********************************************************
11+
12+
using System.Text;
13+
using Azure.Core;
14+
using Azure.Identity;
15+
using Microsoft.Data.SqlClient;
16+
using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider;
17+
18+
partial class Program
19+
{
20+
// Helper function to dump binary data
21+
// Can truncate the output if needed
22+
public static string ByteArrayToHexString(byte[] byteArray, int maxLen = 16)
23+
{
24+
StringBuilder hex = new(byteArray.Length * 2);
25+
foreach (byte b in byteArray)
26+
hex.AppendFormat("{0:x2}", b);
27+
28+
return hex.ToString()[..maxLen];
29+
}
30+
31+
// Build SQL Connection String
32+
public static string GetSQLConnectionString(bool useAE = true)
33+
{
34+
const string _EnvVar = "ConnectContosoHR";
35+
36+
string? sqlConn =
37+
Environment.GetEnvironmentVariable(_EnvVar, EnvironmentVariableTarget.Process)
38+
?? throw new ArgumentException($"Missing environment variable, {_EnvVar}");
39+
40+
// Add AE settings if needed
41+
// You could also use a connection string builder, SqlConnectionStringBuilder
42+
if (useAE)
43+
sqlConn += ";Column Encryption Setting=Enabled;Attestation Protocol=None;";
44+
45+
return sqlConn;
46+
}
47+
48+
// Login to Azure and get token to Azure SQL DB OAuth2 token
49+
// This uses Azure CLI for authentication, but you could change this
50+
// to use other methods such as Managed Identity, Service Principal, etc.
51+
// You'll get an error if you don't have the Azure CLI installed and have yet to login.
52+
// Learn more about the various Azure token credential sources at
53+
// https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet
54+
public static (TokenCredential? tok, string? oauth2Sql) LoginToAure()
55+
{
56+
try
57+
{
58+
var credential = new AzureCliCredential();
59+
var oauth2TokenSql = credential.GetToken(
60+
new TokenRequestContext(
61+
["https://database.windows.net/.default"])).Token;
62+
return (credential, oauth2TokenSql);
63+
}
64+
catch (Exception ex)
65+
{
66+
Console.WriteLine(ex.Message);
67+
return (null, null);
68+
}
69+
}
70+
71+
// We need to register the use of AKV for AE, do these once per app on startup
72+
public static void RegisterAkvForAe(TokenCredential cred)
73+
{
74+
var akvAeProvider = new SqlColumnEncryptionAzureKeyVaultProvider(cred);
75+
SqlConnection.RegisterColumnEncryptionKeyStoreProviders(
76+
customProviders: new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>() {
77+
{ SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvAeProvider }
78+
});
79+
}
80+
}

0 commit comments

Comments
 (0)