Certificate request implemented.

This commit is contained in:
Andrey Shabarshov 2022-02-22 17:58:43 +00:00
parent e07b45d43f
commit 8d5702b621
8 changed files with 303 additions and 39 deletions

View File

@ -6,16 +6,11 @@ namespace QRBee.Core.Security
{
public class ReadableCertificateRequest
{
private byte[] _rsaPublicKey;
private byte[] _signature;
public string SubjectName { get; set; }
public string RsaPublicKey
{
get => _rsaPublicKey != null ? Convert.ToBase64String(_rsaPublicKey): null;
set => _rsaPublicKey = value != null ? Convert.FromBase64String(value) : null;
}
public StringRSAParameters RsaPublicKey { get; set; }
public string Signature
{
@ -23,7 +18,7 @@ namespace QRBee.Core.Security
set => _signature = value != null ? Convert.FromBase64String(value) : null;
}
public string AsDataForSignature() => $"{SubjectName}|{RsaPublicKey}";
public string AsDataForSignature() => $"{SubjectName}|{RsaPublicKey.ConvertToJson()}";
}
}

View File

@ -0,0 +1,27 @@
using Newtonsoft.Json;
namespace QRBee.Core.Security
{
public class StringRSAParameters
{
public string StringExponent { get; set; }
public string StringModulus { get; set; }
public string StringP { get; set; }
public string StringQ { get; set; }
public string StringDP { get; set; }
public string StringDQ { get; set; }
public string StringInverseQ { get; set; }
public string StringD { get; set; }
public string ConvertToJson() => JsonConvert.SerializeObject(this);
public static StringRSAParameters ConvertFromJson(string json) => JsonConvert.DeserializeObject<StringRSAParameters>(json);
}
}

View File

@ -82,6 +82,7 @@
<Compile Include="MainActivity.cs" />
<Compile Include="Resources\Resource.designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\CryptoHelper.cs" />
<Compile Include="Services\LocalSettings.cs" />
<Compile Include="Services\AndroidPrivateKeyHandler.cs" />
<Compile Include="Services\QRScannerService.cs" />

View File

