Dependency Injection container added. SecurityService work stated, not finished. Minor UI improvements. BROKEN

This commit is contained in:
Andrey Shabarshov 2022-02-16 17:35:00 +00:00
parent 21f16b862c
commit 551a44c38d
24 changed files with 385 additions and 139 deletions

View File

@ -36,7 +36,9 @@
/// 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() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}"; public string AsQRCodeString() => $"{AsDataForSignature()}|{ClientSignature}";
public string AsDataForSignature() => $"{ClientId}|{TimeStampUTC:O}|{MerchantRequest.AsQRCodeString()}";
/// <summary> /// <summary>
/// Convert from string /// Convert from string
@ -56,7 +58,8 @@
{ {
MerchantRequest = MerchantToClientRequest.FromString(string.Join("|", s.Skip(2))), 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[3]
}; };
return res; return res;

View File

@ -44,7 +44,9 @@ namespace QRBee.Core.Data
/// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature) /// Convert MerchantToClientRequest to string to be used as QR Code source (along with merchant signature)
/// </summary> /// </summary>
/// <returns>String conversion</returns> /// <returns>String conversion</returns>
public string AsQRCodeString() => $"{MerchantTransactionId}|{MerchantId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:O}"; public string AsQRCodeString() => $"{AsDataForSignature()}|{MerchantSignature}";
public string AsDataForSignature() => $"{MerchantId}|{MerchantTransactionId}|{Name}|{Amount.ToString("0.00", CultureInfo.InvariantCulture)}|{TimeStampUTC:O}";
/// <summary> /// <summary>
/// Convert from string /// Convert from string
@ -55,9 +57,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 != 5) if (s.Length != 6)
{ {
throw new ApplicationException("Expected 5 elements"); throw new ApplicationException("Expected 6 elements");
} }
var res = new MerchantToClientRequest var res = new MerchantToClientRequest
@ -66,7 +68,8 @@ namespace QRBee.Core.Data
MerchantTransactionId = s[1], MerchantTransactionId = s[1],
Name = s[2], Name = s[2],
Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture), Amount = Convert.ToDecimal(s[3], CultureInfo.InvariantCulture),
TimeStampUTC = DateTime.ParseExact(s[4],"O",null) TimeStampUTC = DateTime.ParseExact(s[4],"O",null),
MerchantSignature = s[5]
}; };

View File

@ -17,14 +17,14 @@ namespace QRBee.Core.Security
/// Generate new private key and store it /// Generate new private key and store it
/// </summary> /// </summary>
/// <param name="subjectName"></param> /// <param name="subjectName"></param>
/// <returns>Certificate request to be sent to CA</returns> /// <returns>Certificate request to be sent to CA in PEM format</returns>
byte [] GeneratePrivateKey(string? subjectName = null); string 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></returns> /// <returns>Certificate request to be sent to CA in PEM format</returns>
byte[] CreateCertificateRequest(); string CreateCertificateRequest();
/// <summary> /// <summary>
/// Attach CA-generated public key certificate to the private key /// Attach CA-generated public key certificate to the private key

View File

@ -0,0 +1,39 @@
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();
}
}
}

View File

@ -6,11 +6,15 @@ using Android.Runtime;
using Android.OS; using Android.OS;
using Android.Support.V4.Content; using Android.Support.V4.Content;
using AndroidX.Core.App; using AndroidX.Core.App;
using Microsoft.Extensions.DependencyInjection;
using Plugin.Fingerprint; using Plugin.Fingerprint;
using QRBee.Core.Security;
using QRBee.Droid.Services;
using QRBee.Services;
namespace QRBee.Droid namespace QRBee.Droid
{ {
[Activity(Label = "QRBee", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize )] [Activity(Label = "QRBee", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Orientation, ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{ {
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
@ -23,7 +27,7 @@ namespace QRBee.Droid
CrossFingerprint.SetCurrentActivityResolver(()=>Xamarin.Essentials.Platform.CurrentActivity); CrossFingerprint.SetCurrentActivityResolver(()=>Xamarin.Essentials.Platform.CurrentActivity);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App()); LoadApplication(new App(AddServices));
ZXing.Mobile.MobileBarcodeScanner.Initialize(Application); 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)
@ -37,11 +41,22 @@ namespace QRBee.Droid
} }
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{ {
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
ZXing.Net.Mobile.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults); ZXing.Net.Mobile.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
} }
private static void AddServices(IServiceCollection services)
{
services
.AddSingleton<ISecurityService,AndroidSecurityService>()
.AddSingleton<ILocalSettings, LocalSettings>()
.AddSingleton<IQRScanner, QRScannerService>()
;
}
} }
} }

