From e1cd96005cda7cbba30f662b1cf55c54de566520 Mon Sep 17 00:00:00 2001 From: omerlh Date: Wed, 14 Nov 2018 20:33:44 +0200 Subject: [PATCH 1/2] extract all key managment operations to different class --- src/Controllers/EncryptController.cs | 58 +++---------- src/KeyManagment/AzureKeyVaultKeyManagment.cs | 86 +++++++++++++++++++ src/KeyManagment/IKeyManagment.cs | 19 ++++ src/Startup.cs | 20 +++-- src/appsettings.Development.json | 6 +- src/appsettings.json | 3 + 6 files changed, 137 insertions(+), 55 deletions(-) create mode 100644 src/KeyManagment/AzureKeyVaultKeyManagment.cs create mode 100644 src/KeyManagment/IKeyManagment.cs diff --git a/src/Controllers/EncryptController.cs b/src/Controllers/EncryptController.cs index 70cca5f36..dcacca35a 100644 --- a/src/Controllers/EncryptController.cs +++ b/src/Controllers/EncryptController.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Serilog; +using Hamuste.KeyManagment; namespace Hamuste.Controllers { @@ -24,11 +25,9 @@ namespace Hamuste.Controllers public class EncryptController : Controller { private readonly IKubernetes mKubernetes; - private readonly IKeyVaultClient mKeyVaultClient; private readonly IAuthorizationService mAuthorizationService; private readonly IHttpContextAccessor mHttpContextAccessor; - private readonly string mKeyVaultName; - private readonly string mKeyType; + private readonly IKeyManagment mKeyManagment; private readonly ILogger mAuditLogger = Log.ForContext().AsAudit(); private readonly ILogger mLogger = Log.ForContext(); @@ -37,18 +36,14 @@ public class EncryptController : Controller public EncryptController( IKubernetes kubernetes, - IKeyVaultClient keyVaultClient, IAuthorizationService authorizationService, IHttpContextAccessor httpContextAccessor, - IConfiguration configuration) + IKeyManagment keyManagment) { mKubernetes = kubernetes; - mKeyVaultClient = keyVaultClient; mAuthorizationService = authorizationService; mHttpContextAccessor = httpContextAccessor; - - mKeyVaultName = configuration["KeyVault:Name"]; - mKeyType = configuration["KeyVault:KeyType"]; + mKeyManagment = keyManagment; } [HttpPost] @@ -71,33 +66,15 @@ public async Task Encrypt([FromBody]EncryptRequest body) body.NamesapceName); return BadRequest(); } - - var id = $"{body.NamesapceName}:{body.SerivceAccountName}"; - var hash = ComputeKeyId(id); - - var keyId = $"https://{mKeyVaultName}.vault.azure.net/keys/{hash}"; - - try - { - await mKeyVaultClient.GetKeyAsync(keyId); - } - catch (KeyVaultErrorException e) when (e.Response.StatusCode == HttpStatusCode.NotFound) - { - mLogger.Information( - "KeyVault key was not found for Namespace {namesapce} and ServiceAccountName {serviceAccount}, creating new one.", - body.NamesapceName, body.SerivceAccountName); - - await mKeyVaultClient.CreateKeyAsync($"https://{mKeyVaultName}.vault.azure.net", hash, mKeyType, 2048); - } - - var encryptionResult = await mKeyVaultClient.EncryptAsync(keyId, "RSA-OAEP", Encoding.UTF8.GetBytes(body.Data)); + + var encryptedData = await mKeyManagment.Encrypt(body.Data, $"{body.NamesapceName}:{body.SerivceAccountName}"); mAuditLogger.Information("Encryption request succeeded, SourceIP: {sourceIp}, ServiceAccount: {serviceAccount}, Namesacpe: {namespace}", Request.HttpContext.Connection.RemoteIpAddress, body.SerivceAccountName, body.NamesapceName); - return Content(Convert.ToBase64String(encryptionResult.Result)); + return Content(encryptedData); } [HttpPost] @@ -123,20 +100,16 @@ public async Task Decrypt([FromBody] DecryptRequest body) Request.HttpContext.Connection.RemoteIpAddress, id); - var hash = ComputeKeyId(id); - - var keyId = $"https://{mKeyVaultName}.vault.azure.net/keys/{hash}"; - try + try { - var encryptionResult = - await mKeyVaultClient.DecryptAsync(keyId, "RSA-OAEP", Convert.FromBase64String(body.EncryptedData)); + var data = await mKeyManagment.Decrypt(body.EncryptedData, id); mAuditLogger.Information("Decryption request succeeded, SourceIP: {sourceIp}, ServiceAccount user Name: {sa}", Request.HttpContext.Connection.RemoteIpAddress, id); - return Content(Encoding.UTF8.GetString(encryptionResult.Result)); + return Content(data); } - catch (KeyVaultErrorException e) + catch (DecryptionFailureException e) { mLogger.Warning(e, "Decryption request failed, ServiceAccount: {sa}", Request.HttpContext.Connection.RemoteIpAddress, @@ -145,13 +118,6 @@ public async Task Decrypt([FromBody] DecryptRequest body) } } - private string ComputeKeyId(string serviceUserName) - { - return - WebEncoders.Base64UrlEncode( - SHA256.Create().ComputeHash( - Encoding.UTF8.GetBytes(serviceUserName))) - .Replace("_", "-"); - } + } } diff --git a/src/KeyManagment/AzureKeyVaultKeyManagment.cs b/src/KeyManagment/AzureKeyVaultKeyManagment.cs new file mode 100644 index 000000000..6b81ea240 --- /dev/null +++ b/src/KeyManagment/AzureKeyVaultKeyManagment.cs @@ -0,0 +1,86 @@ +using System; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.KeyVault.Models; +using Microsoft.Extensions.Configuration; +using Serilog; + +namespace Hamuste.KeyManagment +{ + public class AzureKeyVaultKeyManagment : IKeyManagment + { + private readonly IKeyVaultClient mKeyVaultClient; + private readonly string mKeyVaultName; + private readonly string mKeyType; + private readonly short mKeyLength; + private readonly ILogger mLogger = Log.ForContext(); + + public AzureKeyVaultKeyManagment(IKeyVaultClient keyVaultClient, + IConfiguration configuration) + { + mKeyVaultClient = keyVaultClient; + + mKeyVaultName = configuration["KeyVault:Name"]; + mKeyType = configuration["KeyVault:KeyType"]; + + if (!Int16.TryParse(configuration["KeyVault:KeyLength"], out mKeyLength)){ + throw new Exception($"Expected key lenght int, got {configuration["KeyVault:KeyLength"]}"); + } + } + + public async Task Decrypt(string encryptedData, string serviceAccountId) + { + var hash = ComputeKeyId(serviceAccountId); + + var keyId = $"https://{mKeyVaultName}.vault.azure.net/keys/{hash}"; + try + { + var encryptionResult = + await mKeyVaultClient.DecryptAsync(keyId, "RSA-OAEP", Convert.FromBase64String(encryptedData)); + + return Encoding.UTF8.GetString(encryptionResult.Result); + } + catch (KeyVaultErrorException e) + { + throw new DecryptionFailureException("KeyVault decription failed", e); + } + } + + public async Task Encrypt(string data, string serviceAccountId, bool createKeyIfMissing = true) + { + var hash = ComputeKeyId(serviceAccountId); + + var keyId = $"https://{mKeyVaultName}.vault.azure.net/keys/{hash}"; + + try + { + await mKeyVaultClient.GetKeyAsync(keyId); + } + catch (KeyVaultErrorException e) when (e.Response.StatusCode == HttpStatusCode.NotFound && createKeyIfMissing) + { + mLogger.Information( + "KeyVault key was not found for service account id {serviceAccount}, creating new one.", + serviceAccountId); + + await mKeyVaultClient.CreateKeyAsync($"https://{mKeyVaultName}.vault.azure.net", hash, mKeyType, mKeyLength); + } + + var encryptionResult = await mKeyVaultClient.EncryptAsync(keyId, "RSA-OAEP", Encoding.UTF8.GetBytes(data)); + + return Convert.ToBase64String(encryptionResult.Result); + } + + private string ComputeKeyId(string serviceUserName) + { + return + WebEncoders.Base64UrlEncode( + SHA256.Create().ComputeHash( + Encoding.UTF8.GetBytes(serviceUserName))) + .Replace("_", "-"); + } + } +} \ No newline at end of file diff --git a/src/KeyManagment/IKeyManagment.cs b/src/KeyManagment/IKeyManagment.cs new file mode 100644 index 000000000..f87faf10b --- /dev/null +++ b/src/KeyManagment/IKeyManagment.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace Hamuste.KeyManagment +{ + public interface IKeyManagment + { + Task Encrypt(string data, string serviceAccountId, bool createKeyIfMissing = true); + Task Decrypt(string encryptedData, string serviceAccountId); + } + + public class DecryptionFailureException : Exception + { + public DecryptionFailureException(string reason, Exception innerException) : base(reason, innerException) + { + + } + } +} \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index a763bfc46..e097aa400 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -11,8 +11,9 @@ using Microsoft.IdentityModel.Clients.ActiveDirectory; using Serilog; using System.Linq; -using Microsoft.AspNetCore.Http; - +using Microsoft.AspNetCore.Http; +using Hamuste.KeyManagment; + namespace Hamuste { public class Startup { @@ -55,16 +56,20 @@ public void ConfigureServices (IServiceCollection services) { config = string.IsNullOrEmpty(Configuration["Kubernetes:ProxyUrl"]) ? KubernetesClientConfiguration.InClusterConfig() : new KubernetesClientConfiguration {Host = Configuration["Kubernetes:ProxyUrl"]}; - - return new Kubernetes(config); }); - services.AddSingleton(s => - { - return new KeyVaultClient(GetToken); + services.AddSingleton(s => { + var provider = Configuration.GetValue("KeyManagment:Provider"); + if (provider == "AzureKeyVault"){ + return new AzureKeyVaultKeyManagment(s.GetService(), Configuration); + } else { + throw new InvalidOperationException($"Unsupported provider type: {provider}"); + } }); + services.AddSingleton(_ => new KeyVaultClient(GetToken)); + services.AddAuthentication().AddScheme("kubernetes", null); services.AddAuthorization(options => { @@ -80,7 +85,6 @@ public void ConfigureServices (IServiceCollection services) { public async Task GetToken(string authority, string resource, string scope) { - Console.WriteLine("Requesting a token!"); var clientId = Configuration["ActiveDirectory:ClientId"]; var clientSecret = Configuration["ActiveDirectory:ClientSecret"]; diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json index 7d7e227b1..394cea0c8 100644 --- a/src/appsettings.Development.json +++ b/src/appsettings.Development.json @@ -21,6 +21,10 @@ }, "KeyVault": { "Name": "k8spoc", - "KeyType": "RSA" + "KeyType": "RSA", + "KeyLength": "2048" + }, + "KeyManagment": { + "Provider": "AzureKeyVault" } } diff --git a/src/appsettings.json b/src/appsettings.json index 8b175f378..dd9b2906b 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -11,5 +11,8 @@ } ], "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] + }, + "KeyManagment": { + "Provider": "AzureKeyVault" } } \ No newline at end of file From 15f59b9da715a0330a57ee7703a771c379033b57 Mon Sep 17 00:00:00 2001 From: omerlh Date: Wed, 14 Nov 2018 21:41:51 +0200 Subject: [PATCH 2/2] Added AES KMS --- src/KeyManagment/AESKeyManagment.cs | 35 +++++++++++++++++++++++++++++ src/Startup.cs | 13 ++++++++++- src/appsettings.Development.json | 5 ++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/KeyManagment/AESKeyManagment.cs diff --git a/src/KeyManagment/AESKeyManagment.cs b/src/KeyManagment/AESKeyManagment.cs new file mode 100644 index 000000000..b6b1daa2e --- /dev/null +++ b/src/KeyManagment/AESKeyManagment.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; + +namespace Hamuste.KeyManagment +{ + // inspired by: https://stackoverflow.com/a/43936866/4792970 + public class AESKeyManagment : IKeyManagment + { + private string mKey; + private IDataProtector mDataProtector; + + public AESKeyManagment(string key, IDataProtectionProvider dataProtectionProvider) + { + mDataProtector = dataProtectionProvider.CreateProtector(key); + } + public Task Decrypt(string encryptedData, string serviceAccountId) + { + var protector = mDataProtector.CreateProtector(serviceAccountId); + return Task.FromResult( + Encoding.UTF8.GetString(protector.Unprotect(Convert.FromBase64String(encryptedData)))); + } + + public Task Encrypt(string data, string serviceAccountId, bool createKeyIfMissing = true) + { + var protector = mDataProtector.CreateProtector(serviceAccountId); + return Task.FromResult( + Convert.ToBase64String(protector.Protect(Encoding.UTF8.GetBytes(data)))); + } + } +} \ No newline at end of file diff --git a/src/Startup.cs b/src/Startup.cs index e097aa400..13733bbf2 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -13,6 +13,9 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Hamuste.KeyManagment; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; +using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; namespace Hamuste { @@ -59,11 +62,19 @@ public void ConfigureServices (IServiceCollection services) { return new Kubernetes(config); }); + services.AddDataProtection(); + services.AddSingleton(s => { var provider = Configuration.GetValue("KeyManagment:Provider"); if (provider == "AzureKeyVault"){ return new AzureKeyVaultKeyManagment(s.GetService(), Configuration); - } else { + } + else if (provider == "AESKey") + { + var key = Configuration.GetValue("KeyManagment:AES:Key"); + return new AESKeyManagment(key, s.GetService()); + } + else { throw new InvalidOperationException($"Unsupported provider type: {provider}"); } }); diff --git a/src/appsettings.Development.json b/src/appsettings.Development.json index 394cea0c8..b420e58f9 100644 --- a/src/appsettings.Development.json +++ b/src/appsettings.Development.json @@ -25,6 +25,9 @@ "KeyLength": "2048" }, "KeyManagment": { - "Provider": "AzureKeyVault" + "Provider": "AESKey", + "AES": { + "Key": "123456" + } } }