@ -24,8 +24,8 @@ namespace QRBee.Droid.Services
private const int RSABits = 2048;
private const int CertificateValidityDays = 3650;
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{SignedCertificateFileName}";
private string PrivateRsaKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{RawRsaKeyFileName}";
private string PrivateKeyFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/{SignedCertificateFileName}";
private string PrivateRsaKeyFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/{RawRsaKeyFileName}";
/// <inheritdoc/>
public bool Exists()
@ -41,26 +41,51 @@ namespace QRBee.Droid.Services
File.Delete(PrivateRsaKeyFileName);
using var rsa = RSA.Create(RSABits);
var bytes = rsa.ExportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, new PbeParameters(PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, EncryptionIterationCount));
var s = ExportKeyToJson(rsa,true);
var bytes = CryptoHelper.EncryptStringAES(s, VeryBadNeverUsePrivateKeyPassword);
File.WriteAllBytes(PrivateRsaKeyFileName, bytes);
}
return CreateCertificateRequest(subjectName);
}
private static string ExportKeyToJson(RSA rsa,bool includePrivateKey)
{
//Workaround for absence of half of cryptography subsystem in Mono
var stringParameters = ExportKey(rsa, includePrivateKey);
var s = stringParameters.ConvertToJson();
return s;
}
private static StringRSAParameters ExportKey(RSA rsa, bool includePrivateKey)
{
var rsaParameters = rsa.ExportParameters(includePrivateKey);
var stringParameters = new StringRSAParameters
{
StringExponent = SafeConvertToBase64(rsaParameters.Exponent),
StringModulus = SafeConvertToBase64(rsaParameters.Modulus),
StringP = SafeConvertToBase64(rsaParameters.P),
StringQ = SafeConvertToBase64(rsaParameters.Q),
StringDP = SafeConvertToBase64(rsaParameters.DP),
StringDQ = SafeConvertToBase64(rsaParameters.DQ),
StringInverseQ = SafeConvertToBase64(rsaParameters.InverseQ),
StringD = SafeConvertToBase64(rsaParameters.D)
};
return stringParameters;
}
private static string SafeConvertToBase64(byte[] bytes) => bytes == null ? "" : Convert.ToBase64String(bytes);
/// <inheritdoc/>
public ReadableCertificateRequest CreateCertificateRequest(string subjectName)
{
if (File.Exists(PrivateRsaKeyFileName))
throw new ApplicationException("Private key does not exist");
var bytes = File.ReadAllBytes(PrivateRsaKeyFileName);
using var rsa = RSA.Create(RSABits);
rsa.ImportEncryptedPkcs8PrivateKey(VeryBadNeverUsePrivateKeyPassword, bytes, out _);
using var rsa = LoadRsaPrivateKey();
var request = new ReadableCertificateRequest
{
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
RsaPublicKey = ExportKey(rsa,false),
SubjectName = subjectName
};
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -144,12 +169,7 @@ namespace QRBee.Droid.Services
{
// heavily modified version of:
// https://stackoverflow.com/questions/18462064/associate-a-private-key-with-the-x509certificate2-class-in-net
// we can't use LoadPrivateKey here as it creating non-exportable key
var pk = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUsePrivateKeyPassword, X509KeyStorageFlags.Exportable);
using var rsa = pk.GetRSAPrivateKey();
if (rsa == null)
throw new CryptographicException("Can't get PrivateKey");
using var rsa = LoadRsaPrivateKey();
var newPk = cert.CopyWithPrivateKey(rsa);
@ -164,5 +184,30 @@ namespace QRBee.Droid.Services
}
}
private RSA LoadRsaPrivateKey()
{
var bytes = File.ReadAllBytes(PrivateRsaKeyFileName);
var s = CryptoHelper.DecryptStringAES(bytes, VeryBadNeverUsePrivateKeyPassword);
var stringParameters = StringRSAParameters.ConvertFromJson(s);
var rsaParameters = new RSAParameters
{
Exponent = SafeConvertFromBase64(stringParameters.StringExponent),
Modulus = SafeConvertFromBase64(stringParameters.StringModulus),
P = SafeConvertFromBase64(stringParameters.StringP),
Q = SafeConvertFromBase64(stringParameters.StringQ),
DP = SafeConvertFromBase64(stringParameters.StringDP),
DQ = SafeConvertFromBase64(stringParameters.StringDQ),
InverseQ = SafeConvertFromBase64(stringParameters.StringInverseQ),
D = SafeConvertFromBase64(stringParameters.StringD)
};
var rsa = RSA.Create(rsaParameters);
if (rsa == null)
throw new CryptographicException("Can't get PrivateKey");
return rsa;
}
private static byte[] SafeConvertFromBase64(string s) => string.IsNullOrWhiteSpace(s) ? null : Convert.FromBase64String(s);
}
}

View File

