From 82b8d1820e5c2cc60351d0d9485a92cd5018bac8 Mon Sep 17 00:00:00 2001 From: Andrey Shabarshov Date: Fri, 4 Mar 2022 17:17:10 +0000 Subject: [PATCH] Work on Transaction started. Pre-checks complete. --- QRBee.Core/Client/Client.cs | 16 ++- QRBee.Core/Data/ClientCardData.cs | 31 ++++- QRBee/QRBee.Android/MainActivity.cs | 10 +- .../QRBee/ViewModels/MerchantPageViewModel.cs | 3 +- QRBeeApi/Controllers/QRBeeController.cs | 6 +- QRBeeApi/Services/Database/IStorage.cs | 16 ++- QRBeeApi/Services/Database/Storage.cs | 23 +++- QRBeeApi/Services/Database/TransactionInfo.cs | 7 + QRBeeApi/Services/IQRBeeAPI.cs | 2 +- QRBeeApi/Services/QRBeeAPIService.cs | 120 ++++++++++++++++-- 10 files changed, 198 insertions(+), 36 deletions(-) diff --git a/QRBee.Core/Client/Client.cs b/QRBee.Core/Client/Client.cs index 8e70d31..75ca88b 100644 --- a/QRBee.Core/Client/Client.cs +++ b/QRBee.Core/Client/Client.cs @@ -207,18 +207,18 @@ namespace QRBee.Core.Client /// Success /// A server side error occurred. - public virtual System.Threading.Tasks.Task InsertTransactionAsync(PaymentRequest body) + public virtual System.Threading.Tasks.Task PayAsync(PaymentRequest body) { - return InsertTransactionAsync(body, System.Threading.CancellationToken.None); + return PayAsync(body, System.Threading.CancellationToken.None); } /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// Success /// A server side error occurred. - public virtual async System.Threading.Tasks.Task InsertTransactionAsync(PaymentRequest body, System.Threading.CancellationToken cancellationToken) + public virtual async System.Threading.Tasks.Task PayAsync(PaymentRequest body, System.Threading.CancellationToken cancellationToken) { var urlBuilder_ = new System.Text.StringBuilder(); - urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee/InsertTransaction"); + urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/QRBee/Pay"); var client_ = _httpClient; var disposeClient_ = false; @@ -230,6 +230,7 @@ namespace QRBee.Core.Client content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); PrepareRequest(client_, request_, urlBuilder_); @@ -254,7 +255,12 @@ namespace QRBee.Core.Client var status_ = (int)response_.StatusCode; if (status_ == 200) { - return; + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; } else { diff --git a/QRBee.Core/Data/ClientCardData.cs b/QRBee.Core/Data/ClientCardData.cs index a407e50..f2a0d41 100644 --- a/QRBee.Core/Data/ClientCardData.cs +++ b/QRBee.Core/Data/ClientCardData.cs @@ -15,7 +15,36 @@ /// WARNING: this should always be encrypted and never transmitted in clear text form. /// /// Converted string - public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}"; + public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo ?? 0}"; + + public static ClientCardData FromString(string input) + { + var s = input.Split('|'); + if (s.Length < 7) + { + throw new ApplicationException("Expected 7 or more elements"); + } + + var res = new ClientCardData() + { + TransactionId = s[0], + CardNumber = s[1], + ExpirationDateMMYY = s[2], + ValidFrom = s[3], + CardHolderName = s[4], + CVC = s[5] + }; + + if (!string.IsNullOrWhiteSpace(s[6])) + res.IssueNo = Convert.ToInt32(s[6]); + + if (res.IssueNo <= 0) + { + res.IssueNo = null; + } + + return res; + } } } diff --git a/QRBee/QRBee.Android/MainActivity.cs b/QRBee/QRBee.Android/MainActivity.cs index 8005002..fb3d8fd 100644 --- a/QRBee/QRBee.Android/MainActivity.cs +++ b/QRBee/QRBee.Android/MainActivity.cs @@ -30,16 +30,10 @@ namespace QRBee.Droid LoadApplication(new App(AddServices)); ZXing.Mobile.MobileBarcodeScanner.Initialize(Application); - if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.Camera) == (int) Permission.Granted) + if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.Camera) != (int) Permission.Granted) { - + ActivityCompat.RequestPermissions(this, new String[] {Manifest.Permission.Camera}, 0); } - else - { - ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.Camera }, 0); - // ActivityCompat.RequestPermissions(this, new String[] { Manifest.Permission.UseFingerprint }, 0); - } - } public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) diff --git a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs index fcaba51..56b7639 100644 --- a/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs +++ b/QRBee/QRBee/ViewModels/MerchantPageViewModel.cs @@ -48,8 +48,9 @@ namespace QRBee.ViewModels //QrCode = null; IsVisible = false; - await service.InsertTransactionAsync(paymentRequest); + var response = await service.PayAsync(paymentRequest); + //TODO handle response await Application.Current.MainPage.DisplayAlert("Success", "The transaction completed successfully ", "Ok"); } catch (Exception e) diff --git a/QRBeeApi/Controllers/QRBeeController.cs b/QRBeeApi/Controllers/QRBeeController.cs index 1fd8bc6..a278377 100644 --- a/QRBeeApi/Controllers/QRBeeController.cs +++ b/QRBeeApi/Controllers/QRBeeController.cs @@ -35,11 +35,11 @@ namespace QRBee.Api.Controllers return _service.Update(clientId,value); } - [HttpPost("InsertTransaction")] - public Task InsertTransaction([FromBody] PaymentRequest value) + [HttpPost("Pay")] + public Task Pay([FromBody] PaymentRequest value) { _logger.LogInformation($"Trying to insert new transaction {value.ClientResponse.MerchantRequest.MerchantTransactionId}"); - return _service.InsertTransaction(value); + return _service.Pay(value); } } } diff --git a/QRBeeApi/Services/Database/IStorage.cs b/QRBeeApi/Services/Database/IStorage.cs index 62ca07f..732932b 100644 --- a/QRBeeApi/Services/Database/IStorage.cs +++ b/QRBeeApi/Services/Database/IStorage.cs @@ -31,6 +31,13 @@ /// Information to be inserted Task PutTransactionInfo(TransactionInfo info); + /// + /// Retrieve transaction information from database + /// + /// Identifier by which transaction information will be retrieved + /// Transaction information + Task GetTransactionInfoByTransactionId(string id); + /// /// Inserts CertificateInfo into database /// @@ -43,6 +50,13 @@ /// /// Identifier by which certificate information will be retrieved /// Certificate information - Task GetCertificateInfo(string id); + Task GetCertificateInfoByCertificateId(string id); + + /// + /// Retrieve certificate information from database + /// + /// Identifier by which certificate information will be retrieved + /// Certificate information + Task GetCertificateInfoByUserId(string clientId); } } diff --git a/QRBeeApi/Services/Database/Storage.cs b/QRBeeApi/Services/Database/Storage.cs index fa4a1df..dbbc0bf 100644 --- a/QRBeeApi/Services/Database/Storage.cs +++ b/QRBeeApi/Services/Database/Storage.cs @@ -102,6 +102,12 @@ namespace QRBee.Api.Services.Database return cursor.Current.FirstOrDefault(); } + public async Task GetTransactionInfoByTransactionId(string id) + { + var transaction = await TryGetTransactionInfo(id); + return transaction ?? throw new ApplicationException($"Transaction with Id: {id} not found."); + } + public async Task InsertCertificate(CertificateInfo info) { var collection = _database.GetCollection("Certificates"); @@ -131,7 +137,7 @@ namespace QRBee.Api.Services.Database /// null if certificate doesn't exist or CertificateInfo private async Task TryGetCertificateInfo(string id) { - var collection = _database.GetCollection("Transactions"); + var collection = _database.GetCollection("Certificates"); using var cursor = await collection.FindAsync($"{{ Id: \"{id}\" }}"); if (!await cursor.MoveNextAsync()) { @@ -141,10 +147,23 @@ namespace QRBee.Api.Services.Database return cursor.Current.FirstOrDefault(); } - public async Task GetCertificateInfo(string id) + public async Task GetCertificateInfoByCertificateId(string id) { var certificate = await TryGetCertificateInfo(id); return certificate ?? throw new ApplicationException($"Certificate with Id: {id} not found."); } + + public async Task GetCertificateInfoByUserId(string clientId) + { + var collection = _database.GetCollection("Certificates"); + using var cursor = await collection.FindAsync($"{{ ClientId: \"{clientId}\" }}"); + if (!await cursor.MoveNextAsync()) + { + throw new ApplicationException($"Certificate with ClientId: {clientId} not found."); + } + + return cursor.Current.FirstOrDefault() ?? throw new ApplicationException($"Certificate with ClientId: {clientId} not found."); + } + } } diff --git a/QRBeeApi/Services/Database/TransactionInfo.cs b/QRBeeApi/Services/Database/TransactionInfo.cs index e892bd9..be05600 100644 --- a/QRBeeApi/Services/Database/TransactionInfo.cs +++ b/QRBeeApi/Services/Database/TransactionInfo.cs @@ -34,5 +34,12 @@ namespace QRBee.Api.Services.Database public PaymentRequest Request { get; set; } + public enum TransactionStatus + { + Pending = 0, + Rejected = 1, + Succeeded = 2, + } + public TransactionStatus Status { get; set; } = TransactionStatus.Pending; } } diff --git a/QRBeeApi/Services/IQRBeeAPI.cs b/QRBeeApi/Services/IQRBeeAPI.cs index ad4c504..e3c6dc3 100644 --- a/QRBeeApi/Services/IQRBeeAPI.cs +++ b/QRBeeApi/Services/IQRBeeAPI.cs @@ -28,7 +28,7 @@ namespace QRBee.Api.Services /// Handles InsertTransaction request /// /// Payment request - Task InsertTransaction(PaymentRequest value); + Task Pay(PaymentRequest value); } } diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs index 57814de..2c0a1bb 100644 --- a/QRBeeApi/Services/QRBeeAPIService.cs +++ b/QRBeeApi/Services/QRBeeAPIService.cs @@ -45,7 +45,7 @@ namespace QRBee.Api.Services public async Task Register(RegistrationRequest request) { - Validate(request); + ValidateRegistration(request); var info = Convert(request); @@ -69,18 +69,11 @@ namespace QRBee.Api.Services public Task Update(string clientId, RegistrationRequest request) { - Validate(request); + ValidateRegistration(request); var info = Convert(request); return _storage.UpdateUser(info); } - - public Task InsertTransaction(PaymentRequest value) - { - var info = Convert(value); - return _storage.PutTransactionInfo(info); - } - - private void Validate(RegistrationRequest request) + private void ValidateRegistration(RegistrationRequest request) { if (request == null) { @@ -92,14 +85,14 @@ namespace QRBee.Api.Services 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) { throw new ApplicationException($"Name \"{name}\" isn't valid"); } var freq = Regex.Matches(email, @"[^@]+@[^@]+").Count; - if (string.IsNullOrEmpty(email) || email.IndexOf('@')<0 || freq>=2 || email.Length >= MaxEmailLength) + if (string.IsNullOrEmpty(email) || email.IndexOf('@') < 0 || freq >= 2 || email.Length >= MaxEmailLength) { throw new ApplicationException($"Email \"{email}\" isn't valid"); } @@ -120,9 +113,9 @@ namespace QRBee.Api.Services var verified = rsa.VerifyData( data, signature, - HashAlgorithmName.SHA256, + HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 - ); + ); if (!verified) { @@ -130,6 +123,105 @@ namespace QRBee.Api.Services } } + public async Task Pay(PaymentRequest value) + { + //1. Check payment request parameters for validity + ValidateTransaction(value); + + //2. Check client signature + var t2 = CheckSignature( + value.ClientResponse.AsDataForSignature(), + value.ClientResponse.ClientSignature, + value.ClientResponse.ClientId); + + //3. Check merchant signature + var t3 = CheckSignature( + value.ClientResponse.MerchantRequest.AsDataForSignature(), + value.ClientResponse.MerchantRequest.MerchantSignature, + value.ClientResponse.MerchantRequest.MerchantId); + + //4. Check if transaction was already processed + var t4 = CheckTransaction(value.ClientResponse.MerchantRequest.MerchantTransactionId); + + //Parallel task execution + await Task.WhenAll(t2, t3, t4); + + //5. Decrypt client card data + var clientCardData = DecryptClientData(value.ClientResponse.EncryptedClientCardData); + + //6. Check client card data for validity + //7. Register preliminary transaction record with expiry of one minute + //8. Send client card data to a payment gateway + //9. Record transaction with result + //10. Make response for merchant + var info = Convert(value); + await _storage.PutTransactionInfo(info); + return new PaymentResponse(); + } + + private void ValidateTransaction(PaymentRequest request) + { + if (request == null) + { + throw new NullReferenceException(); + } + + var clientId = request.ClientResponse.ClientId; + var merchantId = request.ClientResponse.MerchantRequest.MerchantId; + var transactionId = request.ClientResponse.MerchantRequest.MerchantTransactionId; + var amount = request.ClientResponse.MerchantRequest.Amount; + + if (clientId == null || merchantId == null || transactionId == null) + { + throw new ApplicationException("Id isn't valid"); + } + + if (amount is <= 0 or >= 10000) + { + throw new ApplicationException($"Amount \"{amount}\" isn't valid"); + } + } + + private async Task CheckSignature(string data,string signature, string id) + { + var info = await _storage.GetCertificateInfoByUserId(id); + var certificate = _securityService.Deserialize(info.Certificate); + + var check = _securityService.Verify( + Encoding.UTF8.GetBytes(data), + System.Convert.FromBase64String(signature), + certificate); + + if (!check) + { + throw new ApplicationException($"Signature is incorrect for Id: {id}."); + } + } + + private async Task CheckTransaction(string transactionId) + { + var info = await _storage.GetTransactionInfoByTransactionId(transactionId); + switch (info.Status) + { + case TransactionInfo.TransactionStatus.Succeeded: + throw new ApplicationException($"Transaction with Id: {transactionId} was already made."); + case TransactionInfo.TransactionStatus.Rejected: + throw new ApplicationException($"Transaction with Id: {transactionId} is not valid."); + case TransactionInfo.TransactionStatus.Pending: + throw new ApplicationException($"Transaction with Id: {transactionId} is already in progress."); + default: + return; + } + } + + private ClientCardData DecryptClientData(string encryptedClientCardData) + { + var info = System.Convert.FromBase64String(encryptedClientCardData); + var bytes = _securityService.Decrypt(info); + var s = Encoding.UTF8.GetString(bytes); + return ClientCardData.FromString(s); + } + private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters) { var rsaParameters = new RSAParameters