Encrypted Client card data block added. Android public and private keeys separated into different files.

This commit is contained in:
Andrey Shabarshov 2022-02-25 17:57:01 +00:00
parent 81a87262c5
commit 4722585e17
14 changed files with 185 additions and 145 deletions

View File

@ -2,49 +2,20 @@
{ {
public class ClientCardData public class ClientCardData
{ {
public string TransactionId { get; set; }
public string CardNumber public string CardNumber { get; set; }
{ public string ExpirationDateMMYY { get; set; }
get; public string ValidFrom { get; set; }
set; public string CardHolderName { get; set; }
} public string CVC { get; set; }
public int? IssueNo { 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;
}
/// <summary> /// <summary>
/// Convert ClientCardData to string to be used as a source for encryption. /// 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. /// WARNING: this should always be encrypted and never transmitted in clear text form.
/// </summary> /// </summary>
/// <returns>Converted string</returns> /// <returns>Converted string</returns>
public string AsString() => $"{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}"; public string AsString() => $"{TransactionId}|{CardNumber}|{ExpirationDateMMYY}|{ValidFrom}|{CardHolderName}|{CVC}|{IssueNo}";
} }
} }

View File

@ -12,7 +12,13 @@ namespace QRBee.Core.Data
set; set;
} }
public string Certificate public string ClientCertificate
{
get;
set;
}
public string APIServerCertificate
{ {
get; get;
set; set;

View File

@ -1,4 +1,5 @@
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace QRBee.Core.Security namespace QRBee.Core.Security
{ {
@ -38,6 +39,12 @@ namespace QRBee.Core.Security
/// self-signed until CA issues a proper certificate /// self-signed until CA issues a proper certificate
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
X509Certificate2 LoadPrivateKey(); RSA LoadPrivateKey();
/// <summary>
/// Get public key certificate
/// </summary>
/// <returns>Public key certificate</returns>
X509Certificate2 GetCertificate();
} }
} }

View File

@ -47,6 +47,11 @@ namespace QRBee.Core.Security
// -------------------------- certificate services -------------------------- // -------------------------- certificate services --------------------------
/// <summary>
/// API Server certificate
/// </summary>
X509Certificate2 APIServerCertificate { get; set; }
/// <summary> /// <summary>
/// Convert binary block to X509Certificate2. /// Convert binary block to X509Certificate2.
/// <see cref="X509Certificate2.CreateFromPem"/> /// <see cref="X509Certificate2.CreateFromPem"/>

View File

@ -39,12 +39,13 @@ namespace QRBee.Core.Security
/// <inheritdoc/> /// <inheritdoc/>
public byte[] Decrypt(byte[] data) public byte[] Decrypt(byte[] data)
{ {
using var myCert = LoadPrivateKey(); using var rsa = LoadPrivateKey();
using var rsa = myCert.GetRSAPrivateKey();
var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found"); var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found");
return res; return res;
} }
public abstract X509Certificate2 APIServerCertificate { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public byte [] Encrypt(byte[] data, X509Certificate2 destCert) public byte [] Encrypt(byte[] data, X509Certificate2 destCert)
{ {
@ -63,8 +64,7 @@ namespace QRBee.Core.Security
/// <inheritdoc/> /// <inheritdoc/>
public byte[] Sign(byte[] data) public byte[] Sign(byte[] data)
{ {
using var myCert = LoadPrivateKey(); using var rsa = LoadPrivateKey();
using var rsa = myCert.GetRSAPrivateKey();
var res = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found"); var res = rsa?.SignData(data, 0, data.Length, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No private key found");
return res; return res;
} }
@ -86,16 +86,12 @@ namespace QRBee.Core.Security
return serNo; return serNo;
} }
private X509Certificate2 LoadPrivateKey() private RSA LoadPrivateKey()
{ {
if (!PrivateKeyHandler.Exists()) if (!PrivateKeyHandler.Exists())
PrivateKeyHandler.GeneratePrivateKey(); //TODO: subject name PrivateKeyHandler.GeneratePrivateKey(); //TODO: subject name
var pk = PrivateKeyHandler.LoadPrivateKey(); var pk = PrivateKeyHandler.LoadPrivateKey();
if (!IsValid(pk) )
{
throw new CryptographicException("CA private key is not valid");
}
return pk; return pk;
} }

View File

@ -81,7 +81,7 @@ namespace QRBee.Droid.Services
/// <inheritdoc/> /// <inheritdoc/>
public ReadableCertificateRequest CreateCertificateRequest(string subjectName) public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
{ {
using var rsa = LoadRsaPrivateKey(); using var rsa = LoadPrivateKey();
var request = new ReadableCertificateRequest var request = new ReadableCertificateRequest
{ {
@ -96,85 +96,68 @@ namespace QRBee.Droid.Services
return request; return request;
} }
/// <summary> ///// <summary>
/// Generate EXPORTABLE certificate ///// Generate EXPORTABLE certificate
/// </summary> ///// </summary>
/// <param name="subjectName"></param> ///// <param name="subjectName"></param>
/// <returns></returns> ///// <returns></returns>
private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName) //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;
//}
///// <summary>
///// Generate CA certificate request (i.e. with KeyCertSign usage extension)
///// </summary>
///// <param name="distinguishedName"></param>
///// <param name="rsa"></param>
///// <returns></returns>
//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 cert = new X509Certificate2(PrivateKeyFileName);
var distinguishedName = new X500DistinguishedName($"CN={subjectName}"); return cert;
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;
}
/// <summary>
/// Generate CA certificate request (i.e. with KeyCertSign usage extension)
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="rsa"></param>
/// <returns></returns>
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;
}
/// <inheritdoc/>
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;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
public void AttachCertificate(X509Certificate2 cert) public void AttachCertificate(X509Certificate2 cert)
{ {
// heavily modified version of: var bytes = cert.Export(X509ContentType.Cert);
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
using var rsa = LoadRsaPrivateKey();
var newPk = cert.CopyWithPrivateKey(rsa); File.WriteAllBytes(PrivateKeyFileName, bytes);
var pkcs12data = newPk.Export(X509ContentType.Pfx, VeryBadNeverUsePrivateKeyPassword);
File.WriteAllBytes(PrivateKeyFileName, pkcs12data);
lock ( _syncObject ) lock ( _syncObject )
{ {
@ -184,7 +167,7 @@ namespace QRBee.Droid.Services
} }
} }
private RSA LoadRsaPrivateKey() public RSA LoadPrivateKey()
{ {
var bytes = File.ReadAllBytes(PrivateRsaKeyFileName); var bytes = File.ReadAllBytes(PrivateRsaKeyFileName);
var s = CryptoHelper.DecryptStringAES(bytes, VeryBadNeverUsePrivateKeyPassword); var s = CryptoHelper.DecryptStringAES(bytes, VeryBadNeverUsePrivateKeyPassword);

View File

@ -1,12 +1,15 @@
using System; using System;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.IO;
using QRBee.Core.Security; using QRBee.Core.Security;
namespace QRBee.Droid.Services namespace QRBee.Droid.Services
{ {
internal class AndroidSecurityService : SecurityServiceBase internal class AndroidSecurityService : SecurityServiceBase
{ {
private X509Certificate2 _apiServerCertificate;
private string ApiServerCertificateFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/ApiServerCertificate.bin";
public AndroidSecurityService(IPrivateKeyHandler privateKeyHandler) public AndroidSecurityService(IPrivateKeyHandler privateKeyHandler)
: base(privateKeyHandler) : base(privateKeyHandler)
@ -51,6 +54,29 @@ namespace QRBee.Droid.Services
return builder.ToString(); 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);
}
}
} }
} }

View File

@ -7,7 +7,7 @@ namespace QRBee.Services
{ {
public class Settings public class Settings
{ {
//TODO add ClientId
public string ClientId { get; set; } public string ClientId { get; set; }
public string PIN { get; set; } public string PIN { get; set; }
public bool IsRegistered => !string.IsNullOrWhiteSpace(ClientId); public bool IsRegistered => !string.IsNullOrWhiteSpace(ClientId);
@ -20,7 +20,7 @@ namespace QRBee.Services
public string ExpirationDate { get; set; } public string ExpirationDate { get; set; }
public string CardHolderName { get; set; } public string CardHolderName { get; set; }
public string CVC { get; set; } public string CVC { get; set; }
public string IssueNo { get; set; } public int? IssueNo { get; set; }
public string Password { get; set; } public string Password { get; set; }
} }

View File

@ -12,6 +12,7 @@ namespace QRBee.ViewModels
{ {
private readonly IQRScanner _scanner; private readonly IQRScanner _scanner;
private readonly ISecurityService _securityService; private readonly ISecurityService _securityService;
private readonly ILocalSettings _localSettings;
public bool _isAcceptDenyButtonVisible; public bool _isAcceptDenyButtonVisible;
public bool _isQrVisible; public bool _isQrVisible;
public bool _isScanButtonVisible; public bool _isScanButtonVisible;
@ -21,10 +22,11 @@ namespace QRBee.ViewModels
private string _qrCode; private string _qrCode;
private MerchantToClientRequest _merchantToClientRequest; private MerchantToClientRequest _merchantToClientRequest;
public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService) public ClientPageViewModel(IQRScanner scanner, ISecurityService securityService, ILocalSettings localSettings)
{ {
_scanner = scanner; _scanner = scanner;
_securityService = securityService; _securityService = securityService;
_localSettings = localSettings;
ScanCommand = new Command(OnScanButtonClicked); ScanCommand = new Command(OnScanButtonClicked);
AcceptQrCommand = new Command(OnAcceptQrCommand); AcceptQrCommand = new Command(OnAcceptQrCommand);
DenyQrCommand = new Command(OnDenyQrCommand); 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"); var answer = await Application.Current.MainPage.DisplayAlert("Confirmation", "Would you like to accept the offer?", "Yes", "No");
if (!answer) return; if (!answer) return;
var settings = _localSettings.LoadSettings();
var response = new ClientToMerchantResponse var response = new ClientToMerchantResponse
{ {
//TODO get client id from database ClientId = settings.ClientId,
ClientId = Guid.NewGuid().ToString("D"),
TimeStampUTC = DateTime.UtcNow, 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())); var clientSignature = _securityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature()));
response.ClientSignature = Convert.ToBase64String(clientSignature); response.ClientSignature = Convert.ToBase64String(clientSignature);
@ -168,13 +168,30 @@ namespace QRBee.ViewModels
IsScanButtonVisible = true; 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) public void OnDenyQrCommand(object obj)
{ {
QrCode = null; QrCode = null;
IsQrVisible = false; IsQrVisible = false;
IsAcceptDenyButtonVisible = false; IsAcceptDenyButtonVisible = false;
IsScanButtonVisible = true; IsScanButtonVisible = true;
Amount = ""; Amount = "";
} }
} }

