diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index 7f79108abe..e96c1f530c 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -11,8 +11,11 @@ namespace Bit.Core.Models.Business DateTime? Refresh { get; set; } DateTime? Expires { get; set; } bool Trial { get; set; } + string Hash { get; set; } string Signature { get; set; } - byte[] GetSignatureData(); + byte[] SignatureBytes { get; } + byte[] GetDataBytes(bool forHash = false); + byte[] ComputeHash(); bool VerifySignature(X509Certificate2 certificate); byte[] Sign(X509Certificate2 certificate); } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 9de23bb164..da1f5e41fb 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -71,6 +71,7 @@ namespace Bit.Core.Models.Business Trial = false; } + Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); } @@ -95,18 +96,29 @@ namespace Bit.Core.Models.Business public DateTime? Refresh { get; set; } public DateTime? Expires { get; set; } public bool Trial { get; set; } + public string Hash { get; set; } public string Signature { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); - public byte[] GetSignatureData() + public byte[] GetDataBytes(bool forHash = false) { string data = null; if(Version == 1) { var props = typeof(OrganizationLicense) .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes))) + .Where(p => + !p.Name.Equals(nameof(Signature)) && + !p.Name.Equals(nameof(SignatureBytes)) && + ( + !forHash || + ( + !p.Name.Equals(nameof(Hash)) && + !p.Name.Equals(nameof(Issued)) && + !p.Name.Equals(nameof(Refresh)) + ) + )) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); @@ -120,6 +132,14 @@ namespace Bit.Core.Models.Business return Encoding.UTF8.GetBytes(data); } + public byte[] ComputeHash() + { + using(var alg = SHA256.Create()) + { + return alg.ComputeHash(GetDataBytes(true)); + } + } + public bool CanUse(Guid installationId) { if(!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) @@ -167,7 +187,7 @@ namespace Bit.Core.Models.Business { using(var rsa = certificate.GetRSAPublicKey()) { - return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.VerifyData(GetDataBytes(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } @@ -180,7 +200,7 @@ namespace Bit.Core.Models.Business using(var rsa = certificate.GetRSAPrivateKey()) { - return rsa.SignData(GetSignatureData(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.SignData(GetDataBytes(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } } diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 957bf34c17..f645dd1338 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -29,6 +29,8 @@ namespace Bit.Core.Models.Business Refresh = billingInfo?.UpcomingInvoice?.Date; Trial = (billingInfo?.Subscription?.TrialEndDate.HasValue ?? false) && billingInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; + + Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); } @@ -43,18 +45,29 @@ namespace Bit.Core.Models.Business public DateTime? Refresh { get; set; } public DateTime? Expires { get; set; } public bool Trial { get; set; } + public string Hash { get; set; } public string Signature { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); - public byte[] GetSignatureData() + public byte[] GetDataBytes(bool forHash = false) { string data = null; if(Version == 1) { var props = typeof(UserLicense) .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes))) + .Where(p => + !p.Name.Equals(nameof(Signature)) && + !p.Name.Equals(nameof(SignatureBytes)) && + ( + !forHash || + ( + !p.Name.Equals(nameof(Hash)) && + !p.Name.Equals(nameof(Issued)) && + !p.Name.Equals(nameof(Refresh)) + ) + )) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); @@ -68,6 +81,14 @@ namespace Bit.Core.Models.Business return Encoding.UTF8.GetBytes(data); } + public byte[] ComputeHash() + { + using(var alg = SHA256.Create()) + { + return alg.ComputeHash(GetDataBytes(true)); + } + } + public bool CanUse(User user) { if(Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) @@ -109,7 +130,7 @@ namespace Bit.Core.Models.Business { using(var rsa = certificate.GetRSAPublicKey()) { - return rsa.VerifyData(GetSignatureData(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.VerifyData(GetDataBytes(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } @@ -122,7 +143,7 @@ namespace Bit.Core.Models.Business using(var rsa = certificate.GetRSAPrivateKey()) { - return rsa.SignData(GetSignatureData(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return rsa.SignData(GetDataBytes(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 9e9ab78d25..c100b6c885 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -152,7 +152,7 @@ namespace Bit.Core.Services if(!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats) { var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id); - if(userCount >= newPlanSeats) + if(userCount > newPlanSeats) { throw new BadRequestException($"Your organization currently has {userCount} seats filled. Your new plan " + $"only has ({newPlanSeats}) seats. Remove some users."); @@ -651,7 +651,7 @@ namespace Bit.Core.Services if(license.Seats.HasValue && (!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value)) { var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id); - if(userCount >= license.Seats.Value) + if(userCount > license.Seats.Value) { throw new BadRequestException($"Your organization currently has {userCount} seats filled. " + $"Your new license only has ({ license.Seats.Value}) seats. Remove some users.");