From e07b45d43ffc4dd022c885537b249071aa6d2509 Mon Sep 17 00:00:00 2001 From: Andrey Shabarshov Date: Sat, 19 Feb 2022 15:13:56 +0000 Subject: [PATCH] Certificate information can now be stored in database. Second attempt at key generation under Android still no luck. --- QRBee.Core/QRBee.Core.csproj | 8 ++++ QRBee.Core/Security/IPrivateKeyHandler.cs | 4 +- QRBee/QRBee.Android/QRBee.Android.csproj | 1 + .../Services/AndroidPrivateKeyHandler.cs | 46 ++++++++++-------- QRBee/QRBee.iOS/QRBee.iOS.csproj | 7 +-- QRBee/QRBee/QRBee.csproj | 8 ++-- QRBee/QRBee/ViewModels/RegisterViewModel.cs | 2 +- QRBeeApi/QRBee.Api.csproj | 8 ++++ QRBeeApi/Services/Database/CertificateInfo.cs | 14 ++++++ QRBeeApi/Services/Database/IStorage.cs | 14 ++++++ QRBeeApi/Services/Database/Storage.cs | 48 ++++++++++++++++++- QRBeeApi/Services/IQRBeeAPI.cs | 3 +- QRBeeApi/Services/QRBeeAPIService.cs | 17 ++++++- QRBeeApi/Services/ServerPrivateKeyHandler.cs | 30 +++++++----- 14 files changed, 165 insertions(+), 45 deletions(-) create mode 100644 QRBeeApi/Services/Database/CertificateInfo.cs diff --git a/QRBee.Core/QRBee.Core.csproj b/QRBee.Core/QRBee.Core.csproj index a0edeeb..d9267ae 100644 --- a/QRBee.Core/QRBee.Core.csproj +++ b/QRBee.Core/QRBee.Core.csproj @@ -6,6 +6,14 @@ latest + + True + + + + True + + diff --git a/QRBee.Core/Security/IPrivateKeyHandler.cs b/QRBee.Core/Security/IPrivateKeyHandler.cs index 2c8c0b7..4396bf3 100644 --- a/QRBee.Core/Security/IPrivateKeyHandler.cs +++ b/QRBee.Core/Security/IPrivateKeyHandler.cs @@ -18,13 +18,13 @@ namespace QRBee.Core.Security /// /// /// Certificate request to be sent to CA in PEM format - ReadableCertificateRequest GeneratePrivateKey(string? subjectName = null); + ReadableCertificateRequest GeneratePrivateKey(string subjectName = null); /// /// Re-create certificate request if CA response was not received in time. /// /// Certificate request to be sent to CA in PEM format - ReadableCertificateRequest CreateCertificateRequest(); + ReadableCertificateRequest CreateCertificateRequest(string subjectName); /// /// Attach CA-generated public key certificate to the private key diff --git a/QRBee/QRBee.Android/QRBee.Android.csproj b/QRBee/QRBee.Android/QRBee.Android.csproj index 136e5b2..4f615ef 100644 --- a/QRBee/QRBee.Android/QRBee.Android.csproj +++ b/QRBee/QRBee.Android/QRBee.Android.csproj @@ -38,6 +38,7 @@ false false + true true diff --git a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs index 9197da5..796cef3 100644 --- a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs +++ b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs @@ -13,49 +13,55 @@ namespace QRBee.Droid.Services /// public class AndroidPrivateKeyHandler : IPrivateKeyHandler { - private X509Certificate2? _certificate; + private X509Certificate2 _certificate; private readonly object _syncObject = new object(); - private const string FileName = "private_key.p12"; - protected string CommonName { get; set; } + private const string RawRsaKeyFileName = "rsa.key"; + private const string SignedCertificateFileName = "private_key.p12"; + private const string VeryBadNeverUsePrivateKeyPassword = "’³¶¾]Ô $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}"; + private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{SignedCertificateFileName}"; + private string PrivateRsaKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{RawRsaKeyFileName}"; /// public bool Exists() => File.Exists(PrivateKeyFileName); /// - public ReadableCertificateRequest GeneratePrivateKey(string? subjectName) + 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); + if ( File.Exists(PrivateRsaKeyFileName) ) + File.Delete(PrivateRsaKeyFileName); - _certificate?.Dispose(); - _certificate = new X509Certificate2(pkcs12data, CertificatePassword); + using var rsa = RSA.Create(RSABits); + var bytes = rsa.ExportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, EncryptionIterationCount)); + File.WriteAllBytes(PrivateRsaKeyFileName, bytes); } - return CreateCertificateRequest(); + return CreateCertificateRequest(subjectName); } /// - public ReadableCertificateRequest CreateCertificateRequest() + public ReadableCertificateRequest CreateCertificateRequest(string subjectName) { - var pk = LoadPrivateKey(); - var rsa = pk.GetRSAPublicKey(); + if (File.Exists(PrivateRsaKeyFileName)) + throw new ApplicationException("Private key does not exist"); + + var bytes = File.ReadAllBytes(PrivateRsaKeyFileName); + using var rsa = RSA.Create(RSABits); + rsa.ImportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, bytes, out _); var request = new ReadableCertificateRequest { RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()), - SubjectName = pk.SubjectName.Name + SubjectName = subjectName }; var data = Encoding.UTF8.GetBytes(request.AsDataForSignature()); @@ -128,7 +134,7 @@ namespace QRBee.Droid.Services if (!Exists()) throw new CryptographicException("PrivateKey does not exist"); - _certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword); + _certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword); return _certificate; } } @@ -140,14 +146,14 @@ namespace QRBee.Droid.Services // 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); + var pk = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword, 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); + var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword); File.WriteAllBytes(PrivateKeyFileName, pkcs12data); lock ( _syncObject ) diff --git a/QRBee/QRBee.iOS/QRBee.iOS.csproj b/QRBee/QRBee.iOS/QRBee.iOS.csproj index 687f47d..76d084d 100644 --- a/QRBee/QRBee.iOS/QRBee.iOS.csproj +++ b/QRBee/QRBee.iOS/QRBee.iOS.csproj @@ -51,6 +51,7 @@ Entitlements.plist None -all + true none @@ -133,10 +134,10 @@ - 2.1.4 + 2.1.5 - - + + 2.4.1 diff --git a/QRBee/QRBee/QRBee.csproj b/QRBee/QRBee/QRBee.csproj index b56f655..808d2a7 100644 --- a/QRBee/QRBee/QRBee.csproj +++ b/QRBee/QRBee/QRBee.csproj @@ -7,17 +7,19 @@ latest + True latest + True - - - + + + diff --git a/QRBee/QRBee/ViewModels/RegisterViewModel.cs b/QRBee/QRBee/ViewModels/RegisterViewModel.cs index 48f4284..d4050d4 100644 --- a/QRBee/QRBee/ViewModels/RegisterViewModel.cs +++ b/QRBee/QRBee/ViewModels/RegisterViewModel.cs @@ -133,7 +133,7 @@ namespace QRBee.ViewModels DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"), Email = Email, Name = Name, - CertificateRequest = _privateKeyHandler.CreateCertificateRequest(), + CertificateRequest = _privateKeyHandler.CreateCertificateRequest(Email), RegisterAsMerchant = false }; diff --git a/QRBeeApi/QRBee.Api.csproj b/QRBeeApi/QRBee.Api.csproj index 298d8da..3df1c32 100644 --- a/QRBeeApi/QRBee.Api.csproj +++ b/QRBeeApi/QRBee.Api.csproj @@ -7,6 +7,14 @@ 3b7dc7f1-0b82-4746-b99b-73c43c8826e0 + + True + + + + True + + diff --git a/QRBeeApi/Services/Database/CertificateInfo.cs b/QRBeeApi/Services/Database/CertificateInfo.cs new file mode 100644 index 0000000..8c21e68 --- /dev/null +++ b/QRBeeApi/Services/Database/CertificateInfo.cs @@ -0,0 +1,14 @@ +using System.Security.Cryptography.X509Certificates; +using MongoDB.Bson.Serialization.Attributes; + +namespace QRBee.Api.Services.Database +{ + public class CertificateInfo + { + + [BsonId] public string? Id { get; set; } + public string? ClientId { get; set; } + public string? Certificate { get; set; } + public DateTime ServerTimeStamp { get; set; } + } +} diff --git a/QRBeeApi/Services/Database/IStorage.cs b/QRBeeApi/Services/Database/IStorage.cs index a839d15..62ca07f 100644 --- a/QRBeeApi/Services/Database/IStorage.cs +++ b/QRBeeApi/Services/Database/IStorage.cs @@ -30,5 +30,19 @@ /// /// Information to be inserted Task PutTransactionInfo(TransactionInfo info); + + /// + /// Inserts CertificateInfo into database + /// + /// Information to be inserted + /// + Task InsertCertificate(CertificateInfo info); + + /// + /// Retrieve certificate information from database + /// + /// Identifier by which certificate information will be retrieved + /// Certificate information + Task GetCertificateInfo(string id); } } diff --git a/QRBeeApi/Services/Database/Storage.cs b/QRBeeApi/Services/Database/Storage.cs index 505814d..fa4a1df 100644 --- a/QRBeeApi/Services/Database/Storage.cs +++ b/QRBeeApi/Services/Database/Storage.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Options; using MongoDB.Driver; using QRBee.Api.Controllers; @@ -100,5 +101,50 @@ namespace QRBee.Api.Services.Database return cursor.Current.FirstOrDefault(); } + + public async Task InsertCertificate(CertificateInfo info) + { + var collection = _database.GetCollection("Certificates"); + + if (info.Id == null) + { + throw new ApplicationException("Info Id is null."); + } + + var certificate = await TryGetCertificateInfo(info.Id); + + if (certificate == null) + { + await collection.InsertOneAsync(info); + _logger.LogInformation($"Inserted new certificate with ID: {info.Id}"); + return; + } + + _logger.LogInformation($"Found certificate with ID: {info.Id}"); + + } + + /// + /// Try to find if the Certificate already exists in the database + /// + /// parameter by which to find CertificateInfo + /// null if certificate doesn't exist or CertificateInfo + private async Task TryGetCertificateInfo(string id) + { + var collection = _database.GetCollection("Transactions"); + using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}"); + if (!await cursor.MoveNextAsync()) + { + return null; + } + + return cursor.Current.FirstOrDefault(); + } + + public async Task GetCertificateInfo(string id) + { + var certificate = await TryGetCertificateInfo(id); + return certificate ?? throw new ApplicationException($"Certificate with Id: {id} not found."); + } } } diff --git a/QRBeeApi/Services/IQRBeeAPI.cs b/QRBeeApi/Services/IQRBeeAPI.cs index 0d9f0ff..ad4c504 100644 --- a/QRBeeApi/Services/IQRBeeAPI.cs +++ b/QRBeeApi/Services/IQRBeeAPI.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Mvc; using QRBee.Core; using QRBee.Core.Data; diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs index e6fc023..0c7f098 100644 --- a/QRBeeApi/Services/QRBeeAPIService.cs +++ b/QRBeeApi/Services/QRBeeAPIService.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using QRBee.Api.Services.Database; @@ -49,7 +50,9 @@ namespace QRBee.Api.Services var clientId = await _storage.PutUserInfo(info); var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey)); - //TODO save certificate to certificate mongoDB collection + + var convertedClientCertificate = Convert(clientCertificate, clientId); + await _storage.InsertCertificate(convertedClientCertificate); return new RegistrationResponse { @@ -124,5 +127,17 @@ namespace QRBee.Api.Services return new TransactionInfo(request, DateTime.UtcNow); } + private CertificateInfo Convert(X509Certificate2 certificate, string clientId) + { + var convertedCertificate = _securityService.Serialize(certificate); + return new CertificateInfo + { + Id = certificate.SerialNumber, + ClientId = clientId, + Certificate = convertedCertificate, + ServerTimeStamp = DateTime.UtcNow + }; + } + } } diff --git a/QRBeeApi/Services/ServerPrivateKeyHandler.cs b/QRBeeApi/Services/ServerPrivateKeyHandler.cs index c5c9372..1b50a1d 100644 --- a/QRBeeApi/Services/ServerPrivateKeyHandler.cs +++ b/QRBeeApi/Services/ServerPrivateKeyHandler.cs @@ -14,11 +14,10 @@ namespace QRBee.Api.Services 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 const string VeryBadNeverUseCertificatePassword = "+ñèbòFëc׎ßRúß¿ãçPN"; private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}"; @@ -27,33 +26,38 @@ namespace QRBee.Api.Services => File.Exists(PrivateKeyFileName); /// - public ReadableCertificateRequest GeneratePrivateKey(string? subjectName) + public ReadableCertificateRequest GeneratePrivateKey(string subjectName) { // locking used to make sure that only one thread generating a private key lock (_syncObject) { - var pk = CreateSelfSignedServerCertificate(subjectName ?? CommonName); - var pkcs12data = pk.Export(X509ContentType.Pfx, CertificatePassword); + var pk = CreateSelfSignedServerCertificate(subjectName); + var pkcs12data = pk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword); File.WriteAllBytes(PrivateKeyFileName, pkcs12data); _certificate?.Dispose(); - _certificate = new X509Certificate2(pkcs12data, CertificatePassword); + _certificate = new X509Certificate2(pkcs12data, VeryBadNeverUseCertificatePassword); } - return CreateCertificateRequest(); + return CreateCertificateRequest(subjectName); } /// - public ReadableCertificateRequest CreateCertificateRequest() + public ReadableCertificateRequest CreateCertificateRequest(string subjectName) { //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(); + if (rsa == null) + { + throw new ApplicationException("Object missing public key."); + } + var request = new ReadableCertificateRequest { RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()), - SubjectName = pk.SubjectName.Name + SubjectName = subjectName }; var data = Encoding.UTF8.GetBytes(request.AsDataForSignature()); @@ -147,7 +151,7 @@ namespace QRBee.Api.Services if (!Exists()) throw new CryptographicException("PrivateKey does not exist"); - _certificate = new X509Certificate2(PrivateKeyFileName, CertificatePassword); + _certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword); return _certificate; } } @@ -159,14 +163,14 @@ namespace QRBee.Api.Services // 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); + var pk = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword, 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); + var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUseCertificatePassword); File.WriteAllBytes(PrivateKeyFileName, pkcs12data); lock ( _syncObject )