View File

@ -131,8 +131,7 @@ namespace QRBee.ViewModels
{ {
var trans = new MerchantToClientRequest var trans = new MerchantToClientRequest
{ {
//TODO get merchant id from database MerchantId = _settings.LoadSettings().ClientId,
MerchantId = Guid.NewGuid().ToString("D"),
MerchantTransactionId = Guid.NewGuid().ToString("D"), MerchantTransactionId = Guid.NewGuid().ToString("D"),
Name = Name, Name = Name,
Amount = Amount, Amount = Amount,

View File

@ -50,7 +50,7 @@ namespace QRBee.ViewModels
public string ExpirationDate { get; set; } public string ExpirationDate { get; set; }
public string CardHolderName { get; set; } public string CardHolderName { get; set; }
public string CVC { get; set; } public string CVC { get; set; }
public string IssueNo { get; set; } public int? IssueNo { get; set; }
public Color Password1Color { get; set; } public Color Password1Color { get; set; }
public Color Password2Color { get; set;} public Color Password2Color { get; set;}
@ -125,7 +125,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);
} }
@ -149,6 +149,7 @@ namespace QRBee.ViewModels
try try
{ {
//TODO Register if not, otherwise update
// FOR TESTING PURPOSES // FOR TESTING PURPOSES
//!settings.IsRegistered //!settings.IsRegistered
if (true) if (true)
@ -158,10 +159,14 @@ namespace QRBee.ViewModels
// Save ClientId to LocalSettings // Save ClientId to LocalSettings
settings = _settings.LoadSettings(); settings = _settings.LoadSettings();
settings.ClientId = response.ClientId; settings.ClientId = response.ClientId;
// Save server public key certificate
_securityService.APIServerCertificate = _securityService.Deserialize(response.APIServerCertificate);
await _settings.SaveSettings(settings); await _settings.SaveSettings(settings);
// Attach certificate to privateKey (replace self-sighed with server issued certificate) // 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(); 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

@ -62,7 +62,8 @@ namespace QRBee.Api.Services
return new RegistrationResponse return new RegistrationResponse
{ {
ClientId = clientId, ClientId = clientId,
Certificate = _securityService.Serialize(clientCertificate) ClientCertificate = _securityService.Serialize(clientCertificate),
APIServerCertificate = _securityService.Serialize(_securityService.APIServerCertificate)
}; };
} }

View File

@ -29,7 +29,10 @@ namespace QRBee.Api.Services
var req = CreateClientCertRequest(distinguishedName, rsa); var req = CreateClientCertRequest(distinguishedName, rsa);
var pk = PrivateKeyHandler.LoadPrivateKey(); 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(1),
DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays), DateTimeOffset.UtcNow + TimeSpan.FromDays(CertificateValidityPeriodDays),
Guid.NewGuid() Guid.NewGuid()
@ -92,6 +95,13 @@ namespace QRBee.Api.Services
return builder.ToString(); return builder.ToString();
} }
/// <inheritdoc/>
public override X509Certificate2 APIServerCertificate
{
get => PrivateKeyHandler.GetCertificate();
set => throw new ApplicationException("Do not call this");
}
} }
} }

View File

@ -55,7 +55,7 @@ namespace QRBee.Api.Services
public ReadableCertificateRequest CreateCertificateRequest(string subjectName) 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. //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(); var rsa = pk.GetRSAPrivateKey();
if (rsa == null) if (rsa == null)
@ -172,7 +172,7 @@ namespace QRBee.Api.Services
/// <inheritdoc/> /// <inheritdoc/>
public X509Certificate2 LoadPrivateKey() private X509Certificate2 Load()
{ {
if (_certificate != null) if (_certificate != null)
return _certificate; 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;
}
/// <inheritdoc/> /// <inheritdoc/>
public void AttachCertificate(X509Certificate2 cert) public void AttachCertificate(X509Certificate2 cert)
{ {