@ -0,0 +1,136 @@
using System;
using System.IO;
using System.Security.Cryptography;
using Stream = System.IO.Stream;
namespace QRBee.Droid.Services
{
internal class CryptoHelper
{
//https://stackoverflow.com/questions/202011/encrypt-and-decrypt-a-string-in-c
//While an app specific salt is not the best practice for
//password based encryption, it's probably safe enough as long as
//it is truly uncommon. Also too much work to alter this answer otherwise.
// Never use salt like this
private static byte[] _salt = System.Text.Encoding.UTF8.GetBytes("wÑÏU4)MÓvcvsª");
/// <summary>
/// Encrypt the given string using AES. The string can be decrypted using
/// DecryptStringAES(). The sharedSecret parameters must match.
/// </summary>
/// <param name="plainText">The text to encrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for encryption.</param>
public static byte[] EncryptStringAES(string plainText, string sharedSecret)
{
if (string.IsNullOrEmpty(plainText))
throw new ArgumentNullException(nameof(plainText));
if (string.IsNullOrEmpty(sharedSecret))
throw new ArgumentNullException(nameof(sharedSecret));
RijndaelManaged aesAlg = null; // RijndaelManaged object used to encrypt the data.
try
{
// generate the key from the shared secret and the salt
var key = new Rfc2898DeriveBytes(sharedSecret, _salt);
// Create a RijndaelManaged object
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Create a decryptor to perform the stream transform.
var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
using MemoryStream msEncrypt = new MemoryStream();
// prepend the IV
msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof(int));
msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (var swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(plainText);
}
}
return msEncrypt.ToArray();
}
finally
{
// Clear the RijndaelManaged object.
aesAlg?.Clear();
}
}
/// <summary>
/// Decrypt the given string. Assumes the string was encrypted using
/// EncryptStringAES(), using an identical sharedSecret.
/// </summary>
/// <param name="cipherText">The text to decrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for decryption.</param>
public static string DecryptStringAES(byte[] bytes, string sharedSecret)
{
if (string.IsNullOrEmpty(sharedSecret))
throw new ArgumentNullException(nameof(sharedSecret));
// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;
// Declare the string used to hold
// the decrypted text.
string plaintext = null;
try
{
// generate the key from the shared secret and the salt
var key = new Rfc2898DeriveBytes(sharedSecret, _salt);
// Create the streams used for decryption.
using var msDecrypt = new MemoryStream(bytes);
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Get the initialization vector from the encrypted stream
aesAlg.IV = ReadByteArray(msDecrypt);
// Create a decrytor to perform the stream transform.
var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
using var srDecrypt = new StreamReader(csDecrypt);
plaintext = srDecrypt.ReadToEnd();
}
finally
{
// Clear the RijndaelManaged object.
aesAlg?.Clear();
}
return plaintext;
}
private static byte[] ReadByteArray(Stream s)
{
byte[] rawLength = new byte[sizeof(int)];
if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
{
throw new SystemException("Stream did not contain properly formatted byte array");
}
byte[] buffer = new byte[BitConverter.ToInt32(rawLength, 0)];
if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
{
throw new SystemException("Did not read byte array properly");
}
return buffer;
}
}
}

View File

@ -103,11 +103,13 @@ namespace QRBee.ViewModels
var client = new HttpClient(GetInsecureHandler());
var service = new Core.Client.Client(_settings.QRBeeApiUrl,client);
Settings settings;
RegistrationRequest request;
try
{
//TODO Check if ClientId already in LocalSettings. If Yes update data in database
var settings = _settings.LoadSettings();
settings = _settings.LoadSettings();
//save local settings
settings.CardHolderName = CardHolderName;
@ -128,7 +130,7 @@ namespace QRBee.ViewModels
_privateKeyHandler.GeneratePrivateKey(settings.Name);
}
var request = new RegistrationRequest
request = new RegistrationRequest
{
DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"),
Email = Email,
@ -136,7 +138,17 @@ namespace QRBee.ViewModels
CertificateRequest = _privateKeyHandler.CreateCertificateRequest(Email),
RegisterAsMerchant = false
};
}
catch (Exception e)
{
//TODO: delete exception message in error message
var page = Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
await page.DisplayAlert("Error", $"The ClientSide isn't working: {e.Message}", "Ok");
return;
}
try
{
if (!settings.IsRegistered)
{
var response = await service.RegisterAsync(request);
@ -159,8 +171,6 @@ namespace QRBee.ViewModels
await page.DisplayAlert("Success", "Your data has been updated successfully", "Ok");
}
await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}
catch (Exception e)

View File

