11 changed files with 356 additions and 167 deletions
@ -1,7 +1,9 @@ |
|||||||
secrets.json |
secrets.json |
||||||
database.json |
database.json |
||||||
|
databaseTo.json |
||||||
|
|
||||||
# Development keys |
# Development keys |
||||||
*.key |
*.key |
||||||
*.crt |
*.crt |
||||||
*.pfx |
*.pfx |
||||||
|
*.lock |
||||||
|
|||||||
@ -0,0 +1,121 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Text; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using Bit.KeyConnector.Repositories; |
||||||
|
using Bit.KeyConnector.Services; |
||||||
|
using Microsoft.Extensions.DependencyInjection; |
||||||
|
using Microsoft.Extensions.Hosting; |
||||||
|
using Microsoft.Extensions.Logging; |
||||||
|
|
||||||
|
namespace Bit.KeyConnector.HostedServices |
||||||
|
{ |
||||||
|
public class TransferToHostedService : IHostedService, IDisposable |
||||||
|
{ |
||||||
|
private readonly IUserKeyRepository _userKeyRepository; |
||||||
|
private readonly ICryptoService _cryptoService; |
||||||
|
private readonly KeyConnectorSettings _keyConnectorSettings; |
||||||
|
private readonly ILogger<TransferToHostedService> _logger; |
||||||
|
|
||||||
|
public TransferToHostedService( |
||||||
|
IUserKeyRepository userKeyRepository, |
||||||
|
ICryptoService cryptoService, |
||||||
|
KeyConnectorSettings keyConnectorSettings, |
||||||
|
ILogger<TransferToHostedService> logger) |
||||||
|
{ |
||||||
|
_userKeyRepository = userKeyRepository; |
||||||
|
_cryptoService = cryptoService; |
||||||
|
_keyConnectorSettings = keyConnectorSettings; |
||||||
|
_logger = logger; |
||||||
|
} |
||||||
|
|
||||||
|
public virtual async Task StartAsync(CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
if (!ShouldRun()) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
_logger.LogInformation("Starting service to transfer to new database."); |
||||||
|
WriteLockfile(); |
||||||
|
|
||||||
|
var transferToProvider = ConfigureTransferToServices(); |
||||||
|
var transferToCryptoService = transferToProvider.GetRequiredService<ICryptoService>(); |
||||||
|
var transferToUserKeyRepository = transferToProvider.GetRequiredService<IUserKeyRepository>(); |
||||||
|
|
||||||
|
var userKeys = await _userKeyRepository.ReadAllAsync(); |
||||||
|
_logger.LogInformation("Found {0} user keys to transfer.", userKeys.Count); |
||||||
|
foreach (var userKey in userKeys) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
var key = await _cryptoService.AesDecryptToB64Async(userKey.Key); |
||||||
|
userKey.Key = await transferToCryptoService.AesEncryptToB64Async(key); |
||||||
|
await transferToUserKeyRepository.CreateAsync(userKey); |
||||||
|
} |
||||||
|
catch (Exception e) |
||||||
|
{ |
||||||
|
_logger.LogWarning(e, "Failed to transfer user key {0}. Skip it.", userKey.Id); |
||||||
|
} |
||||||
|
} |
||||||
|
_logger.LogInformation("Finished transferring user keys."); |
||||||
|
} |
||||||
|
|
||||||
|
public virtual Task StopAsync(CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
return Task.FromResult(0); |
||||||
|
} |
||||||
|
|
||||||
|
public virtual void Dispose() |
||||||
|
{ } |
||||||
|
|
||||||
|
private bool ShouldRun() |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(_keyConnectorSettings.TransferTo.LockfilePath)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (File.Exists(_keyConnectorSettings.TransferTo.LockfilePath)) |
||||||
|
{ |
||||||
|
_logger.LogInformation("Not running transfer service due to lock file existing at {0}.", |
||||||
|
_keyConnectorSettings.TransferTo.LockfilePath); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (_keyConnectorSettings.TransferTo?.RsaKey == null || |
||||||
|
_keyConnectorSettings.TransferTo.Database == null) |
||||||
|
{ |
||||||
|
_logger.LogInformation("Not running transfer service due to missing settings."); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
private void WriteLockfile() |
||||||
|
{ |
||||||
|
using var fs = File.Create(_keyConnectorSettings.TransferTo.LockfilePath); |
||||||
|
var nowString = Encoding.UTF8.GetBytes(DateTime.UtcNow.ToString()); |
||||||
|
fs.Write(nowString, 0, nowString.Length); |
||||||
|
} |
||||||
|
|
||||||
|
private IServiceProvider ConfigureTransferToServices() |
||||||
|
{ |
||||||
|
var settings = new KeyConnectorSettings |
||||||
|
{ |
||||||
|
RsaKey = _keyConnectorSettings.TransferTo.RsaKey, |
||||||
|
Certificate = _keyConnectorSettings.TransferTo.Certificate, |
||||||
|
Database = _keyConnectorSettings.TransferTo.Database |
||||||
|
}; |
||||||
|
var transferToServices = new ServiceCollection(); |
||||||
|
transferToServices.AddSingleton(s => settings); |
||||||
|
transferToServices.AddRsaKeyProvider(settings); |
||||||
|
transferToServices.AddBaseServices(); |
||||||
|
var efDatabaseProvider = transferToServices.AddDatabase(settings); |
||||||
|
var transferToProvider = transferToServices.BuildServiceProvider(); |
||||||
|
return transferToProvider; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -1,9 +1,12 @@ |
|||||||
using System; |
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Threading.Tasks; |
||||||
using Bit.KeyConnector.Models; |
using Bit.KeyConnector.Models; |
||||||
|
|
||||||
namespace Bit.KeyConnector.Repositories |
namespace Bit.KeyConnector.Repositories |
||||||
{ |
{ |
||||||
public interface IUserKeyRepository : IRepository<UserKeyModel, Guid> |
public interface IUserKeyRepository : IRepository<UserKeyModel, Guid> |
||||||
{ |
{ |
||||||
|
Task<List<UserKeyModel>> ReadAllAsync(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,181 @@ |
|||||||
|
using System; |
||||||
|
using System.Security.Claims; |
||||||
|
using Bit.KeyConnector.Repositories; |
||||||
|
using Bit.KeyConnector.Services; |
||||||
|
using IdentityModel; |
||||||
|
using IdentityServer4.AccessTokenValidation; |
||||||
|
using JsonFlatFileDataStore; |
||||||
|
using Microsoft.AspNetCore.Builder; |
||||||
|
using Microsoft.AspNetCore.Hosting; |
||||||
|
using Microsoft.AspNetCore.Identity; |
||||||
|
using Microsoft.Extensions.DependencyInjection; |
||||||
|
using Microsoft.Extensions.Hosting; |
||||||
|
|
||||||
|
namespace Bit.KeyConnector |
||||||
|
{ |
||||||
|
public static class ServiceCollectionExtensions |
||||||
|
{ |
||||||
|
public static void AddRsaKeyProvider( |
||||||
|
this IServiceCollection services, |
||||||
|
KeyConnectorSettings settings) |
||||||
|
{ |
||||||
|
var rsaKeyProvider = settings.RsaKey.Provider?.ToLowerInvariant(); |
||||||
|
if (rsaKeyProvider == "certificate" || rsaKeyProvider == "pkcs11") |
||||||
|
{ |
||||||
|
if (rsaKeyProvider == "certificate") |
||||||
|
{ |
||||||
|
services.AddSingleton<IRsaKeyService, LocalCertificateRsaKeyService>(); |
||||||
|
} |
||||||
|
else if (rsaKeyProvider == "pkcs11") |
||||||
|
{ |
||||||
|
services.AddSingleton<IRsaKeyService, Pkcs11RsaKeyService>(); |
||||||
|
} |
||||||
|
|
||||||
|
var certificateProvider = settings.Certificate.Provider?.ToLowerInvariant(); |
||||||
|
if (certificateProvider == "store") |
||||||
|
{ |
||||||
|
services.AddSingleton<ICertificateProviderService, StoreCertificateProviderService>(); |
||||||
|
} |
||||||
|
else if (certificateProvider == "filesystem") |
||||||
|
{ |
||||||
|
services.AddSingleton<ICertificateProviderService, FilesystemCertificateProviderService>(); |
||||||
|
} |
||||||
|
else if (certificateProvider == "azurestorage") |
||||||
|
{ |
||||||
|
services.AddSingleton<ICertificateProviderService, AzureStorageCertificateProviderService>(); |
||||||
|
} |
||||||
|
else if (certificateProvider == "azurekv") |
||||||
|
{ |
||||||
|
services.AddSingleton<ICertificateProviderService, AzureKeyVaultCertificateProviderService>(); |
||||||
|
} |
||||||
|
else if (certificateProvider == "vault") |
||||||
|
{ |
||||||
|
services.AddSingleton<ICertificateProviderService, HashicorpVaultCertificateProviderService>(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
throw new Exception("Unknown certificate provider configured."); |
||||||
|
} |
||||||
|
} |
||||||
|
else if (rsaKeyProvider == "azurekv") |
||||||
|
{ |
||||||
|
services.AddSingleton<IRsaKeyService, AzureKeyVaultRsaKeyService>(); |
||||||
|
} |
||||||
|
else if (rsaKeyProvider == "gcpkms") |
||||||
|
{ |
||||||
|
services.AddSingleton<IRsaKeyService, GoogleCloudKmsRsaKeyService>(); |
||||||
|
} |
||||||
|
else if (rsaKeyProvider == "awskms") |
||||||
|
{ |
||||||
|
services.AddSingleton<IRsaKeyService, AwsKmsRsaKeyService>(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
throw new Exception("Unknown rsa key provider configured."); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static bool AddDatabase( |
||||||
|
this IServiceCollection services, |
||||||
|
KeyConnectorSettings settings) |
||||||
|
{ |
||||||
|
var databaseProvider = settings.Database.Provider?.ToLowerInvariant(); |
||||||
|
var efDatabaseProvider = databaseProvider == "sqlserver" || databaseProvider == "postgresql" || |
||||||
|
databaseProvider == "mysql" || databaseProvider == "sqlite"; |
||||||
|
|
||||||
|
if (databaseProvider == "json") |
||||||
|
{ |
||||||
|
// Assign foobar to keyProperty in order to not use incrementing Id functionality |
||||||
|
services.AddSingleton<IDataStore>( |
||||||
|
new DataStore(settings.Database.JsonFilePath, keyProperty: "--foobar--")); |
||||||
|
services.AddSingleton<IApplicationDataRepository, Repositories.JsonFile.ApplicationDataRepository>(); |
||||||
|
services.AddSingleton<IUserKeyRepository, Repositories.JsonFile.UserKeyRepository>(); |
||||||
|
} |
||||||
|
else if (databaseProvider == "mongo") |
||||||
|
{ |
||||||
|
services.AddSingleton<IApplicationDataRepository, Repositories.Mongo.ApplicationDataRepository>(); |
||||||
|
services.AddSingleton<IUserKeyRepository, Repositories.Mongo.UserKeyRepository>(); |
||||||
|
} |
||||||
|
else if (efDatabaseProvider) |
||||||
|
{ |
||||||
|
if (databaseProvider == "sqlserver") |
||||||
|
{ |
||||||
|
services.AddDbContext<Repositories.EntityFramework.DatabaseContext, |
||||||
|
Repositories.EntityFramework.SqlServerDatabaseContext>(); |
||||||
|
} |
||||||
|
else if (databaseProvider == "postgresql") |
||||||
|
{ |
||||||
|
services.AddDbContext<Repositories.EntityFramework.DatabaseContext, |
||||||
|
Repositories.EntityFramework.PostgreSqlDatabaseContext>(); |
||||||
|
} |
||||||
|
else if (databaseProvider == "mysql") |
||||||
|
{ |
||||||
|
services.AddDbContext<Repositories.EntityFramework.DatabaseContext, |
||||||
|
Repositories.EntityFramework.MySqlDatabaseContext>(); |
||||||
|
} |
||||||
|
else if (databaseProvider == "sqlite") |
||||||
|
{ |
||||||
|
services.AddDbContext<Repositories.EntityFramework.DatabaseContext, |
||||||
|
Repositories.EntityFramework.SqliteDatabaseContext>(); |
||||||
|
} |
||||||
|
services.AddSingleton<IApplicationDataRepository, |
||||||
|
Repositories.EntityFramework.ApplicationDataRepository>(); |
||||||
|
services.AddSingleton<IUserKeyRepository, Repositories.EntityFramework.UserKeyRepository>(); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
throw new Exception("No database configured."); |
||||||
|
} |
||||||
|
|
||||||
|
return efDatabaseProvider; |
||||||
|
} |
||||||
|
|
||||||
|
public static void AddAuthentication( |
||||||
|
this IServiceCollection services, |
||||||
|
IWebHostEnvironment environment, |
||||||
|
KeyConnectorSettings settings) |
||||||
|
{ |
||||||
|
services.Configure<IdentityOptions>(options => |
||||||
|
{ |
||||||
|
options.ClaimsIdentity = new ClaimsIdentityOptions |
||||||
|
{ |
||||||
|
UserNameClaimType = JwtClaimTypes.Email, |
||||||
|
UserIdClaimType = JwtClaimTypes.Subject |
||||||
|
}; |
||||||
|
}); |
||||||
|
services |
||||||
|
.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) |
||||||
|
.AddIdentityServerAuthentication(options => |
||||||
|
{ |
||||||
|
options.Authority = settings.IdentityServerUri; |
||||||
|
options.RequireHttpsMetadata = !environment.IsDevelopment() && |
||||||
|
settings.IdentityServerUri.StartsWith("https"); |
||||||
|
options.NameClaimType = ClaimTypes.Email; |
||||||
|
options.SupportedTokens = SupportedTokens.Jwt; |
||||||
|
}); |
||||||
|
|
||||||
|
services |
||||||
|
.AddAuthorization(config => |
||||||
|
{ |
||||||
|
config.AddPolicy("Application", policy => |
||||||
|
{ |
||||||
|
policy.RequireAuthenticatedUser(); |
||||||
|
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external"); |
||||||
|
policy.RequireClaim(JwtClaimTypes.Scope, "api"); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
if (environment.IsDevelopment()) |
||||||
|
{ |
||||||
|
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static void AddBaseServices( |
||||||
|
this IServiceCollection services) |
||||||
|
{ |
||||||
|
services.AddSingleton<ICryptoFunctionService, CryptoFunctionService>(); |
||||||
|
services.AddSingleton<ICryptoService, CryptoService>(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue