Client response QR code size reduced. Cloud provider support.

This commit is contained in:
Andrey Shabarshov 2022-03-26 13:31:17 +00:00
parent 1ab2706a53
commit 5c041da318
10 changed files with 132 additions and 91 deletions

View File

@ -16,7 +16,7 @@
/// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature) /// Convert ClientToMerchantResponse to string to be used as QR Code source (along with client signature)
/// </summary> /// </summary>
/// <returns> Converted string</returns> /// <returns> Converted string</returns>
public string AsQRCodeString() => $"{AsDataForSignature()}|{ClientSignature}"; public string AsQRCodeString() => $"{ClientId}|{TimeStampUTC:O}|{ClientSignature}";
public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}"; public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}";
@ -31,15 +31,14 @@
var s = input.Split('|'); var s = input.Split('|');
if (s.Length < 3) if (s.Length < 3)
{ {
throw new ApplicationException("Expected 3 or more elements"); throw new ApplicationException($"Expected 3 or more elements but got {s.Length}");
} }
var res = new ClientToMerchantResponse() var res = new ClientToMerchantResponse()
{ {
MerchantRequest = MerchantToClientRequest.FromString(string.Join("|", s.Skip(2))), ClientId = s[0],
ClientId = s[0], TimeStampUTC = DateTime.ParseExact(s[1], "O", null),
TimeStampUTC = DateTime.ParseExact(s[1], "O", null), ClientSignature = s[2]
ClientSignature = s[3]
}; };
return res; return res;

View File

@ -33,9 +33,9 @@ namespace QRBee.Core.Data
public static MerchantToClientRequest FromString(string input) public static MerchantToClientRequest FromString(string input)
{ {
var s = input.Split('|'); var s = input.Split('|');
if (s.Length != 6) if (s.Length < 6)
{ {
throw new ApplicationException("Expected 6 elements"); throw new ApplicationException($"Expected 6 elements but got {s.Length}");
} }
var res = new MerchantToClientRequest var res = new MerchantToClientRequest

View File

@ -13,18 +13,6 @@
/// Convert PaymentRequest to string /// Convert PaymentRequest to string
/// </summary> /// </summary>
/// <returns>Converted string</returns> /// <returns>Converted string</returns>
public string AsString() => ClientResponse.AsQRCodeString(); public string AsString() => $"{ClientResponse.AsDataForSignature()}|{ClientResponse.ClientSignature}";
public static PaymentRequest FromString(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ApplicationException("The input is wrong!");
}
//doesn't work
var response = ClientToMerchantResponse.FromString(input);
return new PaymentRequest(){ClientResponse = response};
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace QRBee.Droid
{ {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.2.0.155")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.0.11")]
public partial class Resource public partial class Resource
{ {

View File

@ -9,7 +9,13 @@ namespace QRBee.Droid.Services
{ {
internal class LocalSettings : ILocalSettings internal class LocalSettings : ILocalSettings
{ {
// Use https://10.0.2.2:7000 if you are running in emulator and API server on localhost
#if false
public string QRBeeApiUrl => "https://10.0.2.2:7000"; public string QRBeeApiUrl => "https://10.0.2.2:7000";
#else
public string QRBeeApiUrl => "https://qrbee-api.azurewebsites.net/";
#endif
public async Task SaveSettings(Settings settings) public async Task SaveSettings(Settings settings)
{ {

View File

@ -17,6 +17,8 @@ namespace QRBee.ViewModels
private decimal _amount; private decimal _amount;
private string _qrCode; private string _qrCode;
private MerchantToClientRequest _lastRequest;
public Command GenerateQrCommand { get; } public Command GenerateQrCommand { get; }
public Command ScanCommand{ get; } public Command ScanCommand{ get; }
@ -42,7 +44,13 @@ namespace QRBee.ViewModels
var client = new HttpClient(GetInsecureHandler()); var client = new HttpClient(GetInsecureHandler());
var service = new Core.Client.Client(_settings.QRBeeApiUrl, client); var service = new Core.Client.Client(_settings.QRBeeApiUrl, client);
var paymentRequest = PaymentRequest.FromString(result); var clientResponse = ClientToMerchantResponse.FromString(result);
clientResponse.MerchantRequest = _lastRequest;
var paymentRequest = new PaymentRequest
{
ClientResponse = clientResponse
};
//QrCode = null; //QrCode = null;
IsVisible = false; IsVisible = false;
@ -152,19 +160,21 @@ namespace QRBee.ViewModels
} }
else else
{ {
var trans = new MerchantToClientRequest var request = new MerchantToClientRequest
{ {
MerchantId = _settings.LoadSettings().ClientId, MerchantId = _settings.LoadSettings().ClientId,
MerchantTransactionId = Guid.NewGuid().ToString("D"), MerchantTransactionId = Guid.NewGuid().ToString("D"),
Name = Name, Name = Name,
Amount = Amount, Amount = Amount,
TimeStampUTC = DateTime.UtcNow TimeStampUTC = DateTime.UtcNow
}; };
var merchantSignature = _securityService.Sign(Encoding.UTF8.GetBytes(trans.AsDataForSignature())); var merchantSignature = _securityService.Sign(Encoding.UTF8.GetBytes(request.AsDataForSignature()));
trans.MerchantSignature = Convert.ToBase64String(merchantSignature); request.MerchantSignature = Convert.ToBase64String(merchantSignature);
QrCode = trans.AsQRCodeString(); _lastRequest = request;
QrCode = request.AsQRCodeString();
IsVisible = true; IsVisible = true;
} }

View File

@ -124,7 +124,7 @@ namespace QRBee.ViewModels
await _settings.SaveSettings(settings); await _settings.SaveSettings(settings);
//if (!_privateKeyHandler.Exists()) if (!_privateKeyHandler.Exists())
{ {
_privateKeyHandler.GeneratePrivateKey(settings.Name); _privateKeyHandler.GeneratePrivateKey(settings.Name);
} }

View File

@ -5,9 +5,10 @@ namespace QRBee.Api.Services.Database
public class CertificateInfo public class CertificateInfo
{ {
[BsonId] public string? Id { get; set; } [BsonId] public string? Id { get; set; }
public string? ClientId { get; set; } public string? ClientId { get; set; }
public string? Certificate { get; set; } public string? Email { get; set; }
public string? Certificate { get; set; }
public DateTime ServerTimeStamp { get; set; } public DateTime ServerTimeStamp { get; set; }
} }
} }

View File

@ -121,7 +121,7 @@ namespace QRBee.Api.Services.Database
throw new ApplicationException("Info Id is null."); throw new ApplicationException("Info Id is null.");
} }
var certificate = await TryGetCertificateInfo(info.Id); var certificate = await TryGetCertificateInfoByEmail(info.Email ?? throw new ApplicationException("Email is not set"));
if (certificate == null) if (certificate == null)
{ {
@ -130,7 +130,10 @@ namespace QRBee.Api.Services.Database
return; return;
} }
_logger.LogInformation($"Found certificate with ID: {info.Id}"); await collection.DeleteOneAsync($"{{ _id: \"{certificate.Id}\" }}");
await collection.ReplaceOneAsync($"{{ _id: \"{info.Id}\" }}", info, new ReplaceOptions() { IsUpsert = true });
_logger.LogInformation($"Found certificate with old ID: {certificate.Id} and replaced with new ID: {info.Id}");
} }
@ -151,6 +154,23 @@ namespace QRBee.Api.Services.Database
return cursor.Current.FirstOrDefault(); return cursor.Current.FirstOrDefault();
} }
/// <summary>
/// Try to find if the Certificate already exists in the database
/// </summary>
/// <param name="email">parameter by which to find CertificateInfo</param>
/// <returns>null if certificate doesn't exist or CertificateInfo</returns>
private async Task<CertificateInfo?> TryGetCertificateInfoByEmail(string email)
{
var collection = _database.GetCollection<CertificateInfo>("Certificates");
using var cursor = await collection.FindAsync($"{{ Email: \"{email}\" }}");
if (!await cursor.MoveNextAsync())
{
return null;
}
return cursor.Current.FirstOrDefault();
}
public async Task<CertificateInfo> GetCertificateInfoByCertificateId(string id) public async Task<CertificateInfo> GetCertificateInfoByCertificateId(string id)
{ {
var certificate = await TryGetCertificateInfo(id); var certificate = await TryGetCertificateInfo(id);

View File

@ -57,7 +57,7 @@ namespace QRBee.Api.Services
var clientCertificate = _securityService.CreateCertificate(clientId,bytes); var clientCertificate = _securityService.CreateCertificate(clientId,bytes);
var convertedClientCertificate = Convert(clientCertificate, clientId); var convertedClientCertificate = Convert(clientCertificate, clientId,request.Email);
await _storage.InsertCertificate(convertedClientCertificate); await _storage.InsertCertificate(convertedClientCertificate);
return new RegistrationResponse return new RegistrationResponse
@ -126,62 +126,78 @@ namespace QRBee.Api.Services
public async Task<PaymentResponse> Pay(PaymentRequest value) public async Task<PaymentResponse> Pay(PaymentRequest value)
{ {
//1. Check payment request parameters for validity try
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
await CheckClientCardData(clientCardData);
//7. Register preliminary transaction record with expiry of one minute
var info = Convert(value);
info.Status = TransactionInfo.TransactionStatus.Pending;
await _storage.PutTransactionInfo(info);
//8. Send client card data to a payment gateway
var res = await _paymentGateway.Payment(info, clientCardData);
//9. Record transaction with result
if (res.Success)
{ {
info.Status=TransactionInfo.TransactionStatus.Succeeded; //1. Check payment request parameters for validity
} ValidateTransaction(value);
else
{ //2. Check client signature
info.Status = TransactionInfo.TransactionStatus.Rejected; var t2 = CheckSignature(
info.RejectReason = res.ErrorMessage; value.ClientResponse.AsDataForSignature(),
} value.ClientResponse.ClientSignature,
await _storage.UpdateTransaction(info); 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
await CheckClientCardData(clientCardData);
//7. Register preliminary transaction record with expiry of one minute
var info = Convert(value);
info.Status = TransactionInfo.TransactionStatus.Pending;
await _storage.PutTransactionInfo(info);
//8. Send client card data to a payment gateway
var res = await _paymentGateway.Payment(info, clientCardData);
//9. Record transaction with result
if (res.Success)
{
info.Status=TransactionInfo.TransactionStatus.Succeeded;
}
else
{
info.Status = TransactionInfo.TransactionStatus.Rejected;
info.RejectReason = res.ErrorMessage;
}
await _storage.UpdateTransaction(info);
//10. Make response for merchant
var response = MakePaymentResponse(value, info.TransactionId ?? "", info.Status==TransactionInfo.TransactionStatus.Succeeded, info.RejectReason);
return response;
}
catch (Exception e)
{
var response = MakePaymentResponse(value, "", false, e.Message);
return response;
}
}
private PaymentResponse MakePaymentResponse(PaymentRequest value, string transactionId, bool result = true, string? errorMessage = null)
{
//10. Make response for merchant
var response = new PaymentResponse var response = new PaymentResponse
{ {
ServerTransactionId = info.TransactionId, ServerTransactionId = transactionId,
PaymentRequest = value, PaymentRequest = value,
ServerTimeStampUTC = DateTime.UtcNow, ServerTimeStampUTC = DateTime.UtcNow,
Success = res.Success, Success = result,
RejectReason = res.ErrorMessage, RejectReason = errorMessage,
}; };
var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature())); var signature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
@ -224,7 +240,7 @@ namespace QRBee.Api.Services
if (!check) if (!check)
{ {
throw new ApplicationException($"Signature is incorrect for Id: {id}."); throw new ApplicationException($"Signature is incorrect for Id: {id}. Data: {data}");
} }
} }
@ -309,13 +325,14 @@ namespace QRBee.Api.Services
return new TransactionInfo(request, DateTime.UtcNow); return new TransactionInfo(request, DateTime.UtcNow);
} }
private CertificateInfo Convert(X509Certificate2 certificate, string clientId) private CertificateInfo Convert(X509Certificate2 certificate, string clientId, string email)
{ {
var convertedCertificate = _securityService.Serialize(certificate); var convertedCertificate = _securityService.Serialize(certificate);
return new CertificateInfo return new CertificateInfo
{ {
Id = certificate.SerialNumber, Id = certificate.SerialNumber,
ClientId = clientId, ClientId = clientId,
Email = email,
Certificate = convertedCertificate, Certificate = convertedCertificate,
ServerTimeStamp = DateTime.UtcNow ServerTimeStamp = DateTime.UtcNow
}; };