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
|
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="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Services\LocalSettings.cs" />
|
<Compile Include="Services\LocalSettings.cs" />
|
||||||
<Compile Include="Services\QRScannerService.cs" />
|
<Compile Include="Services\QRScannerService.cs" />
|
||||||
|
<Compile Include="Services\SecurityService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Resources\AboutResources.txt" />
|
<None Include="Resources\AboutResources.txt" />
|
||||||
@ -106,6 +107,10 @@
|
|||||||
<AndroidResource Include="Resources\drawable\icon_feed.png" />
|
<AndroidResource Include="Resources\drawable\icon_feed.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<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">
|
<ProjectReference Include="..\QRBee\QRBee.csproj">
|
||||||
<Project>{C651AD58-D087-4261-8C8E-EBA6140F3E72}</Project>
|
<Project>{C651AD58-D087-4261-8C8E-EBA6140F3E72}</Project>
|
||||||
<Name>QRBee</Name>
|
<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