View File

@ -59,6 +59,9 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection">
<Version>6.0.0</Version>
</PackageReference>
<PackageReference Include="Plugin.Fingerprint"> <PackageReference Include="Plugin.Fingerprint">
<Version>2.1.4</Version> <Version>2.1.4</Version>
</PackageReference> </PackageReference>
@ -80,7 +83,7 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\LocalSettings.cs" /> <Compile Include="Services\LocalSettings.cs" />
<Compile Include="Services\QRScannerService.cs" /> <Compile Include="Services\QRScannerService.cs" />
<Compile Include="Services\SecurityService.cs" /> <Compile Include="Services\AndroidSecurityService.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Resources\AboutResources.txt" /> <None Include="Resources\AboutResources.txt" />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="launcher_background">#FFFFFF</color> <color name="launcher_background">#FFFFFF</color>
<color name="colorPrimary">#3F51B5</color> <color name="colorPrimary">#FFC928</color>
<color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark">#FFC928</color>
<color name="colorAccent">#FF4081</color> <color name="colorAccent">#FFC928</color>
</resources> </resources>

View File

@ -0,0 +1,56 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using QRBee.Core.Security;
namespace QRBee.Droid.Services
{
internal class AndroidSecurityService : SecurityServiceBase
{
public AndroidSecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler)
{
}
/// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{
throw new ApplicationException("Client never issues certificates");
}
private const string CertHeader = "-----BEGIN CERTIFICATE-----";
private const string CertFooter = "-----END CERTIFICATE-----";
/// <inheritdoc/>
public override X509Certificate2 Deserialize(string pemData)
{
var start = pemData.IndexOf(CertHeader);
if (start == -1)
throw new ApplicationException("Invalid certificate format");
start = start + CertHeader.Length;
var end = pemData.IndexOf(CertFooter);
if (end == -1)
throw new ApplicationException("Invalid certificate format");
var base64 = pemData.Substring(start, end - start); // contains new lines, but it does not matter
var data = Convert.FromBase64String(base64);
return new X509Certificate2(data);
}
/// <inheritdoc/>
public override string Serialize(X509Certificate2 cert)
{
// https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem
var builder = new StringBuilder();
builder.AppendLine(CertHeader);
builder.AppendLine(Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
builder.AppendLine(CertFooter);
return builder.ToString();
}
}
}

View File

@ -1,40 +0,0 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using QRBee.Core.Security;
namespace QRBee.Api.Services
{
internal class SecurityService : SecurityServiceBase
{
public SecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler)
{
}
/// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{
throw new ApplicationException("Client never issues certificates");
}
/// <inheritdoc/>
public override X509Certificate2 Deserialize(string pemData)
{
throw new NotImplementedException();
//return X509Certificate2.CreateFromPem(pemData);
}
/// <inheritdoc/>
public override string Serialize(X509Certificate2 cert)
{
throw new NotImplementedException();
// https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem
//var pem = PemEncoding.Write("CERTIFICATE", cert.RawData);
//return new string(pem);
}
}
}

View File

@ -7,7 +7,8 @@
--> -->
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<Color x:Key="Primary">#2196F3</Color> <!--2296F3 -->
<Color x:Key="Primary">#FFC928</Color>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="TextColor" Value="White"></Setter> <Setter Property="TextColor" Value="White"></Setter>
<Setter Property="VisualStateManager.VisualStateGroups"> <Setter Property="VisualStateManager.VisualStateGroups">

View File

