From 4722585e1787f86077951a76026cbff3da295217 Mon Sep 17 00:00:00 2001 From: Andrey Shabarshov Date: Fri, 25 Feb 2022 17:57:01 +0000 Subject: [PATCH] Encrypted Client card data block added. Android public and private keeys separated into different files. --- QRBee.Core/Data/ClientCardData.cs | 45 ++---- QRBee.Core/Data/RegistrationResponse.cs | 8 +- QRBee.Core/Security/IPrivateKeyHandler.cs | 11 +- QRBee.Core/Security/ISecurityService.cs | 5 + QRBee.Core/Security/SecurityServiceBase.cs | 14 +- .../Services/AndroidPrivateKeyHandler.cs | 131 ++++++++---------- .../Services/AndroidSecurityService.cs | 26 ++++ QRBee/QRBee/Services/ILocalSettings.cs | 4 +- QRBee/QRBee/ViewModels/ClientPageViewModel.cs | 39 ++++-- .../QRBee/ViewModels/MerchantPageViewModel.cs | 3 +- QRBee/QRBee/ViewModels/RegisterViewModel.cs | 11 +- QRBeeApi/Services/QRBeeAPIService.cs | 3 +- QRBeeApi/Services/SecurityService.cs | 12 +- QRBeeApi/Services/ServerPrivateKeyHandler.cs | 18 ++- 14 files changed, 185 insertions(+), 145 deletions(-) diff --git a/QRBee.Core/Data/ClientCardData.cs b/QRBee.Core/Data/ClientCardData.cs index 4ac2dd7..a407e50 100644 --- a/QRBee.Core/Data/ClientCardData.cs +++ b/QRBee.Core/Data/ClientCardData.cs @@ -2,49 +2,20 @@ { public class ClientCardData { - - public string CardNumber - { - get; - set; - } - - public string ExpirationDateMMYY - { - get; - set; - } - - public string ValidFrom - { - get; - set; - } - - public string CardHolderName - { - get; - set; - } - - public string CVC - { - get; - set; - } - - public int? IssueNo - { - get; - set; - } + public string TransactionId { get; set; } + public string CardNumber { get; set; } + public string ExpirationDateMMYY { get; set; } + public string ValidFrom { get; set; } + public string CardHolderName { get; set; } + public string CVC { get; set; } + public int? IssueNo { get; set; } /// /// Convert ClientCardData to string to be used as a source for encryption. /// WARNING: this should always be encrypted and never transmitted in clear text form. /// /// Converted string - public string AsString() => $"{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}"; + public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}"; } } diff --git a/QRBee.Core/Data/RegistrationResponse.cs b/QRBee.Core/Data/RegistrationResponse.cs index 945277d..602d479 100644 --- a/QRBee.Core/Data/RegistrationResponse.cs +++ b/QRBee.Core/Data/RegistrationResponse.cs @@ -12,7 +12,13 @@ namespace QRBee.Core.Data set; } - public string Certificate + public string ClientCertificate + { + get; + set; + } + + public string APIServerCertificate { get; set; diff --git a/QRBee.Core/Security/IPrivateKeyHandler.cs b/QRBee.Core/Security/IPrivateKeyHandler.cs index 4396bf3..16a447b 100644 --- a/QRBee.Core/Security/IPrivateKeyHandler.cs +++ b/QRBee.Core/Security/IPrivateKeyHandler.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; namespace QRBee.Core.Security { @@ -38,6 +39,12 @@ namespace QRBee.Core.Security /// self-signed until CA issues a proper certificate /// /// - X509Certificate2 LoadPrivateKey(); + RSA LoadPrivateKey(); + + /// + /// Get public key certificate + /// + /// Public key certificate + X509Certificate2 GetCertificate(); } } diff --git a/QRBee.Core/Security/ISecurityService.cs b/QRBee.Core/Security/ISecurityService.cs index e151c1c..1da7dba 100644 --- a/QRBee.Core/Security/ISecurityService.cs +++ b/QRBee.Core/Security/ISecurityService.cs @@ -47,6 +47,11 @@ namespace QRBee.Core.Security // -------------------------- certificate services -------------------------- + /// + /// API Server certificate + /// + X509Certificate2 APIServerCertificate { get; set; } + /// /// Convert binary block to X509Certificate2. /// diff --git a/QRBee.Core/Security/SecurityServiceBase.cs b/QRBee.Core/Security/SecurityServiceBase.cs index c16d2ac..9f8f74b 100644 --- a/QRBee.Core/Security/SecurityServiceBase.cs +++ b/QRBee.Core/Security/SecurityServiceBase.cs @@ -39,12 +39,13 @@ namespace QRBee.Core.Security /// public byte[] Decrypt(byte[] data) { - using var myCert = LoadPrivateKey(); - using var rsa = myCert.GetRSAPrivateKey(); + using var rsa = LoadPrivateKey(); var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found"); return res; } + public abstract X509Certificate2 APIServerCertificate { get; set; } + /// public byte [] Encrypt(byte[] data, X509Certificate2 destCert) { @@ -63,8 +64,7 @@ namespace QRBee.Core.Security /// public byte[] Sign(byte[] data) { - using var myCert = LoadPrivateKey(); - using var rsa = myCert.GetRSAPrivateKey(); + using var rsa = LoadPrivateKey(); var res = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found"); return res; } @@ -86,16 +86,12 @@ namespace QRBee.Core.Security return serNo; } - private X509Certificate2 LoadPrivateKey() + private RSA LoadPrivateKey() { if (!PrivateKeyHandler.Exists()) PrivateKeyHandler.GeneratePrivateKey(); //TODO: subject name var pk = PrivateKeyHandler.LoadPrivateKey(); - if (!IsValid(pk) ) - { - throw new CryptographicException("CA private key is not valid"); - } return pk; } diff --git a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs index f54f32e..74ff2bb 100644 --- a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs +++ b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs @@ -81,7 +81,7 @@ namespace QRBee.Droid.Services /// public ReadableCertificateRequest CreateCertificateRequest(string subjectName) { - using var rsa = LoadRsaPrivateKey(); + using var rsa = LoadPrivateKey(); var request = new ReadableCertificateRequest { @@ -96,85 +96,68 @@ namespace QRBee.Droid.Services return request; } - /// - /// Generate EXPORTABLE certificate - /// - /// - /// - private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName) + ///// + ///// Generate EXPORTABLE certificate + ///// + ///// + ///// + //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; + //} + + ///// + ///// Generate CA certificate request (i.e. with KeyCertSign usage extension) + ///// + ///// + ///// + ///// + //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; + //} + + + public X509Certificate2 GetCertificate() { - // 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; - } - - /// - /// Generate CA certificate request (i.e. with KeyCertSign usage extension) - /// - /// - /// - /// - 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; - } - - /// - 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, VeryBadNeverUsePrivateKeyPassword); - return _certificate; - } + + var cert = new X509Certificate2(PrivateKeyFileName); + return cert; } /// public void AttachCertificate(X509Certificate2 cert) { - // heavily modified version of: - // https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net - using var rsa = LoadRsaPrivateKey(); + var bytes = cert.Export(X509ContentType.Cert); - var newPk = cert.CopyWithPrivateKey(rsa); - - var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword); - File.WriteAllBytes(PrivateKeyFileName, pkcs12data); + File.WriteAllBytes(PrivateKeyFileName, bytes); lock ( _syncObject ) { @@ -184,7 +167,7 @@ namespace QRBee.Droid.Services } } - private RSA LoadRsaPrivateKey() + public RSA LoadPrivateKey() { var bytes = File.ReadAllBytes(PrivateRsaKeyFileName); var s = CryptoHelper.DecryptStringAES(bytes, VeryBadNeverUsePrivateKeyPassword); diff --git a/QRBee/QRBee.Android/Services/AndroidSecurityService.cs b/QRBee/QRBee.Android/Services/AndroidSecurityService.cs index 83f3ede..af1b040 100644 --- a/QRBee/QRBee.Android/Services/AndroidSecurityService.cs +++ b/QRBee/QRBee.Android/Services/AndroidSecurityService.cs @@ -1,12 +1,15 @@ using System; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.IO; using QRBee.Core.Security; namespace QRBee.Droid.Services { internal class AndroidSecurityService : SecurityServiceBase { + private X509Certificate2 _apiServerCertificate; + private string ApiServerCertificateFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/ApiServerCertificate.bin"; public AndroidSecurityService(IPrivateKeyHandler privateKeyHandler) : base(privateKeyHandler) @@ -51,6 +54,29 @@ namespace QRBee.Droid.Services return builder.ToString(); } + + public override X509Certificate2 APIServerCertificate + { + get + { + if (_apiServerCertificate != null) + { + return _apiServerCertificate; + } + + if (!File.Exists(ApiServerCertificateFileName)) + throw new ApplicationException($"File not found: {ApiServerCertificateFileName}"); + var bytes = File.ReadAllBytes(ApiServerCertificateFileName); + _apiServerCertificate = new X509Certificate2(bytes); + return _apiServerCertificate; + } + set + { + _apiServerCertificate = value; + var bytes = _apiServerCertificate.Export(X509ContentType.Cert); + File.WriteAllBytes(ApiServerCertificateFileName,bytes); + } + } } } diff --git a/QRBee/QRBee/Services/ILocalSettings.cs b/QRBee/QRBee/Services/ILocalSettings.cs index b1ebbcf..c40ebec 100644 --- a/QRBee/QRBee/Services/ILocalSettings.cs +++ b/QRBee/QRBee/Services/ILocalSettings.cs @@ -7,7 +7,7 @@ namespace QRBee.Services { public class Settings { - //TODO add ClientId + public string ClientId { get; set; } public string PIN { get; set; } public bool IsRegistered => !string.IsNullOrWhiteSpace(ClientId); @@ -20,7 +20,7 @@ namespace QRBee.Services public string ExpirationDate { get; set; } public string CardHolderName { get; set; } public string CVC { get; set; } - public string IssueNo { get; set; } + public int? IssueNo { get; set; } public string Password { get; set; } } diff --git a/QRBee/QRBee/ViewModels/ClientPageViewModel.cs b/QRBee/QRBee/ViewModels/ClientPageViewModel.cs index b7e6829..ed6c7ba 100644 --- a/QRBee/QRBee/ViewModels/ClientPageViewModel.cs +++ b/QRBee/QRBee/ViewModels/ClientPageViewModel.cs @@ -12,6 +12,7 @@ namespace QRBee.ViewModels { private readonly IQRScanner _scanner; private readonly ISecurityService _securityService; + private readonly ILocalSettings _localSettings; public bool _isAcceptDenyButtonVisible; public bool _isQrVisible; public bool _isScanButtonVisible; @@ -21,10 +22,11 @@ namespace QRBee.ViewModels private string _qrCode; private MerchantToClientRequest _merchantToClientRequest; - public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService) + public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService, ILocalSettings localSettings) { _scanner = scanner; _securityService = securityService; + _localSettings = localSettings; ScanCommand = new Command(OnScanButtonClicked); AcceptQrCommand = new Command(OnAcceptQrCommand); DenyQrCommand = new Command(OnDenyQrCommand); @@ -149,16 +151,14 @@ namespace QRBee.ViewModels var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No"); if (!answer) return; - + var settings = _localSettings.LoadSettings(); var response = new ClientToMerchantResponse { - //TODO get client id from database - ClientId = Guid.NewGuid().ToString("D"), + ClientId = settings.ClientId, TimeStampUTC = DateTime.UtcNow, - MerchantRequest = _merchantToClientRequest - + MerchantRequest = _merchantToClientRequest, + EncryptedClientCardData = EncryptCardData(settings, _merchantToClientRequest.MerchantTransactionId) }; - // TODO Create client signature. var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature())); response.ClientSignature = Convert.ToBase64String(clientSignature); @@ -168,13 +168,30 @@ namespace QRBee.ViewModels IsScanButtonVisible = true; } + private string EncryptCardData(Settings settings, string transactionId) + { + var clientCardData = new ClientCardData + { + TransactionId = transactionId, + CardNumber = settings.CardNumber, + ExpirationDateMMYY = settings.ExpirationDate, + ValidFrom = settings.ValidFrom, + CardHolderName = settings.CardHolderName, + CVC = settings.CVC, + IssueNo = settings.IssueNo + }; + + var bytes = _securityService.Encrypt(Encoding.UTF8.GetBytes(clientCardData.AsString()),_securityService.APIServerCertificate); + return Convert.ToBase64String(bytes); + } + public void OnDenyQrCommand(object obj) { - QrCode = null; - IsQrVisible = false; + QrCode = null; + IsQrVisible = false; IsAcceptDenyButtonVisible = false; - IsScanButtonVisible = true; - Amount = ""; + IsScanButtonVisible = true; + Amount = ""; } } diff --git a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs index 6ec73fa..fcaba51 100644 --- a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs +++ b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs @@ -131,8 +131,7 @@ namespace QRBee.ViewModels { var trans = new MerchantToClientRequest { - //TODO get merchant id from database - MerchantId = Guid.NewGuid().ToString("D"), + MerchantId = _settings.LoadSettings().ClientId, MerchantTransactionId = Guid.NewGuid().ToString("D"), Name = Name, Amount = Amount, diff --git a/QRBee/QRBee/ViewModels/RegisterViewModel.cs b/QRBee/QRBee/ViewModels/RegisterViewModel.cs index 34ad1eb..e73afd5 100644 --- a/QRBee/QRBee/ViewModels/RegisterViewModel.cs +++ b/QRBee/QRBee/ViewModels/RegisterViewModel.cs @@ -50,7 +50,7 @@ namespace QRBee.ViewModels public string ExpirationDate { get; set; } public string CardHolderName { get; set; } public string CVC { get; set; } - public string IssueNo { get; set; } + public int? IssueNo { get; set; } public Color Password1Color { get; set; } public Color Password2Color { get; set;} @@ -125,7 +125,7 @@ namespace QRBee.ViewModels await _settings.SaveSettings(settings); - if (!_privateKeyHandler.Exists()) + //if (!_privateKeyHandler.Exists()) { _privateKeyHandler.GeneratePrivateKey(settings.Name); } @@ -149,6 +149,7 @@ namespace QRBee.ViewModels try { + //TODO Register if not, otherwise update // FOR TESTING PURPOSES //!settings.IsRegistered if (true) @@ -158,10 +159,14 @@ namespace QRBee.ViewModels // Save ClientId to LocalSettings settings = _settings.LoadSettings(); settings.ClientId = response.ClientId; + + // Save server public key certificate + _securityService.APIServerCertificate = _securityService.Deserialize(response.APIServerCertificate); + await _settings.SaveSettings(settings); // Attach certificate to privateKey (replace self-sighed with server issued certificate) - _privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.Certificate)); + _privateKeyHandler.AttachCertificate(_securityService.Deserialize(response.ClientCertificate)); var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault(); await page.DisplayAlert("Success", "You have been registered successfully", "Ok"); diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs index 8355289..57814de 100644 --- a/QRBeeApi/Services/QRBeeAPIService.cs +++ b/QRBeeApi/Services/QRBeeAPIService.cs @@ -62,7 +62,8 @@ namespace QRBee.Api.Services return new RegistrationResponse { ClientId = clientId, - Certificate = _securityService.Serialize(clientCertificate) + ClientCertificate = _securityService.Serialize(clientCertificate), + APIServerCertificate = _securityService.Serialize(_securityService.APIServerCertificate) }; } diff --git a/QRBeeApi/Services/SecurityService.cs b/QRBeeApi/Services/SecurityService.cs index 72f2250..c915f64 100644 --- a/QRBeeApi/Services/SecurityService.cs +++ b/QRBeeApi/Services/SecurityService.cs @@ -29,7 +29,10 @@ namespace QRBee.Api.Services var req = CreateClientCertRequest(distinguishedName, rsa); var pk = PrivateKeyHandler.LoadPrivateKey(); - var clientCert = req.Create(pk, + var cert = PrivateKeyHandler.GetCertificate(); + var newCert = cert.CopyWithPrivateKey(pk); + + var clientCert = req.Create(newCert, DateTimeOffset.UtcNow - TimeSpan.FromDays(1), DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays), Guid.NewGuid() @@ -92,6 +95,13 @@ namespace QRBee.Api.Services return builder.ToString(); } + + /// + public override X509Certificate2 APIServerCertificate + { + get => PrivateKeyHandler.GetCertificate(); + set => throw new ApplicationException("Do not call this"); + } } } diff --git a/QRBeeApi/Services/ServerPrivateKeyHandler.cs b/QRBeeApi/Services/ServerPrivateKeyHandler.cs index ddf59b4..2857bd4 100644 --- a/QRBeeApi/Services/ServerPrivateKeyHandler.cs +++ b/QRBeeApi/Services/ServerPrivateKeyHandler.cs @@ -55,7 +55,7 @@ namespace QRBee.Api.Services 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 pk = Load(); var rsa = pk.GetRSAPrivateKey(); if (rsa == null) @@ -172,7 +172,7 @@ namespace QRBee.Api.Services /// - public X509Certificate2 LoadPrivateKey() + private X509Certificate2 Load() { if (_certificate != null) return _certificate; @@ -191,6 +191,20 @@ namespace QRBee.Api.Services } } + public RSA LoadPrivateKey() + { + var pk = Load(); + return pk.GetRSAPrivateKey()?? throw new ApplicationException("Private key not found"); + } + + public X509Certificate2 GetCertificate() + { + var pk = Load(); + var bytes = pk.Export(X509ContentType.Cert); + var cert = new X509Certificate2(bytes); + return cert; + } + /// public void AttachCertificate(X509Certificate2 cert) {