mirror of
https://github.com/NecroticBamboo/QRBee.git
synced 2025-12-21 12:11:53 +00:00
Started working on Security service.
This commit is contained in:
parent
1ae205298c
commit
bde2f53f59
@ -1,4 +1,4 @@
|
||||
namespace QRBee.Core
|
||||
namespace QRBee.Core.Data
|
||||
{
|
||||
public record RegistrationRequest
|
||||
{
|
||||
|
||||
43
QRBee.Core/Security/IPrivateKeyHandler.cs
Normal file
43
QRBee.Core/Security/IPrivateKeyHandler.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace QRBee.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key manipulation methods
|
||||
/// </summary>
|
||||
public interface IPrivateKeyHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if private key exists on this machine
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool Exists();
|
||||
|
||||
/// <summary>
|
||||
/// Generate new private key and store it
|
||||
/// </summary>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <returns>Certificate request to be sent to CA</returns>
|
||||
byte [] GeneratePrivateKey(string? subjectName = null);
|
||||
|
||||
/// <summary>
|
||||
/// Re-create certificate request if CA response was not received in time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
byte[] CreateCertificateRequest();
|
||||
|
||||
/// <summary>
|
||||
/// Attach CA-generated public key certificate to the private key
|
||||
/// and store it
|
||||
/// </summary>
|
||||
/// <param name="cert"></param>
|
||||
void AttachCertificate(X509Certificate2 cert);
|
||||
|
||||
/// <summary>
|
||||
/// Load private key. Note that public key certificate part can be
|
||||
/// self-signed until CA issues a proper certificate
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
X509Certificate2 LoadPrivateKey();
|
||||
}
|
||||
}
|
||||
89
QRBee.Core/Security/ISecurityService.cs
Normal file
89
QRBee.Core/Security/ISecurityService.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace QRBee.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// All cryptographic primitives are here.
|
||||
/// </summary>
|
||||
public interface ISecurityService
|
||||
{
|
||||
// -------------------------- encryption --------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Sign block of data
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns>Signature</returns>
|
||||
/// <exception cref="CryptographicException"></exception>
|
||||
byte[] Sign(byte [] data);
|
||||
|
||||
/// <summary>
|
||||
/// Verify digital signature
|
||||
/// </summary>
|
||||
/// <param name="data">Source data</param>
|
||||
/// <param name="signature">Signature to check</param>
|
||||
/// <param name="signedBy">Public key certificate to use</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="CryptographicException"></exception>
|
||||
bool Verify(byte [] data, byte [] signature, X509Certificate2 signedBy);
|
||||
|
||||
/// <summary>
|
||||
/// Encrypt data for the selected client identified by X.509 certificate.
|
||||
/// </summary>
|
||||
/// <param name="data">Clear data to encrypt</param>
|
||||
/// <param name="destCert">Certificate of the destination client</param>
|
||||
/// <returns>Encrypted data</returns>
|
||||
/// <exception cref="CryptographicException"></exception>
|
||||
byte[] Encrypt(byte[] data, X509Certificate2 destCert);
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt data encrypted for this service
|
||||
/// </summary>
|
||||
/// <param name="data">Binary encrypted data</param>
|
||||
/// <returns>Decrypted data</returns>
|
||||
/// <exception cref="CryptographicException"></exception>
|
||||
byte[] Decrypt(byte[] data);
|
||||
|
||||
|
||||
// -------------------------- certificate services --------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Convert binary block to X509Certificate2.
|
||||
/// <see cref="X509Certificate2.CreateFromPem"/>
|
||||
/// </summary>
|
||||
/// <param name="pemData">PEM-encoded certificate</param>
|
||||
/// <returns></returns>
|
||||
X509Certificate2 Deserialize(string pemData);
|
||||
|
||||
/// <summary>
|
||||
/// Convert certificate to PEM-encoded string.
|
||||
/// </summary>
|
||||
/// <param name="cert"></param>
|
||||
/// <returns></returns>
|
||||
string Serialize(X509Certificate2 cert);
|
||||
|
||||
/// <summary>
|
||||
/// Get certificate serial number.
|
||||
/// </summary>
|
||||
/// <param name="cert"></param>
|
||||
/// <returns></returns>
|
||||
string GetSerialNumber(X509Certificate2 cert);
|
||||
|
||||
/// <summary>
|
||||
/// Check if certificate is valid for this particular service.
|
||||
/// Note that such certificates will (and should) fail normal cert chain check.
|
||||
/// Valid certificates issued by different authority will fail the test.
|
||||
/// </summary>
|
||||
/// <param name="destCert">Certificate to check</param>
|
||||
/// <returns>True is certificate is valid for this service use</returns>
|
||||
bool IsValid(X509Certificate2 destCert);
|
||||
|
||||
/// <summary>
|
||||
/// Issue client certificate
|
||||
/// </summary>
|
||||
/// <param name="subjectName">Client name (goes to CN=)</param>
|
||||
/// <param name="rsaPublicKey">Client's RSA public key</param>
|
||||
/// <returns>Certificate</returns>
|
||||
X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey);
|
||||
}
|
||||
}
|
||||
104
QRBee.Core/Security/SecurityServiceBase.cs
Normal file
104
QRBee.Core/Security/SecurityServiceBase.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace QRBee.Core.Security
|
||||
{
|
||||
|
||||
public abstract class SecurityServiceBase : ISecurityService
|
||||
{
|
||||
protected IPrivateKeyHandler PrivateKeyHandler { get; }
|
||||
|
||||
protected SecurityServiceBase(IPrivateKeyHandler privateKeyHandler)
|
||||
{
|
||||
PrivateKeyHandler = privateKeyHandler;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract X509Certificate2 CreateCertificate(string subjectName, byte[] rsaPublicKey);
|
||||
/// <inheritdoc/>
|
||||
public abstract X509Certificate2 Deserialize(string pemData);
|
||||
/// <inheritdoc/>
|
||||
public abstract string Serialize(X509Certificate2 cert);
|
||||
|
||||
/// <summary>
|
||||
/// Subject may only contain letters, numbers, -, . and spaces.
|
||||
/// Subject should not be an empty string.
|
||||
/// International letters are supported, but not tested.
|
||||
/// </summary>
|
||||
/// <param name="subjectName"></param>
|
||||
/// <returns>True for valid subject name</returns>
|
||||
protected static bool IsValidSubjectName(string subjectName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subjectName))
|
||||
return false;
|
||||
return Regex.IsMatch(@"[\w\s[0-9]\-\.]+", subjectName);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Decrypt(byte[] data)
|
||||
{
|
||||
using var myCert = LoadPrivateKey();
|
||||
using var rsa = myCert.GetRSAPrivateKey();
|
||||
var res = rsa?.Decrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No private key found");
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte [] Encrypt(byte[] data, X509Certificate2 destCert)
|
||||
{
|
||||
using var rsa = destCert.GetRSAPublicKey();
|
||||
var res = rsa?.Encrypt(data, RSAEncryptionPadding.Pkcs1) ?? throw new CryptographicException("No destination public key found");
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsValid(X509Certificate2 destCert)
|
||||
{
|
||||
// check if certificate issued by this service
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Sign(byte[] data)
|
||||
{
|
||||
using var myCert = 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");
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Verify(byte[] data, byte[] signature, X509Certificate2 signedBy)
|
||||
{
|
||||
if ( !IsValid(signedBy))
|
||||
throw new CryptographicException("Signer's certificate is not valid");
|
||||
using var rsa = signedBy.GetRSAPublicKey();
|
||||
var res = rsa?.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) ?? throw new CryptographicException("No signer public key found");
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSerialNumber(X509Certificate2 cert)
|
||||
{
|
||||
var serNo = cert.SerialNumber;
|
||||
return serNo;
|
||||
}
|
||||
|
||||
private X509Certificate2 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -80,6 +80,7 @@
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\LocalSettings.cs" />
|
||||
<Compile Include="Services\QRScannerService.cs" />
|
||||
<Compile Include="Services\SecurityService.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Resources\AboutResources.txt" />
|
||||
@ -106,6 +107,10 @@
|
||||
<AndroidResource Include="Resources\drawable\icon_feed.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\QRBee.Core\QRBee.Core.csproj">
|
||||
<Project>{7c461562-66ef-4894-8ad8-f27f0b94053f}</Project>
|
||||
<Name>QRBee.Core</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\QRBee\QRBee.csproj">
|
||||
<Project>{C651AD58-D087-4261-8C8E-EBA6140F3E72}</Project>
|
||||
<Name>QRBee</Name>
|
||||
|
||||
40
QRBee/QRBee.Android/Services/SecurityService.cs
Normal file
40
QRBee/QRBee.Android/Services/SecurityService.cs
Normal file
@ -0,0 +1,40 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
62
QRBeeApi/Services/SecurityService.cs
Normal file
62
QRBeeApi/Services/SecurityService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
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)
|
||||
{
|
||||
if (!IsValidSubjectName(subjectName))
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override X509Certificate2 Deserialize(string pemData)
|
||||
{
|
||||
return X509Certificate2.CreateFromPem(pemData);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string Serialize(X509Certificate2 cert)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user