using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; namespace Bit.Core.Models.Business; public class OrganizationLicense : ILicense { public OrganizationLicense() { } public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId, ILicensingService licenseService, int? version = null) { Version = version.GetValueOrDefault(CurrentLicenseFileVersion); // TODO: Remember to change the constant LicenseType = Enums.LicenseType.Organization; LicenseKey = org.LicenseKey; InstallationId = installationId; Id = org.Id; Name = org.Name; BillingEmail = org.BillingEmail; BusinessName = org.BusinessName; Enabled = org.Enabled; Plan = org.Plan; PlanType = org.PlanType; Seats = org.Seats; MaxCollections = org.MaxCollections; UsePolicies = org.UsePolicies; UseSso = org.UseSso; UseKeyConnector = org.UseKeyConnector; UseScim = org.UseScim; UseGroups = org.UseGroups; UseEvents = org.UseEvents; UseDirectory = org.UseDirectory; UseTotp = org.UseTotp; Use2fa = org.Use2fa; UseApi = org.UseApi; UseResetPassword = org.UseResetPassword; MaxStorageGb = org.MaxStorageGb; SelfHost = org.SelfHost; UsersGetPremium = org.UsersGetPremium; UseCustomPermissions = org.UseCustomPermissions; Issued = DateTime.UtcNow; UsePasswordManager = org.UsePasswordManager; UseSecretsManager = org.UseSecretsManager; SmSeats = org.SmSeats; SmServiceAccounts = org.SmServiceAccounts; LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion; if (subscriptionInfo?.Subscription == null) { if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) { Expires = Refresh = org.ExpirationDate.Value; Trial = false; } else { Expires = Refresh = Issued.AddDays(7); Trial = true; } } else if (subscriptionInfo.Subscription.TrialEndDate.HasValue && subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) { Expires = Refresh = subscriptionInfo.Subscription.TrialEndDate.Value; Trial = true; } else { if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) { // expired Expires = Refresh = org.ExpirationDate.Value; } else if (subscriptionInfo?.Subscription?.PeriodDuration != null && subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { Refresh = DateTime.UtcNow.AddDays(30); Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants .OrganizationSelfHostSubscriptionGracePeriodDays); ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate; } else { Expires = org.ExpirationDate.HasValue ? org.ExpirationDate.Value.AddMonths(11) : Issued.AddYears(1); Refresh = DateTime.UtcNow - Expires > TimeSpan.FromDays(30) ? DateTime.UtcNow.AddDays(30) : Expires; } Trial = false; } Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); } public string LicenseKey { get; set; } public Guid InstallationId { get; set; } public Guid Id { get; set; } public string Name { get; set; } public string BillingEmail { get; set; } public string BusinessName { get; set; } public bool Enabled { get; set; } public string Plan { get; set; } public PlanType PlanType { get; set; } public int? Seats { get; set; } public short? MaxCollections { get; set; } public bool UsePolicies { get; set; } public bool UseSso { get; set; } public bool UseKeyConnector { get; set; } public bool UseScim { get; set; } public bool UseGroups { get; set; } public bool UseEvents { get; set; } public bool UseDirectory { get; set; } public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi { get; set; } public bool UseResetPassword { get; set; } public short? MaxStorageGb { get; set; } public bool SelfHost { get; set; } public bool UsersGetPremium { get; set; } public bool UseCustomPermissions { get; set; } public int Version { get; set; } public DateTime Issued { get; set; } public DateTime? Refresh { get; set; } public DateTime? Expires { get; set; } public DateTime? ExpirationWithoutGracePeriod { get; set; } public bool UsePasswordManager { get; set; } public bool UseSecretsManager { get; set; } public int? SmSeats { get; set; } public int? SmServiceAccounts { get; set; } public bool LimitCollectionCreationDeletion { get; set; } = true; public bool Trial { get; set; } public LicenseType? LicenseType { get; set; } public string Hash { get; set; } public string Signature { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); /// /// Represents the current version of the license format. Should be updated whenever new fields are added. /// /// Intentionally set one version behind to allow self hosted users some time to update before /// getting out of date license errors public const int CurrentLicenseFileVersion = 13; private bool ValidLicenseVersion { get => Version is >= 1 and <= 14; } public byte[] GetDataBytes(bool forHash = false) { string data = null; if (ValidLicenseVersion) { var props = typeof(OrganizationLicense) .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && // UsersGetPremium was added in Version 2 (Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) && // UseEvents was added in Version 3 (Version >= 3 || !p.Name.Equals(nameof(UseEvents))) && // Use2fa was added in Version 4 (Version >= 4 || !p.Name.Equals(nameof(Use2fa))) && // UseApi was added in Version 5 (Version >= 5 || !p.Name.Equals(nameof(UseApi))) && // UsePolicies was added in Version 6 (Version >= 6 || !p.Name.Equals(nameof(UsePolicies))) && // UseSso was added in Version 7 (Version >= 7 || !p.Name.Equals(nameof(UseSso))) && // UseResetPassword was added in Version 8 (Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) && // UseKeyConnector was added in Version 9 (Version >= 9 || !p.Name.Equals(nameof(UseKeyConnector))) && // UseScim was added in Version 10 (Version >= 10 || !p.Name.Equals(nameof(UseScim))) && // UseCustomPermissions was added in Version 11 (Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) && // ExpirationWithoutGracePeriod was added in Version 12 (Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) && // UseSecretsManager, UsePasswordManager, SmSeats, and SmServiceAccounts were added in Version 13 (Version >= 13 || !p.Name.Equals(nameof(UseSecretsManager))) && (Version >= 13 || !p.Name.Equals(nameof(UsePasswordManager))) && (Version >= 13 || !p.Name.Equals(nameof(SmSeats))) && (Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) && // LimitCollectionCreationDeletion was added in Version 14 (Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) && ( !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}"); data = $"license:organization|{props}"; } else { throw new NotSupportedException($"Version {Version} is not supported."); } return Encoding.UTF8.GetBytes(data); } public byte[] ComputeHash() { using (var alg = SHA256.Create()) { return alg.ComputeHash(GetDataBytes(true)); } } public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) { if (!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { exception = "Invalid license. Your organization is disabled or the license has expired."; return false; } if (!ValidLicenseVersion) { exception = $"Version {Version} is not supported."; return false; } if (InstallationId != globalSettings.Installation.Id || !SelfHost) { exception = "Invalid license. Make sure your license allows for on-premise " + "hosting of organizations and that the installation id matches your current installation."; return false; } if (LicenseType != null && LicenseType != Enums.LicenseType.Organization) { exception = "Premium licenses cannot be applied to an organization. " + "Upload this license from your personal account settings page."; return false; } if (!licensingService.VerifyLicense(this)) { exception = "Invalid license."; return false; } exception = ""; return true; } public bool VerifyData(Organization organization, IGlobalSettings globalSettings) { if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; } if (ValidLicenseVersion) { var valid = globalSettings.Installation.Id == InstallationId && organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) && organization.Enabled == Enabled && organization.PlanType == PlanType && organization.Seats == Seats && organization.MaxCollections == MaxCollections && organization.UseGroups == UseGroups && organization.UseDirectory == UseDirectory && organization.UseTotp == UseTotp && organization.SelfHost == SelfHost && organization.Name.Equals(Name); if (valid && Version >= 2) { valid = organization.UsersGetPremium == UsersGetPremium; } if (valid && Version >= 3) { valid = organization.UseEvents == UseEvents; } if (valid && Version >= 4) { valid = organization.Use2fa == Use2fa; } if (valid && Version >= 5) { valid = organization.UseApi == UseApi; } if (valid && Version >= 6) { valid = organization.UsePolicies == UsePolicies; } if (valid && Version >= 7) { valid = organization.UseSso == UseSso; } if (valid && Version >= 8) { valid = organization.UseResetPassword == UseResetPassword; } if (valid && Version >= 9) { valid = organization.UseKeyConnector == UseKeyConnector; } if (valid && Version >= 10) { valid = organization.UseScim == UseScim; } if (valid && Version >= 11) { valid = organization.UseCustomPermissions == UseCustomPermissions; } if (valid && Version >= 13) { valid = organization.UseSecretsManager == UseSecretsManager && organization.UsePasswordManager == UsePasswordManager && organization.SmSeats == SmSeats && organization.SmServiceAccounts == SmServiceAccounts; } // Restore validity check when Flexible Collections are enabled for cloud and self-host // https://bitwarden.atlassian.net/browse/AC-1875 // if (valid && Version >= 14) // { // valid = organization.LimitCollectionCreationDeletion == LimitCollectionCreationDeletion; // } return valid; } else { throw new NotSupportedException($"Version {Version} is not supported."); } } public bool VerifySignature(X509Certificate2 certificate) { using (var rsa = certificate.GetRSAPublicKey()) { return rsa.VerifyData(GetDataBytes(), SignatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } public byte[] Sign(X509Certificate2 certificate) { if (!certificate.HasPrivateKey) { throw new InvalidOperationException("You don't have the private key!"); } using (var rsa = certificate.GetRSAPrivateKey()) { return rsa.SignData(GetDataBytes(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } } }