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 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; }
/// <summary>
/// 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.
/// </summary>
/// <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;
}
public string Certificate
public string ClientCertificate
{
get;
set;
}
public string APIServerCertificate
{
get;
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
{
@ -38,6 +39,12 @@ namespace QRBee.Core.Security
/// self-signed until CA issues a proper certificate
/// </summary>
/// <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 --------------------------
/// <summary>
/// API Server certificate
/// </summary>
X509Certificate2 APIServerCertificate { get; set; }
/// <summary>
/// Convert binary block to X509Certificate2.
/// <see cref="X509Certificate2.CreateFromPem"/>

View File

@ -39,12 +39,13 @@ namespace QRBee.Core.Security
/// <inheritdoc/>
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; }
/// <inheritdoc/>
public byte [] Encrypt(byte[] data, X509Certificate2 destCert)
{
@ -63,8 +64,7 @@ namespace QRBee.Core.Security
/// <inheritdoc/>
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;
}

View File

@ -81,7 +81,7 @@ namespace QRBee.Droid.Services
/// <inheritdoc/>
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;
}
/// <summary>
/// Generate EXPORTABLE certificate
/// </summary>
/// <param name="subjectName"></param>
/// <returns></returns>
private X509Certificate2 CreateSelfSignedClientCertificate(string subjectName)
///// <summary>
///// Generate EXPORTABLE certificate
///// </summary>
///// <param name="subjectName"></param>
///// <returns></returns>
//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 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;
}
/// <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;
}
var cert = new X509Certificate2(PrivateKeyFileName);
return cert;
}
/// <inheritdoc/>
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);

View File

@ -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);
}
}
}
}

View File

@ -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; }
}

View File

@ -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 = "";
}
}

View File

@ -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,

View File

@ -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");

View File

@ -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)
};
}

View File

@ -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();
}
/// <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)
{
//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
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public void AttachCertificate(X509Certificate2 cert)
{