@ -1,21 +1,52 @@
using QRBee.Services; using QRBee.Services;
using QRBee.Views; using QRBee.Views;
using System; using System;
using Microsoft.Extensions.DependencyInjection;
using QRBee.Core.Security;
using QRBee.ViewModels;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace QRBee namespace QRBee
{ {
public partial class App : Application public partial class App : Application
{ {
public App() public App(Action<IServiceCollection> addPlatformServices = null)
{ {
InitializeComponent(); InitializeComponent();
SetupServices(addPlatformServices);
DependencyService.Register<MockDataStore>(); DependencyService.Register<MockDataStore>();
MainPage = new AppShell(); MainPage = new AppShell();
} }
protected static IServiceProvider ServiceProvider { get; set; }
void SetupServices(Action<IServiceCollection> addPlatformServices = null)
{
var services = new ServiceCollection();
// Add platform specific services
addPlatformServices?.Invoke(services);
// TODO: Add core services here
services
.AddSingleton<IPrivateKeyHandler, ClientPrivateKeyHandler>()
;
// Add ViewModels
services
.AddTransient<MerchantPageViewModel>()
.AddTransient<ClientPageViewModel>()
.AddTransient<RegisterViewModel>()
.AddTransient<LoginViewModel>()
;
ServiceProvider = services.BuildServiceProvider();
}
public static BaseViewModel GetViewModel<TViewModel>() where TViewModel : BaseViewModel => ServiceProvider.GetService<TViewModel>();
protected override void OnStart() protected override void OnStart()
{ {

View File

@ -30,8 +30,6 @@
</Shell.Resources> </Shell.Resources>
<TabBar> <TabBar>
<!-- <ShellContent Title="About" Icon="icon_about.png" Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" /> -->
<!-- <ShellContent Title="Browse" Icon="icon_feed.png" ContentTemplate="{DataTemplate local:ItemsPage}" /> -->
<ShellContent Title="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}"/> <ShellContent Title="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}"/>
</TabBar> </TabBar>

View File

@ -14,6 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Plugin.Fingerprint" Version="2.1.4" /> <PackageReference Include="Plugin.Fingerprint" Version="2.1.4" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2244" /> <PackageReference Include="Xamarin.Forms" Version="5.0.0.2244" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" /> <PackageReference Include="Xamarin.Essentials" Version="1.7.0" />

View File

@ -0,0 +1,23 @@
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;
}
}
}

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Text;
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;
@ -8,17 +10,26 @@ namespace QRBee.ViewModels
{ {
internal class ClientPageViewModel: BaseViewModel internal class ClientPageViewModel: BaseViewModel
{ {
public bool _isVisible; private readonly IQRScanner _scanner;
private readonly ISecurityService _securityService;
public bool _isAcceptDenyButtonVisible;
public bool _isQrVisible;
public bool _isScanButtonVisible;
public string _amount; public string _amount;
private string _qrCode; private string _qrCode;
private MerchantToClientRequest _merchantToClientRequest; private MerchantToClientRequest _merchantToClientRequest;
private readonly ClientPage _clientPage;
public ClientPageViewModel(Views.ClientPage clientPage) public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService)
{ {
_scanner = scanner;
_securityService = securityService;
ScanCommand = new Command(OnScanButtonClicked); ScanCommand = new Command(OnScanButtonClicked);
GenerateQrCommand = new Command(OnGenerateQrClicked); AcceptQrCommand = new Command(OnAcceptQrCommand);
_clientPage = clientPage; DenyQrCommand = new Command(OnDenyQrCommand);
IsScanButtonVisible = true;
} }
public Command ScanCommand public Command ScanCommand
@ -26,23 +37,33 @@ namespace QRBee.ViewModels
get; get;
} }
public Command GenerateQrCommand public Command AcceptQrCommand
{ {
get; get;
} }
public Command DenyQrCommand
{
get;
}
private async void OnScanButtonClicked(object sender) private async void OnScanButtonClicked(object sender)
{ {
QrCode = null;
IsQrVisible = false;
IsAcceptDenyButtonVisible = false;
try try
{ {
var scanner = DependencyService.Get<IQRScanner>(); var result = await _scanner.ScanQR();
var result = await scanner.ScanQR();
if (result == null) if (result == null)
return; return;
_merchantToClientRequest = MerchantToClientRequest.FromString(result); _merchantToClientRequest = MerchantToClientRequest.FromString(result);
Amount = $"{_merchantToClientRequest.Amount:N2}"; Amount = $"{_merchantToClientRequest.Amount:N2}";
IsVisible = true; IsAcceptDenyButtonVisible = true;
IsScanButtonVisible = false;
} }
catch (Exception) catch (Exception)
{ {
@ -64,17 +85,45 @@ namespace QRBee.ViewModels
} }
} }
public bool IsVisible public bool IsAcceptDenyButtonVisible
{ {
get => _isVisible; get => _isAcceptDenyButtonVisible;
set set
{ {
if (value == _isVisible) if (value == _isAcceptDenyButtonVisible)
{ {
return; return;
} }
_isVisible = value; _isAcceptDenyButtonVisible = value;
OnPropertyChanged(nameof(IsVisible)); OnPropertyChanged(nameof(IsAcceptDenyButtonVisible));
}
}
public bool IsQrVisible
{
get => _isQrVisible;
set
{
if (value == _isQrVisible)
{
return;
}
_isQrVisible = value;
OnPropertyChanged(nameof(IsQrVisible));
}
}
public bool IsScanButtonVisible
{
get => _isScanButtonVisible;
set
{
if (value == _isScanButtonVisible)
{
return;
}
_isScanButtonVisible = value;
OnPropertyChanged(nameof(IsScanButtonVisible));
} }
} }
@ -83,7 +132,6 @@ namespace QRBee.ViewModels
get => _qrCode; get => _qrCode;
set set
{ {
// _qrCode = $"{Amount}/{Name}";
if (_qrCode == value) if (_qrCode == value)
return; return;
@ -96,12 +144,11 @@ namespace QRBee.ViewModels
/// Reaction on GenerateQR button clicked /// Reaction on GenerateQR button clicked
/// </summary> /// </summary>
/// <param name="obj"></param> /// <param name="obj"></param>
public async void OnGenerateQrClicked(object obj) public async void OnAcceptQrCommand(object obj)
{ {
var answer = await _clientPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No"); var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
if (!answer) if (!answer) return;
return;
var response = new ClientToMerchantResponse var response = new ClientToMerchantResponse
{ {
@ -111,10 +158,23 @@ namespace QRBee.ViewModels
MerchantRequest = _merchantToClientRequest MerchantRequest = _merchantToClientRequest
}; };
// TODO Create merchant signature. // TODO Create client signature.
var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature);
QrCode = response.AsQRCodeString(); QrCode = response.AsQRCodeString();
IsQrVisible = true;
IsAcceptDenyButtonVisible = false;
IsScanButtonVisible = true;
}
public void OnDenyQrCommand(object obj)
{
QrCode = null;
IsQrVisible = false;
IsAcceptDenyButtonVisible = false;
IsScanButtonVisible = true;
Amount = "";
} }
} }

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using QRBee.Core.Data; using QRBee.Core.Data;
using QRBee.Core.Security;
using QRBee.Services; using QRBee.Services;
using Xamarin.Forms; using Xamarin.Forms;
@ -9,6 +11,9 @@ namespace QRBee.ViewModels
{ {
internal class MerchantPageViewModel : BaseViewModel internal class MerchantPageViewModel : BaseViewModel
{ {
private readonly IQRScanner _scanner;
private readonly ILocalSettings _settings;
private readonly ISecurityService _securityService;
private bool _isVisible; private bool _isVisible;
private decimal _amount; private decimal _amount;
private string _qrCode; private string _qrCode;
@ -16,8 +21,11 @@ namespace QRBee.ViewModels
public Command GenerateQrCommand { get; } public Command GenerateQrCommand { get; }
public Command ScanCommand{ get; } public Command ScanCommand{ get; }
public MerchantPageViewModel() public MerchantPageViewModel(IQRScanner scanner, ILocalSettings settings, ISecurityService securityService)
{ {
_scanner = scanner;
_settings = settings;
_securityService = securityService;
ScanCommand = new Command(OnScanButtonClicked); ScanCommand = new Command(OnScanButtonClicked);
GenerateQrCommand = new Command(OnGenerateQrClicked); GenerateQrCommand = new Command(OnGenerateQrClicked);
var localSettings = DependencyService.Resolve<ILocalSettings>(); var localSettings = DependencyService.Resolve<ILocalSettings>();
@ -28,15 +36,13 @@ namespace QRBee.ViewModels
{ {
try try
{ {
var scanner = DependencyService.Get<IQRScanner>(); var result = await _scanner.ScanQR();
var result = await scanner.ScanQR();
if (result == null) if (result == null)
return; return;
var client = new HttpClient(GetInsecureHandler()); var client = new HttpClient(GetInsecureHandler());
var localSettings = DependencyService.Resolve<ILocalSettings>();
var service = new Core.Client.Client(localSettings.QRBeeApiUrl, client); var service = new Core.Client.Client(_settings.QRBeeApiUrl, client);
var paymentRequest = PaymentRequest.FromString(result); var paymentRequest = PaymentRequest.FromString(result);
//QrCode = null; //QrCode = null;
@ -132,7 +138,10 @@ namespace QRBee.ViewModels
Amount = Amount, Amount = Amount,
TimeStampUTC = DateTime.UtcNow TimeStampUTC = DateTime.UtcNow
}; };
// TODO Create merchant signature.
var merchantSignature = _securityService.Sign(Encoding.UTF8.GetBytes(trans.AsDataForSignature()));
trans.MerchantSignature = Convert.ToBase64String(merchantSignature);
QrCode = trans.AsQRCodeString(); QrCode = trans.AsQRCodeString();
IsVisible = true; IsVisible = true;
} }

View File

@ -12,13 +12,14 @@ namespace QRBee.ViewModels
{ {
internal class RegisterViewModel: BaseViewModel internal class RegisterViewModel: BaseViewModel
{ {
private readonly ILocalSettings _settings;
private string _password1; private string _password1;
private string _password2; private string _password2;
public RegisterViewModel() public RegisterViewModel(ILocalSettings localSettings)
{ {
_settings = localSettings;
RegisterCommand = new Command(OnRegisterClicked); RegisterCommand = new Command(OnRegisterClicked);
var localSettings = DependencyService.Resolve<ILocalSettings>();
var settings = localSettings.LoadSettings(); var settings = localSettings.LoadSettings();
Name = settings.Name; Name = settings.Name;
@ -96,14 +97,13 @@ namespace QRBee.ViewModels
{ {
//TODO when to dispose the client? //TODO when to dispose the client?
var client = new HttpClient(GetInsecureHandler()); var client = new HttpClient(GetInsecureHandler());
var localSettings = DependencyService.Resolve<ILocalSettings>();
var service = new Core.Client.Client(localSettings.QRBeeApiUrl,client); var service = new Core.Client.Client(_settings.QRBeeApiUrl,client);
try try
{ {
//TODO Check if ClientId already in LocalSettings. If Yes update data in database //TODO Check if ClientId already in LocalSettings. If Yes update data in database
var settings = localSettings.LoadSettings(); var settings = _settings.LoadSettings();
//save local settings //save local settings
settings.CardHolderName = CardHolderName; settings.CardHolderName = CardHolderName;
@ -117,7 +117,7 @@ namespace QRBee.ViewModels
settings.Name = Name; settings.Name = Name;
settings.PIN = Pin; settings.PIN = Pin;
await localSettings.SaveSettings(settings); await _settings.SaveSettings(settings);
var request = new RegistrationRequest var request = new RegistrationRequest
{ {
@ -132,9 +132,9 @@ namespace QRBee.ViewModels
var response = await service.RegisterAsync(request); var response = await service.RegisterAsync(request);
// Save ClientId to LocalSettings // Save ClientId to LocalSettings
settings = localSettings.LoadSettings(); settings = _settings.LoadSettings();
settings.ClientId = response.ClientId; settings.ClientId = response.ClientId;
await localSettings.SaveSettings(settings); await _settings.SaveSettings(settings);
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");

View File

@ -6,11 +6,9 @@
xmlns:common="clr-namespace:ZXing.Common;assembly=zxing.portable" xmlns:common="clr-namespace:ZXing.Common;assembly=zxing.portable"
x:DataType="viewmodels:ClientPageViewModel" x:DataType="viewmodels:ClientPageViewModel"
x:Class="QRBee.Views.ClientPage"> x:Class="QRBee.Views.ClientPage">
<ContentPage.Content> <ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"> <StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<!-- <Label Text="Hello this is a test page." -->
<!-- VerticalOptions="CenterAndExpand" -->
<!-- HorizontalOptions="CenterAndExpand" /> -->
<StackLayout Orientation="Vertical" VerticalOptions="FillAndExpand"> <StackLayout Orientation="Vertical" VerticalOptions="FillAndExpand">
<StackLayout Orientation="Vertical"> <StackLayout Orientation="Vertical">
<Label VerticalOptions="FillAndExpand" Text="Amount:"/> <Label VerticalOptions="FillAndExpand" Text="Amount:"/>
@ -21,7 +19,8 @@
BarcodeFormat="QR_CODE" BarcodeFormat="QR_CODE"
BarcodeValue="{Binding QrCode}" BarcodeValue="{Binding QrCode}"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"> VerticalOptions="FillAndExpand"
IsVisible="{Binding IsQrVisible}">
<forms:ZXingBarcodeImageView.BarcodeOptions> <forms:ZXingBarcodeImageView.BarcodeOptions>
<common:EncodingOptions Width="300" Height="300" /> <common:EncodingOptions Width="300" Height="300" />
</forms:ZXingBarcodeImageView.BarcodeOptions> </forms:ZXingBarcodeImageView.BarcodeOptions>
@ -31,10 +30,10 @@
<StackLayout Orientation="Vertical" VerticalOptions="End" Margin="0,0,0,10"> <StackLayout Orientation="Vertical" VerticalOptions="End" Margin="0,0,0,10">
<StackLayout Orientation="Horizontal"> <StackLayout Orientation="Horizontal">
<Button Text="Accept" HorizontalOptions="FillAndExpand" BackgroundColor="DarkGreen" IsVisible="{Binding IsVisible}" Command="{Binding GenerateQrCommand}"/> <Button Text="Accept" HorizontalOptions="FillAndExpand" BackgroundColor="DarkGreen" IsVisible="{Binding IsAcceptDenyButtonVisible}" Command="{Binding AcceptQrCommand}"/>
<Button Text="Deny" HorizontalOptions="FillAndExpand" BackgroundColor="DarkRed" IsVisible="{Binding IsVisible}"/> <Button Text="Deny" HorizontalOptions="FillAndExpand" BackgroundColor="DarkRed" IsVisible="{Binding IsAcceptDenyButtonVisible}" Command="{Binding DenyQrCommand}"/>
</StackLayout> </StackLayout>
<Button Text="Scan me" HorizontalOptions="FillAndExpand" BackgroundColor="Aqua" TextColor="Red" Command="{Binding ScanCommand}"/> <Button Text="Scan merchant QR Code" HorizontalOptions="FillAndExpand" BackgroundColor="Aqua" TextColor="Red" Command="{Binding ScanCommand}" IsVisible="{Binding IsScanButtonVisible}"/>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>

View File

@ -11,8 +11,8 @@ namespace QRBee.Views
{ {
public ClientPage() public ClientPage()
{ {
BindingContext = App.GetViewModel<ClientPageViewModel>();
InitializeComponent(); InitializeComponent();
this.BindingContext = new ClientPageViewModel(this);
} }
} }
} }

View File

@ -8,6 +8,4 @@
<views:ClientPage Title="Client"/> <views:ClientPage Title="Client"/>
<views:MerchantPage Title="Merchant"/> <views:MerchantPage Title="Merchant"/>
</TabbedPage.Children> </TabbedPage.Children>
<!-- <ContentPage Title="Merchant" /> -->
</TabbedPage> </TabbedPage>

View File

@ -8,7 +8,7 @@ namespace QRBee.Views
{ {
public MerchantPage() public MerchantPage()
{ {
BindingContext = new MerchantPageViewModel(); BindingContext = App.GetViewModel<MerchantPageViewModel>();
InitializeComponent(); InitializeComponent();
} }
} }

View File

@ -15,7 +15,7 @@ namespace QRBee.Views
public RegisterPage() public RegisterPage()
{ {
InitializeComponent(); InitializeComponent();
this.BindingContext = new RegisterViewModel(); BindingContext = App.GetViewModel<RegisterViewModel>();
} }
} }
} }

View File

@ -1,5 +1,7 @@
using System.Security.Cryptography; using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QRBee.Core.Security; using QRBee.Core.Security;
@ -16,46 +18,70 @@ namespace QRBee.Api.Services
/// <inheritdoc/> /// <inheritdoc/>
public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey) public override X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey)
{ {
if (!IsValidSubjectName(subjectName)) throw new ApplicationException("Client never issues certificates");
throw new CryptographicException("Invalid subject name");
// https://stackoverflow.com/questions/60930065/generate-and-sign-certificate-in-different-machines-c-sharp
using var publicKey = RSA.Create();
publicKey.ImportSubjectPublicKeyInfo(rsaPublicKey, out var nBytes);
//TODO: check that nBytes is within allowed range
var request = new CertificateRequest("CN=" + subjectName, publicKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.NonRepudiation, false));
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
// create a new certificate
using var caPrivateKey = PrivateKeyHandler.LoadPrivateKey();
var certificate = request.Create(
caPrivateKey,
DateTimeOffset.UtcNow.AddSeconds(-1), // user can use it now
DateTimeOffset.UtcNow.AddDays(30), // user need to login every 30 days
Guid.NewGuid().ToByteArray());
return certificate;
} }
/// <summary>
/// Generate Client certificate request (i.e. without KeyCertSign usage extension)
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="rsa"></param>
/// <returns></returns>
private static CertificateRequest CreateRequest(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,
//| X509KeyUsageFlags.KeyCertSign,
false));
// request.CertificateExtensions.Add(
// new X509EnhancedKeyUsageExtension(
// new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
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)
{ {
return X509Certificate2.CreateFromPem(pemData); var start = pemData.IndexOf(CertHeader);
if (start == -1)
throw new ApplicationException("Invalid certificate format");
start = start + CertHeader.Length;
var end = pemData.IndexOf(CertFooter);
if (end == -1)
throw new ApplicationException("Invalid certificate format");
var base64 = pemData.Substring(start, end - start); // contains new lines, but it does not matter
var data = Convert.FromBase64String(base64);
return new X509Certificate2(data);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override string Serialize(X509Certificate2 cert) public override string Serialize(X509Certificate2 cert)
{ {
// https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem // https://stackoverflow.com/questions/43928064/export-private-public-keys-from-x509-certificate-to-pem
var pem = PemEncoding.Write("CERTIFICATE", cert.RawData); var builder = new StringBuilder();
return new string(pem); builder.AppendLine(CertHeader);
builder.AppendLine(Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
builder.AppendLine(CertFooter);
return builder.ToString();
} }
} }

View File

@ -0,0 +1,21 @@
using QRBee.Core.Security;
namespace QRBee.Api.Services
{
/// <summary>
/// Private key handler for API server
/// </summary>
public class ServerPrivateKeyHandler : PrivateKeyHandlerBase
{
private const string CACommonName = "QRBee-CA";
private const string VeryBadAndInsecureCertificatePassword = "U…Š)+œ¶€=ø‘ c¬Í↨ð´áY/ÿ☼æX";
public ServerPrivateKeyHandler()
{
CertificatePassword = VeryBadAndInsecureCertificatePassword;
CommonName = CACommonName;
}
}
}