diff --git a/QRBee.Core/Security/ReadableCertificateRequest.cs b/QRBee.Core/Security/ReadableCertificateRequest.cs index 2643fe5..d4f1cce 100644 --- a/QRBee.Core/Security/ReadableCertificateRequest.cs +++ b/QRBee.Core/Security/ReadableCertificateRequest.cs @@ -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()}"; } } diff --git a/QRBee.Core/Security/StringRSAParameters.cs b/QRBee.Core/Security/StringRSAParameters.cs new file mode 100644 index 0000000..3310e91 --- /dev/null +++ b/QRBee.Core/Security/StringRSAParameters.cs @@ -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(json); + } +} \ No newline at end of file diff --git a/QRBee/QRBee.Android/QRBee.Android.csproj b/QRBee/QRBee.Android/QRBee.Android.csproj index 4f615ef..36a310d 100644 --- a/QRBee/QRBee.Android/QRBee.Android.csproj +++ b/QRBee/QRBee.Android/QRBee.Android.csproj @@ -82,6 +82,7 @@ + diff --git a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs index 796cef3..f54f32e 100644 --- a/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs +++ b/QRBee/QRBee.Android/Services/AndroidPrivateKeyHandler.cs @@ -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}"; /// 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); + /// 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); } } diff --git a/QRBee/QRBee.Android/Services/CryptoHelper.cs b/QRBee/QRBee.Android/Services/CryptoHelper.cs new file mode 100644 index 0000000..6fa64a6 --- /dev/null +++ b/QRBee/QRBee.Android/Services/CryptoHelper.cs @@ -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ª"); + + /// + /// Encrypt the given string using AES. The string can be decrypted using + /// DecryptStringAES(). The sharedSecret parameters must match. + /// + /// The text to encrypt. + /// A password used to generate a key for encryption. + 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(); + } + } + + /// + /// Decrypt the given string. Assumes the string was encrypted using + /// EncryptStringAES(), using an identical sharedSecret. + /// + /// The text to decrypt. + /// A password used to generate a key for decryption. + 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; + } + + } +} \ No newline at end of file diff --git a/QRBee/QRBee/ViewModels/RegisterViewModel.cs b/QRBee/QRBee/ViewModels/RegisterViewModel.cs index d4050d4..cc8f8bc 100644 --- a/QRBee/QRBee/ViewModels/RegisterViewModel.cs +++ b/QRBee/QRBee/ViewModels/RegisterViewModel.cs @@ -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,15 +130,25 @@ namespace QRBee.ViewModels _privateKeyHandler.GeneratePrivateKey(settings.Name); } - var request = new RegistrationRequest + request = new RegistrationRequest { - DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"), - Email = Email, - Name = Name, + DateOfBirth = DateOfBirth.ToString("yyyy-MM-dd"), + Email = Email, + Name = Name, 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) diff --git a/QRBeeApi/Services/QRBeeAPIService.cs b/QRBeeApi/Services/QRBeeAPIService.cs index 0c7f098..4247379 100644 --- a/QRBeeApi/Services/QRBeeAPIService.cs +++ b/QRBeeApi/Services/QRBeeAPIService.cs @@ -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); diff --git a/QRBeeApi/Services/ServerPrivateKeyHandler.cs b/QRBeeApi/Services/ServerPrivateKeyHandler.cs index 1b50a1d..ce84429 100644 --- a/QRBeeApi/Services/ServerPrivateKeyHandler.cs +++ b/QRBeeApi/Services/ServerPrivateKeyHandler.cs @@ -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}"; /// 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;