mirror of
https://github.com/NecroticBamboo/QRBee.git
synced 2025-12-21 12:11:53 +00:00
SecurityService implemented. Not working due to Android limitations (CertificateRequest).
This commit is contained in:
parent
551a44c38d
commit
6da55bc4a5
@ -1,4 +1,6 @@
|
|||||||
namespace QRBee.Core.Data
|
using QRBee.Core.Security;
|
||||||
|
|
||||||
|
namespace QRBee.Core.Data
|
||||||
{
|
{
|
||||||
public record RegistrationRequest
|
public record RegistrationRequest
|
||||||
{
|
{
|
||||||
@ -20,7 +22,7 @@
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CertificateRequest
|
public ReadableCertificateRequest CertificateRequest
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
|
|||||||
@ -18,13 +18,13 @@ namespace QRBee.Core.Security
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subjectName"></param>
|
/// <param name="subjectName"></param>
|
||||||
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
||||||
string GeneratePrivateKey(string? subjectName = null);
|
ReadableCertificateRequest GeneratePrivateKey(string? subjectName = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Re-create certificate request if CA response was not received in time.
|
/// Re-create certificate request if CA response was not received in time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
/// <returns>Certificate request to be sent to CA in PEM format</returns>
|
||||||
string CreateCertificateRequest();
|
ReadableCertificateRequest CreateCertificateRequest();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attach CA-generated public key certificate to the private key
|
/// Attach CA-generated public key certificate to the private key
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
|
|
||||||
namespace QRBee.Core.Security
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Private key handler for API server
|
|
||||||
/// </summary>
|
|
||||||
public class PrivateKeyHandlerBase : IPrivateKeyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
protected string CommonName { get; set; }
|
|
||||||
protected string CertificatePassword { get; set; }
|
|
||||||
public bool Exists()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GeneratePrivateKey(string? subjectName = null)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreateCertificateRequest()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AttachCertificate(X509Certificate2 cert)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate2 LoadPrivateKey()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
QRBee.Core/Security/ReadableCertificateRequest.cs
Normal file
29
QRBee.Core/Security/ReadableCertificateRequest.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace QRBee.Core.Security
|
||||||
|
{
|
||||||
|
public class ReadableCertificateRequest
|
||||||
|
{
|
||||||
|
private byte[] _rsaPublicKey;
|
||||||
|
private byte[] _signature;
|
||||||
|
|
||||||
|
public string SubjectName { get; set; }
|
||||||
|
|
||||||
|
public string RsaPublicKey
|
||||||
|
{
|
||||||
|
get => _rsaPublicKey != null ? Convert.ToBase64String(_rsaPublicKey): null;
|
||||||
|
set => _rsaPublicKey = value != null ? Convert.FromBase64String(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Signature
|
||||||
|
{
|
||||||
|
get => _signature != null ? Convert.ToBase64String(_signature): null;
|
||||||
|
set => _signature = value != null ? Convert.FromBase64String(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AsDataForSignature() => $"{SubjectName}|{RsaPublicKey}";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,6 +55,7 @@ namespace QRBee.Droid
|
|||||||
.AddSingleton<ISecurityService,AndroidSecurityService>()
|
.AddSingleton<ISecurityService,AndroidSecurityService>()
|
||||||
.AddSingleton<ILocalSettings, LocalSettings>()
|
.AddSingleton<ILocalSettings, LocalSettings>()
|
||||||
.AddSingleton<IQRScanner, QRScannerService>()
|
.AddSingleton<IQRScanner, QRScannerService>()
|
||||||
|
.AddSingleton<IPrivateKeyHandler, AndroidPrivateKeyHandler>()
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@
|
|||||||
<Compile Include="Resources\Resource.designer.cs" />
|
<Compile Include="Resources\Resource.designer.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Services\LocalSettings.cs" />
|
<Compile Include="Services\LocalSettings.cs" />
|
||||||
|
<Compile Include="Services\AndroidPrivateKeyHandler.cs" />
|
||||||
<Compile Include="Services\QRScannerService.cs" />
|
<Compile Include="Services\QRScannerService.cs" />
|
||||||
<Compile Include="Services\AndroidSecurityService.cs" />
|
<Compile Include="Services\AndroidSecurityService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
162
QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
Normal file
162
QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using Javax.Xml.Transform;
|
||||||
|
using QRBee.Core.Security;
|
||||||
|
|
||||||
|
namespace QRBee.Droid.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Private key handler for API server
|
||||||
|
/// </summary>
|
||||||
|
public class AndroidPrivateKeyHandler : IPrivateKeyHandler
|
||||||
|
{
|
||||||
|
private X509Certificate2? _certificate;
|
||||||
|
private readonly object _syncObject = new object();
|
||||||
|
|
||||||
|
private const string FileName = "private_key.p12";
|
||||||
|
protected string CommonName { get; set; }
|
||||||
|
private const int RSABits = 2048;
|
||||||
|
private const int CertificateValidityDays = 3650;
|
||||||
|
|
||||||
|
protected string CertificatePassword { get; set; }
|
||||||
|
|
||||||
|
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Exists()
|
||||||
|
=> File.Exists(PrivateKeyFileName);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
|
||||||
|
{
|
||||||
|
// locking used to make sure that only one thread generating a private key
|
||||||
|
lock (_syncObject)
|
||||||
|
{
|
||||||
|
var pk = CreateSelfSignedClientCertificate(subjectName ?? CommonName);
|
||||||
|
var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword);
|
||||||
|
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
|
||||||
|
|
||||||
|
_certificate?.Dispose();
|
||||||
|
_certificate = new X509Certificate2(pkcs12data, CertificatePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateCertificateRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ReadableCertificateRequest CreateCertificateRequest()
|
||||||
|
{
|
||||||
|
var pk = LoadPrivateKey();
|
||||||
|
var rsa = pk.GetRSAPublicKey();
|
||||||
|
|
||||||
|
var request = new ReadableCertificateRequest
|
||||||
|
{
|
||||||
|
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
|
||||||
|
SubjectName = pk.SubjectName.Name
|
||||||
|
};
|
||||||
|
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
|
||||||
|
|
||||||
|
//We can't use SecurityService here because it uses this class. This creates cyclic dependency.
|
||||||
|
var signature = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
|
||||||
|
request.Signature = Convert.ToBase64String(signature);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate EXPORTABLE certificate
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subjectName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName)
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
|
||||||
|
|
||||||
|
var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
|
||||||
|
|
||||||
|
using var rsa = RSA.Create(RSABits);
|
||||||
|
var request = CreateRequest(distinguishedName, rsa);
|
||||||
|
|
||||||
|
var certificate = request.CreateSelfSigned(
|
||||||
|
new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
|
||||||
|
new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="distinguishedName"></param>
|
||||||
|
/// <param name="rsa"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
||||||
|
{
|
||||||
|
//TODO not supported on Android
|
||||||
|
var request = new CertificateRequest(
|
||||||
|
distinguishedName,
|
||||||
|
rsa,
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1
|
||||||
|
);
|
||||||
|
|
||||||
|
request.CertificateExtensions.Add(
|
||||||
|
new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.DataEncipherment
|
||||||
|
| X509KeyUsageFlags.KeyEncipherment
|
||||||
|
| X509KeyUsageFlags.DigitalSignature,
|
||||||
|
false));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public X509Certificate2 LoadPrivateKey()
|
||||||
|
{
|
||||||
|
if (_certificate != null)
|
||||||
|
return _certificate;
|
||||||
|
|
||||||
|
// double locking
|
||||||
|
lock ( _syncObject )
|
||||||
|
{
|
||||||
|
if (_certificate != null)
|
||||||
|
return _certificate;
|
||||||
|
|
||||||
|
if (!Exists())
|
||||||
|
throw new CryptographicException("PrivateKey does not exist");
|
||||||
|
|
||||||
|
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
|
||||||
|
return _certificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void AttachCertificate(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
// heavily modified version of:
|
||||||
|
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
|
||||||
|
|
||||||
|
// we can't use LoadPrivateKey here as it creating non-exportable key
|
||||||
|
var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable);
|
||||||
|
using var rsa = pk.GetRSAPrivateKey();
|
||||||
|
if (rsa == null)
|
||||||
|
throw new CryptographicException("Can't get PrivateKey");
|
||||||
|
|
||||||
|
var newPk = cert.CopyWithPrivateKey(rsa);
|
||||||
|
|
||||||
|
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
|
||||||
|
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
|
||||||
|
|
||||||
|
lock ( _syncObject )
|
||||||
|
{
|
||||||
|
_certificate?.Dispose();
|
||||||
|
_certificate = null;
|
||||||
|
// it will be loaded on the next access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,9 +30,7 @@ namespace QRBee
|
|||||||
addPlatformServices?.Invoke(services);
|
addPlatformServices?.Invoke(services);
|
||||||
|
|
||||||
// TODO: Add core services here
|
// TODO: Add core services here
|
||||||
services
|
|
||||||
.AddSingleton<IPrivateKeyHandler, ClientPrivateKeyHandler>()
|
|
||||||
;
|
|
||||||
|
|
||||||
// Add ViewModels
|
// Add ViewModels
|
||||||
services
|
services
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
using QRBee.Core.Security;
|
|
||||||
|
|
||||||
namespace QRBee.Services
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Private key handler for Client side
|
|
||||||
/// </summary>
|
|
||||||
public class ClientPrivateKeyHandler : PrivateKeyHandlerBase
|
|
||||||
{
|
|
||||||
|
|
||||||
private const string VeryBadAndInsecureCertificatePassword = "Rî‹T=›'ÄζgÚrʯю™pudF";
|
|
||||||
|
|
||||||
public ClientPrivateKeyHandler(ILocalSettings settings)
|
|
||||||
{
|
|
||||||
CertificatePassword = VeryBadAndInsecureCertificatePassword;
|
|
||||||
|
|
||||||
var clientSettings = settings.LoadSettings();
|
|
||||||
CommonName = clientSettings?.ClientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -4,6 +4,7 @@ using System.Net.Http;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using QRBee.Core;
|
using QRBee.Core;
|
||||||
using QRBee.Core.Data;
|
using QRBee.Core.Data;
|
||||||
|
using QRBee.Core.Security;
|
||||||
using QRBee.Services;
|
using QRBee.Services;
|
||||||
using QRBee.Views;
|
using QRBee.Views;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
@ -13,15 +14,18 @@ namespace QRBee.ViewModels
|
|||||||
internal class RegisterViewModel: BaseViewModel
|
internal class RegisterViewModel: BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly ILocalSettings _settings;
|
private readonly ILocalSettings _settings;
|
||||||
|
private readonly IPrivateKeyHandler _privateKeyHandler;
|
||||||
|
private readonly ISecurityService _securityService;
|
||||||
private string _password1;
|
private string _password1;
|
||||||
private string _password2;
|
private string _password2;
|
||||||
public RegisterViewModel(ILocalSettings localSettings)
|
public RegisterViewModel(ILocalSettings localSettings, IPrivateKeyHandler privateKeyHandler, ISecurityService securityService)
|
||||||
{
|
{
|
||||||
_settings = localSettings;
|
_settings = localSettings;
|
||||||
|
_privateKeyHandler = privateKeyHandler;
|
||||||
|
_securityService = securityService;
|
||||||
RegisterCommand = new Command(OnRegisterClicked);
|
RegisterCommand = new Command(OnRegisterClicked);
|
||||||
|
|
||||||
var settings = localSettings.LoadSettings();
|
var settings = localSettings.LoadSettings();
|
||||||
|
|
||||||
Name = settings.Name;
|
Name = settings.Name;
|
||||||
Email = settings.Email;
|
Email = settings.Email;
|
||||||
DateOfBirth = settings.DateOfBirth;
|
DateOfBirth = settings.DateOfBirth;
|
||||||
@ -115,15 +119,21 @@ namespace QRBee.ViewModels
|
|||||||
settings.IssueNo = IssueNo;
|
settings.IssueNo = IssueNo;
|
||||||
settings.ValidFrom = ValidFrom;
|
settings.ValidFrom = ValidFrom;
|
||||||
settings.Name = Name;
|
settings.Name = Name;
|
||||||
settings.PIN = Pin;
|
settings.PIN = Pin;
|
||||||
|
|
||||||
await _settings.SaveSettings(settings);
|
await _settings.SaveSettings(settings);
|
||||||
|
|
||||||
|
if (!_privateKeyHandler.Exists())
|
||||||
|
{
|
||||||
|
_privateKeyHandler.GeneratePrivateKey(settings.Name);
|
||||||
|
}
|
||||||
|
|
||||||
var request = new RegistrationRequest
|
var request = new RegistrationRequest
|
||||||
{
|
{
|
||||||
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
|
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
|
||||||
Email = Email,
|
Email = Email,
|
||||||
Name = Name,
|
Name = Name,
|
||||||
|
CertificateRequest = _privateKeyHandler.CreateCertificateRequest(),
|
||||||
RegisterAsMerchant = false
|
RegisterAsMerchant = false
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,6 +146,9 @@ namespace QRBee.ViewModels
|
|||||||
settings.ClientId = response.ClientId;
|
settings.ClientId = response.ClientId;
|
||||||
await _settings.SaveSettings(settings);
|
await _settings.SaveSettings(settings);
|
||||||
|
|
||||||
|
// Attach certificate to privateKey (replace self-sighed with server issued certificate)
|
||||||
|
_privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.Certificate));
|
||||||
|
|
||||||
var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
|
var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
|
||||||
await page.DisplayAlert("Success", "You have been registered successfully", "Ok");
|
await page.DisplayAlert("Success", "You have been registered successfully", "Ok");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using MongoDB.Driver;
|
|||||||
using QRBee.Api;
|
using QRBee.Api;
|
||||||
using QRBee.Api.Services;
|
using QRBee.Api.Services;
|
||||||
using QRBee.Api.Services.Database;
|
using QRBee.Api.Services.Database;
|
||||||
|
using QRBee.Core.Security;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -24,10 +25,12 @@ builder.Services.AddEndpointsApiExplorer();
|
|||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddSingleton<IQRBeeAPI,QRBeeAPI>()
|
.AddSingleton<IQRBeeAPI,QRBeeAPIService>()
|
||||||
.AddSingleton<IStorage, Storage>()
|
.AddSingleton<IStorage, Storage>()
|
||||||
.Configure<DatabaseSettings>(builder.Configuration.GetSection("QRBeeDatabase"))
|
.Configure<DatabaseSettings>(builder.Configuration.GetSection("QRBeeDatabase"))
|
||||||
.AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings()))
|
.AddSingleton<IMongoClient>( cfg => new MongoClient(cfg.GetRequiredService<IOptions<DatabaseSettings>>().Value.ToMongoDbSettings()))
|
||||||
|
.AddSingleton<IPrivateKeyHandler, ServerPrivateKeyHandler>()
|
||||||
|
.AddSingleton<ISecurityService, SecurityService>()
|
||||||
;
|
;
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ using QRBee.Core.Data;
|
|||||||
namespace QRBee.Api.Services
|
namespace QRBee.Api.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// QRBeeAPI interface
|
/// QRBeeAPIService interface
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IQRBeeAPI
|
public interface IQRBeeAPI
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,23 +1,43 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using QRBee.Api.Services.Database;
|
using QRBee.Api.Services.Database;
|
||||||
using QRBee.Core;
|
using QRBee.Core;
|
||||||
using QRBee.Core.Data;
|
using QRBee.Core.Data;
|
||||||
|
using QRBee.Core.Security;
|
||||||
|
|
||||||
namespace QRBee.Api.Services
|
namespace QRBee.Api.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of <see href="IQRBeeAPI"/>
|
/// Implementation of <see href="IQRBeeAPI"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class QRBeeAPI: IQRBeeAPI
|
public class QRBeeAPIService: IQRBeeAPI
|
||||||
{
|
{
|
||||||
private readonly IStorage _storage;
|
private readonly IStorage _storage;
|
||||||
|
private readonly ISecurityService _securityService;
|
||||||
|
private readonly IPrivateKeyHandler _privateKeyHandler;
|
||||||
|
private static readonly object _lock = new ();
|
||||||
|
|
||||||
private const int MaxNameLength = 512;
|
private const int MaxNameLength = 512;
|
||||||
private const int MaxEmailLength = 512;
|
private const int MaxEmailLength = 512;
|
||||||
|
|
||||||
public QRBeeAPI(IStorage storage)
|
public QRBeeAPIService(IStorage storage, ISecurityService securityService, IPrivateKeyHandler privateKeyHandler)
|
||||||
{
|
{
|
||||||
_storage = storage;
|
_storage = storage;
|
||||||
|
_securityService = securityService;
|
||||||
|
_privateKeyHandler = privateKeyHandler;
|
||||||
|
Init(_privateKeyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Init(IPrivateKeyHandler privateKeyHandler)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!privateKeyHandler.Exists())
|
||||||
|
{
|
||||||
|
privateKeyHandler.GeneratePrivateKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RegistrationResponse> Register(RegistrationRequest request)
|
public async Task<RegistrationResponse> Register(RegistrationRequest request)
|
||||||
@ -28,8 +48,14 @@ namespace QRBee.Api.Services
|
|||||||
var info = Convert(request);
|
var info = Convert(request);
|
||||||
|
|
||||||
var clientId = await _storage.PutUserInfo(info);
|
var clientId = await _storage.PutUserInfo(info);
|
||||||
|
var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey));
|
||||||
|
//TODO save certificate to certificate mongoDB collection
|
||||||
|
|
||||||
return new RegistrationResponse{ClientId = clientId};
|
return new RegistrationResponse
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
Certificate = _securityService.Serialize(clientCertificate)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Update(string clientId, RegistrationRequest request)
|
public Task Update(string clientId, RegistrationRequest request)
|
||||||
@ -45,7 +71,7 @@ namespace QRBee.Api.Services
|
|||||||
return _storage.PutTransactionInfo(info);
|
return _storage.PutTransactionInfo(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Validate(RegistrationRequest request)
|
private void Validate(RegistrationRequest request)
|
||||||
{
|
{
|
||||||
if (request == null)
|
if (request == null)
|
||||||
{
|
{
|
||||||
@ -55,6 +81,7 @@ namespace QRBee.Api.Services
|
|||||||
var name = request.Name;
|
var name = request.Name;
|
||||||
var email = request.Email;
|
var email = request.Email;
|
||||||
var dateOfBirth = request.DateOfBirth;
|
var dateOfBirth = request.DateOfBirth;
|
||||||
|
var certificateRequest = request.CertificateRequest;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter)==false || name.Length>=MaxNameLength)
|
if (string.IsNullOrEmpty(name) || name.All(char.IsLetter)==false || name.Length>=MaxNameLength)
|
||||||
{
|
{
|
||||||
@ -76,6 +103,15 @@ namespace QRBee.Api.Services
|
|||||||
throw new ApplicationException($"DateOfBirth \"{dateOfBirth}\" isn't valid");
|
throw new ApplicationException($"DateOfBirth \"{dateOfBirth}\" isn't valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Check digital signature
|
||||||
|
var verified = _securityService.Verify(
|
||||||
|
Encoding.UTF8.GetBytes(certificateRequest.AsDataForSignature()),
|
||||||
|
Encoding.UTF8.GetBytes(certificateRequest.Signature),
|
||||||
|
_privateKeyHandler.LoadPrivateKey());
|
||||||
|
if (!verified)
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"Digital signature is not valid.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UserInfo Convert(RegistrationRequest request)
|
private static UserInfo Convert(RegistrationRequest request)
|
||||||
@ -10,6 +10,10 @@ namespace QRBee.Api.Services
|
|||||||
internal class SecurityService : SecurityServiceBase
|
internal class SecurityService : SecurityServiceBase
|
||||||
{
|
{
|
||||||
|
|
||||||
|
private const int CertificateValidityPeriodDays = 365;
|
||||||
|
private const string CertHeader = "-----BEGIN CERTIFICATE-----";
|
||||||
|
private const string CertFooter = "-----END CERTIFICATE-----";
|
||||||
|
|
||||||
public SecurityService(IPrivateKeyHandler privateKeyHandler)
|
public SecurityService(IPrivateKeyHandler privateKeyHandler)
|
||||||
: base(privateKeyHandler)
|
: base(privateKeyHandler)
|
||||||
{
|
{
|
||||||
@ -18,7 +22,20 @@ namespace QRBee.Api.Services
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
|
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
|
||||||
{
|
{
|
||||||
throw new ApplicationException("Client never issues certificates");
|
using var rsa = new RSACryptoServiceProvider();
|
||||||
|
rsa.ImportRSAPublicKey(rsaPublicKey, out _);
|
||||||
|
|
||||||
|
var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
|
||||||
|
var req = CreateClientCertRequest(distinguishedName, rsa);
|
||||||
|
|
||||||
|
var pk = PrivateKeyHandler.LoadPrivateKey();
|
||||||
|
var clientCert = req.Create(pk,
|
||||||
|
DateTimeOffset.UtcNow - TimeSpan.FromDays(1),
|
||||||
|
DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays),
|
||||||
|
Guid.NewGuid()
|
||||||
|
.ToByteArray());
|
||||||
|
|
||||||
|
return clientCert;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -27,7 +44,7 @@ namespace QRBee.Api.Services
|
|||||||
/// <param name="distinguishedName"></param>
|
/// <param name="distinguishedName"></param>
|
||||||
/// <param name="rsa"></param>
|
/// <param name="rsa"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static CertificateRequest CreateRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
private static CertificateRequest CreateClientCertRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
||||||
{
|
{
|
||||||
var request = new CertificateRequest(
|
var request = new CertificateRequest(
|
||||||
distinguishedName,
|
distinguishedName,
|
||||||
@ -41,19 +58,11 @@ namespace QRBee.Api.Services
|
|||||||
X509KeyUsageFlags.DataEncipherment
|
X509KeyUsageFlags.DataEncipherment
|
||||||
| X509KeyUsageFlags.KeyEncipherment
|
| X509KeyUsageFlags.KeyEncipherment
|
||||||
| X509KeyUsageFlags.DigitalSignature,
|
| X509KeyUsageFlags.DigitalSignature,
|
||||||
//| X509KeyUsageFlags.KeyCertSign,
|
|
||||||
false));
|
false));
|
||||||
|
|
||||||
|
|
||||||
// request.CertificateExtensions.Add(
|
|
||||||
// new X509EnhancedKeyUsageExtension(
|
|
||||||
// new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string CertHeader = "-----BEGIN CERTIFICATE-----";
|
|
||||||
private const string CertFooter = "-----END CERTIFICATE-----";
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override X509Certificate2 Deserialize(string pemData)
|
public override X509Certificate2 Deserialize(string pemData)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,20 +1,180 @@
|
|||||||
using QRBee.Core.Security;
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text;
|
||||||
|
using QRBee.Core.Security;
|
||||||
|
|
||||||
namespace QRBee.Api.Services
|
namespace QRBee.Api.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Private key handler for API server
|
/// Private key handler for API server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServerPrivateKeyHandler : PrivateKeyHandlerBase
|
public class ServerPrivateKeyHandler : IPrivateKeyHandler
|
||||||
{
|
{
|
||||||
private const string CACommonName = "QRBee-CA";
|
private X509Certificate2? _certificate;
|
||||||
|
private readonly object _syncObject = new object();
|
||||||
|
|
||||||
private const string VeryBadAndInsecureCertificatePassword = "U…Š)+œ¶€=ø‘ c¬Í↨ð´áY/ÿ☼æX";
|
private const string FileName = "private_key.p12";
|
||||||
|
protected string CommonName { get; set; }
|
||||||
|
private const int RSABits = 2048;
|
||||||
|
private const int CertificateValidityDays = 3650;
|
||||||
|
|
||||||
public ServerPrivateKeyHandler()
|
protected string CertificatePassword { get; set; }
|
||||||
|
|
||||||
|
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Exists()
|
||||||
|
=> File.Exists(PrivateKeyFileName);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ReadableCertificateRequest GeneratePrivateKey(string? subjectName)
|
||||||
{
|
{
|
||||||
CertificatePassword = VeryBadAndInsecureCertificatePassword;
|
// locking used to make sure that only one thread generating a private key
|
||||||
CommonName = CACommonName;
|
lock (_syncObject)
|
||||||
|
{
|
||||||
|
var pk = CreateSelfSignedServerCertificate(subjectName ?? CommonName);
|
||||||
|
var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword);
|
||||||
|
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
|
||||||
|
|
||||||
|
_certificate?.Dispose();
|
||||||
|
_certificate = new X509Certificate2(pkcs12data, CertificatePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateCertificateRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ReadableCertificateRequest CreateCertificateRequest()
|
||||||
|
{
|
||||||
|
//TODO in fact server should create certificate request in standard format if we ever want to get externally sighed certificate.
|
||||||
|
var pk = LoadPrivateKey();
|
||||||
|
var rsa = pk.GetRSAPublicKey();
|
||||||
|
|
||||||
|
var request = new ReadableCertificateRequest
|
||||||
|
{
|
||||||
|
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
|
||||||
|
SubjectName = pk.SubjectName.Name
|
||||||
|
};
|
||||||
|
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
|
||||||
|
|
||||||
|
//We can't use SecurityService here because it uses this class. This creates cyclic dependency.
|
||||||
|
var signature = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
|
||||||
|
request.Signature = Convert.ToBase64String(signature);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
//private static string AsCsr(CertificateRequest request)
|
||||||
|
//{
|
||||||
|
// // https://stackoverflow.com/questions/65943968/how-to-convert-a-csr-text-file-into-net-core-standard-certificaterequest-for-s
|
||||||
|
|
||||||
|
// var encoded = request.CreateSigningRequest();
|
||||||
|
// var payload = Convert.ToBase64String(encoded, Base64FormattingOptions.InsertLineBreaks);
|
||||||
|
// using var stream = new MemoryStream();
|
||||||
|
// using (var writer = new StreamWriter(stream, System.Text.Encoding.UTF8, 512, true))
|
||||||
|
// {
|
||||||
|
// writer.WriteLine("-----BEGIN CERTIFICATE REQUEST-----");
|
||||||
|
// writer.WriteLine(payload);
|
||||||
|
// writer.WriteLine("-----END CERTIFICATE REQUEST-----");
|
||||||
|
// writer.Flush();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// stream.Position = 0;
|
||||||
|
// using (var reader = new StreamReader(stream))
|
||||||
|
// {
|
||||||
|
// return reader.ReadToEnd();
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate EXPORTABLE certificate
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subjectName"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private X509Certificate2 CreateSelfSignedServerCertificate(string subjectName)
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/questions/42786986/how-to-create-a-valid-self-signed-x509certificate2-programmatically-not-loadin
|
||||||
|
|
||||||
|
var distinguishedName = new X500DistinguishedName($"CN={subjectName}");
|
||||||
|
|
||||||
|
using RSA rsa = RSA.Create(RSABits);
|
||||||
|
var request = CreateClientCertificateRequest(distinguishedName, rsa);
|
||||||
|
|
||||||
|
var certificate = request.CreateSelfSigned(
|
||||||
|
new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
|
||||||
|
new DateTimeOffset(DateTime.UtcNow.AddDays(CertificateValidityDays))
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="distinguishedName"></param>
|
||||||
|
/// <param name="rsa"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static CertificateRequest CreateClientCertificateRequest(X500DistinguishedName distinguishedName, RSA rsa)
|
||||||
|
{
|
||||||
|
var request = new CertificateRequest(
|
||||||
|
distinguishedName,
|
||||||
|
rsa,
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1
|
||||||
|
);
|
||||||
|
|
||||||
|
request.CertificateExtensions.Add(
|
||||||
|
new X509KeyUsageExtension(
|
||||||
|
X509KeyUsageFlags.DataEncipherment
|
||||||
|
| X509KeyUsageFlags.KeyEncipherment
|
||||||
|
| X509KeyUsageFlags.DigitalSignature,
|
||||||
|
false));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public X509Certificate2 LoadPrivateKey()
|
||||||
|
{
|
||||||
|
if (_certificate != null)
|
||||||
|
return _certificate;
|
||||||
|
|
||||||
|
// double locking
|
||||||
|
lock ( _syncObject )
|
||||||
|
{
|
||||||
|
if (_certificate != null)
|
||||||
|
return _certificate;
|
||||||
|
|
||||||
|
if (!Exists())
|
||||||
|
throw new CryptographicException("PrivateKey does not exist");
|
||||||
|
|
||||||
|
_certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword);
|
||||||
|
return _certificate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void AttachCertificate(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
// heavily modified version of:
|
||||||
|
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
|
||||||
|
|
||||||
|
// we can't use LoadPrivateKey here as it creating non-exportable key
|
||||||
|
var pk = new X509Certificate2(PrivateKeyFileName, CertificatePassword, X509KeyStorageFlags.Exportable);
|
||||||
|
using var rsa = pk.GetRSAPrivateKey();
|
||||||
|
if (rsa == null)
|
||||||
|
throw new CryptographicException("Can't get PrivateKey");
|
||||||
|
|
||||||
|
var newPk = cert.CopyWithPrivateKey(rsa);
|
||||||
|
|
||||||
|
var pkcs12data = newPk.Export(X509ContentType.Pfx, CertificatePassword);
|
||||||
|
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
|
||||||
|
|
||||||
|
lock ( _syncObject )
|
||||||
|
{
|
||||||
|
_certificate?.Dispose();
|
||||||
|
_certificate = null;
|
||||||
|
// it will be loaded on the next access
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user