using System.Reflection; using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses.Attributes; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Settings; using Bit.Core.Utilities; namespace Bit.Core.Billing.Licenses.Extensions; public static class LicenseExtensions { public static byte[] ComputeHash(this ILicense license) => SHA256.HashData(license.ToByteArray(true)); public static bool VerifySignature(this ILicense license, X509Certificate2 certificate) { var dataBytes = license.ToByteArray(); var signatureBytes = Convert.FromBase64String(license.Signature); using var rsa = certificate.GetRSAPublicKey(); return rsa.VerifyData(dataBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } public static byte[] Sign(this ILicense license, X509Certificate2 certificate) { if (!certificate.HasPrivateKey) { throw new InvalidOperationException("You don't have the private key!"); } var dataBytes = license.ToByteArray(); using var rsa = certificate.GetRSAPrivateKey(); return rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } public static byte[] ToByteArray(this ILicense license, bool forHash = false) { if (!license.ValidLicenseVersion) { throw new NotSupportedException($"Version {license.Version} is not supported."); } var props = license.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => { var ignoreAttr = p.GetCustomAttribute(); if (ignoreAttr is not null) { return !forHash && ignoreAttr.IncludeInSignature; } var versionAttr = p.GetCustomAttribute(); if (versionAttr is not null) { return versionAttr.Version <= license.Version; } return true; }) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{CoreHelpers.FormatLicenseSignatureValue(p.GetValue(license, null))}") .Aggregate((c, n) => $"{c}|{n}"); var data = $"license:{license.LicenseType.ToString().ToLowerInvariant()}|{props}"; return Encoding.UTF8.GetBytes(data); } public static T GetValue(this ClaimsPrincipal principal, string claimType) { var claim = principal.FindFirst(claimType); if (claim is null) { return default; } // Handle Guid if (typeof(T) == typeof(Guid)) { return Guid.TryParse(claim.Value, out var guid) ? (T)(object)guid : default; } // Handle DateTime if (typeof(T) == typeof(DateTime)) { return DateTime.TryParse(claim.Value, out var dateTime) ? (T)(object)dateTime : default; } // Handle TimeSpan if (typeof(T) == typeof(TimeSpan)) { return TimeSpan.TryParse(claim.Value, out var timeSpan) ? (T)(object)timeSpan : default; } // Check for Nullable Types var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); // Handle Enums if (underlyingType.IsEnum) { if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue)) { return (T)enumValue; // Cast back to T } return default; // Return default value for non-nullable enums or null for nullable enums } // Handle other Nullable Types (e.g., int?, bool?) if (underlyingType == typeof(int)) { return int.TryParse(claim.Value, out var intValue) ? (T)(object)intValue : default; } if (underlyingType == typeof(bool)) { return bool.TryParse(claim.Value, out var boolValue) ? (T)(object)boolValue : default; } if (underlyingType == typeof(double)) { return double.TryParse(claim.Value, out var doubleValue) ? (T)(object)doubleValue : default; } // Fallback to Convert.ChangeType for other types including strings return (T)Convert.ChangeType(claim.Value, underlyingType); } } public static class OrganizationLicenseExtensions { public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime issued) { if (subscriptionInfo?.Subscription == null) { return org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue ? org.ExpirationDate.Value : issued.AddDays(7); } if (subscriptionInfo.Subscription.TrialEndDate.HasValue && subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) { return subscriptionInfo.Subscription.TrialEndDate.Value; } if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) { // expired return org.ExpirationDate.Value; } if (subscriptionInfo?.Subscription?.PeriodDuration != null && subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { return subscriptionInfo.Subscription.PeriodEndDate.Value.AddDays(Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays); } return org.ExpirationDate.HasValue ? org.ExpirationDate.Value.AddMonths(11) : issued.AddYears(1); } public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime? expirationDate, DateTime issued) { if (subscriptionInfo?.Subscription == null) { return org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue ? org.ExpirationDate.Value : issued.AddDays(7); } if (subscriptionInfo.Subscription.TrialEndDate.HasValue && subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) { return subscriptionInfo.Subscription.TrialEndDate.Value; } if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) { return org.ExpirationDate.Value; } if (subscriptionInfo?.Subscription?.PeriodDuration != null && subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { return DateTime.UtcNow.AddDays(30); } return !expirationDate.HasValue || DateTime.UtcNow - expirationDate.Value > TimeSpan.FromDays(30) ? DateTime.UtcNow.AddDays(30) : expirationDate.Value; } public static DateTime? CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo) => subscriptionInfo?.Subscription != null && (!subscriptionInfo.Subscription.TrialEndDate.HasValue || subscriptionInfo.Subscription.TrialEndDate.Value <= DateTime.UtcNow) && (!org.ExpirationDate.HasValue || org.ExpirationDate.Value >= DateTime.UtcNow) && subscriptionInfo.Subscription.PeriodDuration != null && subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) ? subscriptionInfo.Subscription.PeriodEndDate : null; public static bool IsTrialing(this Organization org, SubscriptionInfo subscriptionInfo) => subscriptionInfo?.Subscription == null ? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue : subscriptionInfo.Subscription.TrialEndDate.HasValue && subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; public static bool CanUse( this OrganizationLicense license, IGlobalSettings globalSettings, ClaimsPrincipal claimsPrincipal, out string exception) { var errorMessages = new StringBuilder(); var enabled = claimsPrincipal.GetValue(nameof(OrganizationLicense.Enabled)); if (!enabled) { errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); } var installationId = claimsPrincipal.GetValue(nameof(OrganizationLicense.InstallationId)); if (installationId != globalSettings.Installation.Id) { errorMessages.AppendLine("The installation ID does not match the current installation."); } var selfHost = claimsPrincipal.GetValue(nameof(OrganizationLicense.SelfHost)); if (!selfHost) { errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); } var licenseType = claimsPrincipal.GetValue(nameof(OrganizationLicense.LicenseType)); if (licenseType != LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + "Upload this license from your personal account settings page."); } if (errorMessages.Length > 0) { exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; return false; } exception = ""; return true; } public static bool VerifyData( this OrganizationLicense license, Organization organization, ClaimsPrincipal claimsPrincipal, IGlobalSettings globalSettings) { var issued = claimsPrincipal.GetValue(nameof(OrganizationLicense.Issued)); var expires = claimsPrincipal.GetValue(nameof(OrganizationLicense.Expires)); var installationId = claimsPrincipal.GetValue(nameof(OrganizationLicense.InstallationId)); var licenseKey = claimsPrincipal.GetValue(nameof(OrganizationLicense.LicenseKey)); var enabled = claimsPrincipal.GetValue(nameof(OrganizationLicense.Enabled)); var planType = claimsPrincipal.GetValue(nameof(OrganizationLicense.PlanType)); var seats = claimsPrincipal.GetValue(nameof(OrganizationLicense.Seats)); var maxCollections = claimsPrincipal.GetValue(nameof(OrganizationLicense.MaxCollections)); var useGroups = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseGroups)); var useDirectory = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseDirectory)); var useTotp = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseTotp)); var selfHost = claimsPrincipal.GetValue(nameof(OrganizationLicense.SelfHost)); var name = claimsPrincipal.GetValue(nameof(OrganizationLicense.Name)); var usersGetPremium = claimsPrincipal.GetValue(nameof(OrganizationLicense.UsersGetPremium)); var useEvents = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseEvents)); var use2fa = claimsPrincipal.GetValue(nameof(OrganizationLicense.Use2fa)); var useApi = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseApi)); var usePolicies = claimsPrincipal.GetValue(nameof(OrganizationLicense.UsePolicies)); var useSso = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseSso)); var useResetPassword = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseResetPassword)); var useKeyConnector = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseKeyConnector)); var useScim = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseScim)); var useCustomPermissions = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseCustomPermissions)); var useSecretsManager = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseSecretsManager)); var usePasswordManager = claimsPrincipal.GetValue(nameof(OrganizationLicense.UsePasswordManager)); var smSeats = claimsPrincipal.GetValue(nameof(OrganizationLicense.SmSeats)); var smServiceAccounts = claimsPrincipal.GetValue(nameof(OrganizationLicense.SmServiceAccounts)); var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseAdminSponsoredFamilies)); var useOrganizationDomains = claimsPrincipal.GetValue(nameof(OrganizationLicense.UseOrganizationDomains)); return issued <= DateTime.UtcNow && expires >= DateTime.UtcNow && installationId == globalSettings.Installation.Id && licenseKey == organization.LicenseKey && enabled == organization.Enabled && planType == organization.PlanType && seats == organization.Seats && maxCollections == organization.MaxCollections && useGroups == organization.UseGroups && useDirectory == organization.UseDirectory && useTotp == organization.UseTotp && selfHost == organization.SelfHost && name == organization.Name && usersGetPremium == organization.UsersGetPremium && useEvents == organization.UseEvents && use2fa == organization.Use2fa && useApi == organization.UseApi && usePolicies == organization.UsePolicies && useSso == organization.UseSso && useResetPassword == organization.UseResetPassword && useKeyConnector == organization.UseKeyConnector && useScim == organization.UseScim && useCustomPermissions == organization.UseCustomPermissions && useSecretsManager == organization.UseSecretsManager && usePasswordManager == organization.UsePasswordManager && smSeats == organization.SmSeats && smServiceAccounts == organization.SmServiceAccounts && useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies && useOrganizationDomains == organization.UseOrganizationDomains; } } public static class UserLicenseExtensions { public static DateTime? CalculateFreshExpirationDate(this User user, SubscriptionInfo subscriptionInfo) => subscriptionInfo == null ? user.PremiumExpirationDate?.AddDays(7) : subscriptionInfo.UpcomingInvoice?.Date != null ? subscriptionInfo.UpcomingInvoice.Date.Value.AddDays(7) : user.PremiumExpirationDate?.AddDays(7); public static DateTime? CalculateFreshRefreshDate(this User user, SubscriptionInfo subscriptionInfo) => subscriptionInfo == null ? user.PremiumExpirationDate?.Date : subscriptionInfo?.UpcomingInvoice?.Date; public static bool IsTrialing(this User user, SubscriptionInfo subscriptionInfo) => subscriptionInfo != null && (subscriptionInfo?.Subscription?.TrialEndDate.HasValue ?? false) && subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; public static bool CanUse(this UserLicense license, User user, ClaimsPrincipal claimsPrincipal, out string exception) { var errorMessages = new StringBuilder(); if (!user.EmailVerified) { errorMessages.AppendLine("The user's email is not verified."); } var email = claimsPrincipal.GetValue(nameof(UserLicense.Email)); if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) { errorMessages.AppendLine("The user's email does not match the license email."); } if (errorMessages.Length > 0) { exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; return false; } exception = ""; return true; } public static bool VerifyData(this UserLicense license, User user, ClaimsPrincipal claimsPrincipal) { var licenseKey = claimsPrincipal.GetValue(nameof(UserLicense.LicenseKey)); var premium = claimsPrincipal.GetValue(nameof(UserLicense.Premium)); var email = claimsPrincipal.GetValue(nameof(UserLicense.Email)); return licenseKey == user.LicenseKey && premium == user.Premium && email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); } }