@ -1,4 +1,5 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
@ -49,7 +50,7 @@ namespace QRBee.Api.Services
var info = Convert(request);
var clientId = await _storage.PutUserInfo(info);
var clientCertificate = _securityService.CreateCertificate(clientId,System.Convert.FromBase64String(request.CertificateRequest.RsaPublicKey));
var clientCertificate = _securityService.CreateCertificate(clientId,Encoding.UTF8.GetBytes(request.CertificateRequest.RsaPublicKey.ConvertToJson()));
var convertedClientCertificate = Convert(clientCertificate, clientId);
await _storage.InsertCertificate(convertedClientCertificate);
@ -107,16 +108,45 @@ namespace QRBee.Api.Services
}
//Check digital signature
var verified = _securityService.Verify(
Encoding.UTF8.GetBytes(certificateRequest.AsDataForSignature()),
Encoding.UTF8.GetBytes(certificateRequest.Signature),
_privateKeyHandler.LoadPrivateKey());
using var rsa = LoadRsaPublicKey(certificateRequest.RsaPublicKey);
var data = Encoding.UTF8.GetBytes(certificateRequest.AsDataForSignature());
var signature = System.Convert.FromBase64String(certificateRequest.Signature);
var verified = rsa.VerifyData(
data,
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1
);
if (!verified)
{
throw new ApplicationException($"Digital signature is not valid.");
}
}
private static RSA LoadRsaPublicKey(StringRSAParameters stringParameters)
{
var rsaParameters = new RSAParameters
{
Exponent = SafeConvertFromBase64(stringParameters.StringExponent),
Modulus = SafeConvertFromBase64(stringParameters.StringModulus),
P = SafeConvertFromBase64(stringParameters.StringP),
Q = SafeConvertFromBase64(stringParameters.StringQ),
DP = SafeConvertFromBase64(stringParameters.StringDP),
DQ = SafeConvertFromBase64(stringParameters.StringDQ),
InverseQ = SafeConvertFromBase64(stringParameters.StringInverseQ),
D = SafeConvertFromBase64(stringParameters.StringD)
};
var rsa = RSA.Create(rsaParameters);
if (rsa == null)
throw new CryptographicException("Can't create public key");
return rsa;
}
private static byte[]? SafeConvertFromBase64(string? s) => string.IsNullOrWhiteSpace(s) ? null : System.Convert.FromBase64String(s);
private static UserInfo Convert(RegistrationRequest request)
{
return new UserInfo(request.Name, request.Email, request.DateOfBirth);

View File

@ -19,7 +19,7 @@ namespace QRBee.Api.Services
private const string VeryBadNeverUseCertificatePassword = "+ñèbòFëc׎ßRúß¿ãçPN";
private string PrivateKeyFileName => $"{System.Environment.SpecialFolder.LocalApplicationData}/{FileName}";
private string PrivateKeyFileName => $"{Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)}/{FileName}";
/// <inheritdoc/>
public bool Exists()
@ -56,7 +56,7 @@ namespace QRBee.Api.Services
var request = new ReadableCertificateRequest
{
RsaPublicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey()),
RsaPublicKey = ExportKey(rsa,false),
SubjectName = subjectName
};
var data = Encoding.UTF8.GetBytes(request.AsDataForSignature());
@ -67,6 +67,26 @@ namespace QRBee.Api.Services
return request;
}
private static StringRSAParameters ExportKey(RSA rsa, bool includePrivateKey)
{
var rsaParameters = rsa.ExportParameters(includePrivateKey);
var stringParameters = new StringRSAParameters
{
StringExponent = SafeConvertToBase64(rsaParameters.Exponent),
StringModulus = SafeConvertToBase64(rsaParameters.Modulus),
StringP = SafeConvertToBase64(rsaParameters.P),
StringQ = SafeConvertToBase64(rsaParameters.Q),
StringDP = SafeConvertToBase64(rsaParameters.DP),
StringDQ = SafeConvertToBase64(rsaParameters.DQ),
StringInverseQ = SafeConvertToBase64(rsaParameters.InverseQ),
StringD = SafeConvertToBase64(rsaParameters.D)
};
return stringParameters;
}
private static string SafeConvertToBase64(byte[]? bytes) => bytes == null ? "" : Convert.ToBase64String(bytes);
//private static string AsCsr(CertificateRequest request)
//{
// // https://stackoverflow.com/questions/65943968/how-to-convert-a-csr-text-file-into-net-core-standard-certificaterequest-for-s
@ -149,7 +169,7 @@ namespace QRBee.Api.Services
return _certificate;
if (!Exists())
throw new CryptographicException("PrivateKey does not exist");
GeneratePrivateKey("QRBeeCA");
_certificate = new X509Certificate2(PrivateKeyFileName, VeryBadNeverUseCertificatePassword);
return _certificate;