From dbaaa8b45d0b17036b57fed68c33bbcad198233e Mon Sep 17 00:00:00 2001 From: Lorenzo Moscati Date: Sun, 12 Apr 2026 08:04:48 +0200 Subject: [PATCH] Add missing licence options, bump licence version and add Token claim (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add license options, bump version, add Token claim Added the missing license options: - `ExpirationWithoutGracePeriod` - `UseAutomaticUserConfirmation` - `UsePhisingBlocker` - `UseDisableSmAdsForUsers` - `UseMyItems` Bumped the license version from 15 to 16 (`UseOrganizationDomains` is valid only with version 16). Added `GenerateUserToken` and `GenerateOrgToken` helpers to create the expected `Token` claim that’s still missing in our licenses. Signed-off-by: Lorenzo Moscati * Address Copilot review - Capture `now = DateTime.UtcNow` once per license-generation call and pass it into GenerateUserToken/GenerateOrgToken, so all date fields in the JSON license and the embedded JWT derive from the same instant (fixes drift across separate DateTime.UtcNow calls) - Move set("Token", ...) before ComputeHash/Sign in both GenerateUserLicense and GenerateOrgLicense so all fields are finalised before signing - Add UseRiskInsights claim to the org JWT; confirmed present as a required claim in Bitwarden's OrganizationLicenseClaimsFactory Version is intentionally excluded from both JWTs: neither UserLicenseClaimsFactory nor OrganizationLicenseClaimsFactory generates it, and the claims-path VerifyData never reads it. Signed-off-by: Lorenzo Moscati --------- Signed-off-by: Lorenzo Moscati --- src/licenseGen/Program.cs | 149 ++++++++++++++++++++++++++++--- src/licenseGen/licenseGen.csproj | 1 + 2 files changed, 137 insertions(+), 13 deletions(-) diff --git a/src/licenseGen/Program.cs b/src/licenseGen/Program.cs index 6b464ce..019e3b1 100644 --- a/src/licenseGen/Program.cs +++ b/src/licenseGen/Program.cs @@ -1,11 +1,15 @@ namespace BitwardenSelfLicensor { using Microsoft.Extensions.CommandLineUtils; + using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using SingleFileExtractor.Core; using System; + using System.Collections.Generic; + using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Runtime.Loader; + using System.Security.Claims; using System.Security.Cryptography.X509Certificates; public static class Program @@ -20,7 +24,7 @@ namespace BitwardenSelfLicensor bool ExecExists() => File.Exists(exec.Value()); bool CertExists() => File.Exists(cert.Value()); bool CoreExists() => File.Exists(coreDll.Value()); - bool VerifyTopOptions() => + bool VerifyTopOptions() => !string.IsNullOrWhiteSpace(cert.Value()) && (!string.IsNullOrWhiteSpace(coreDll.Value()) || !string.IsNullOrWhiteSpace(exec.Value())) && CertExists() && @@ -34,7 +38,7 @@ namespace BitwardenSelfLicensor return fileInfo.FullName; } string GetCoreDllPath() => CoreExists() ? coreDll.Value() : GetExtractedDll(); - + app.Command("interactive", config => { string buff="", licensetype="", name="", email="", businessname=""; @@ -360,19 +364,23 @@ namespace BitwardenSelfLicensor type.GetProperty(name).SetValue(license, value); } - set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key); + var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; + + set("LicenseKey", licenseKey); set("Id", userId); set("Name", userName); set("Email", email); set("Premium", true); set("MaxStorageGb", storage == 0 ? short.MaxValue : storage); set("Version", 1); - set("Issued", DateTime.UtcNow); - set("Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1)); - set("Expires", DateTime.UtcNow.AddYears(100)); + var now = DateTime.UtcNow; + set("Issued", now); + set("Refresh", now.AddYears(100).AddMonths(-1)); + set("Expires", now.AddYears(100)); set("Trial", false); set("LicenseType", Enum.Parse(licenseTypeEnum, "User")); + set("Token", GenerateUserToken(cert, userId, licenseKey, userName, email, storage, now)); set("Hash", Convert.ToBase64String((byte[])type.GetMethod("ComputeHash").Invoke(license, new object[0]))); set("Signature", Convert.ToBase64String((byte[])type.GetMethod("Sign").Invoke(license, new object[] { cert }))); @@ -394,12 +402,15 @@ namespace BitwardenSelfLicensor type.GetProperty(name).SetValue(license, value); } - set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key); + var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; + var businessNameFinal = string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName; + + set("LicenseKey", licenseKey); set("InstallationId", instalId); set("Id", Guid.NewGuid()); set("Name", userName); set("BillingEmail", email); - set("BusinessName", string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName); + set("BusinessName", businessNameFinal); set("Enabled", true); set("Plan", "Enterprise (Annually)"); set("PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually")); @@ -424,22 +435,134 @@ namespace BitwardenSelfLicensor set("UseSecretsManager", true); set("SmSeats", int.MaxValue); set("SmServiceAccounts", int.MaxValue); - set("Version", 15); //This is set to 15 to use AllowAdminAccessToAllCollectionItems can be changed to 13 to just use Secrets Manager - set("Issued", DateTime.UtcNow); - set("Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1)); - set("Expires", DateTime.UtcNow.AddYears(100)); + set("Version", 16); + var now = DateTime.UtcNow; + set("Issued", now); + set("Refresh", now.AddYears(100).AddMonths(-1)); + set("Expires", now.AddYears(100)); + set("ExpirationWithoutGracePeriod", now.AddYears(100)); set("Trial", false); set("LicenseType", Enum.Parse(licenseTypeEnum, "Organization")); - set("LimitCollectionCreationDeletion", true); //This will be used in the new version of BitWarden but can be applied now + set("LimitCollectionCreationDeletion", true); set("AllowAdminAccessToAllCollectionItems", true); set("UseRiskInsights", true); set("UseOrganizationDomains", true); set("UseAdminSponsoredFamilies", true); + set("UseAutomaticUserConfirmation", true); + set("UsePhishingBlocker", true); + set("UseDisableSmAdsForUsers", true); + set("UseMyItems", true); + var orgId = (Guid)type.GetProperty("Id").GetValue(license); + + set("Token", GenerateOrgToken(cert, orgId, instalId, licenseKey, email, businessNameFinal, userName, storage, planTypeEnum, now)); set("Hash", Convert.ToBase64String((byte[])type.GetMethod("ComputeHash").Invoke(license, new object[0]))); set("Signature", Convert.ToBase64String((byte[])type.GetMethod("Sign").Invoke(license, new object[] { cert }))); Console.WriteLine(JsonConvert.SerializeObject(license, Formatting.Indented)); } + + private static string GenerateUserToken(X509Certificate2 cert, Guid userId, string licenseKey, string name, string email, short maxStorageGb, DateTime now) + { + var secKey = new X509SecurityKey(cert); + var creds = new SigningCredentials(secKey, SecurityAlgorithms.RsaSha256); + var expires = now.AddYears(100); + + var claims = new List + { + new Claim("LicenseType", "User"), + new Claim("LicenseKey", licenseKey), + new Claim("Id", userId.ToString()), + new Claim("Name", name), + new Claim("Email", email), + new Claim("Premium", "true"), + new Claim("MaxStorageGb", (maxStorageGb == 0 ? short.MaxValue : maxStorageGb).ToString()), + new Claim("Trial", "false"), + new Claim("Issued", now.ToString("o")), + new Claim("Expires", expires.ToString("o")), + new Claim("Refresh", now.AddYears(100).AddMonths(-1).ToString("o")), + }; + + var handler = new JwtSecurityTokenHandler(); + var token = new JwtSecurityToken( + issuer: "bitwarden", + audience: $"user:{userId}", + claims: claims, + notBefore: now, + expires: expires, + signingCredentials: creds); + + return handler.WriteToken(token); + } + + private static string GenerateOrgToken(X509Certificate2 cert, Guid orgId, Guid installationId, string licenseKey, string billingEmail, string businessName, string name, short maxStorageGb, Type planTypeEnum, DateTime now) + { + var secKey = new X509SecurityKey(cert); + var creds = new SigningCredentials(secKey, SecurityAlgorithms.RsaSha256); + var expires = now.AddYears(100); + + // Resolve the integer value of EnterpriseAnnually from the runtime enum + var planTypeInt = Convert.ToInt32(Enum.Parse(planTypeEnum, "EnterpriseAnnually")); + + var claims = new List + { + new Claim("LicenseType", "Organization"), + new Claim("LicenseKey", licenseKey), + new Claim("InstallationId", installationId.ToString()), + new Claim("Id", orgId.ToString()), + new Claim("Name", name), + new Claim("BillingEmail", billingEmail), + new Claim("BusinessName", businessName), + new Claim("Enabled", "true"), + new Claim("Plan", "Enterprise (Annually)"), + new Claim("PlanType", planTypeInt.ToString()), + new Claim("Seats", int.MaxValue.ToString()), + new Claim("MaxCollections", short.MaxValue.ToString()), + new Claim("MaxStorageGb", (maxStorageGb == 0 ? short.MaxValue : maxStorageGb).ToString()), + new Claim("SelfHost", "true"), + new Claim("UsersGetPremium", "true"), + new Claim("UseGroups", "true"), + new Claim("UseDirectory", "true"), + new Claim("UseEvents", "true"), + new Claim("UseTotp", "true"), + new Claim("Use2fa", "true"), + new Claim("UseApi", "true"), + new Claim("UsePolicies", "true"), + new Claim("UseSso", "true"), + new Claim("UseResetPassword", "true"), + new Claim("UseKeyConnector", "true"), + new Claim("UseScim", "true"), + new Claim("UseCustomPermissions", "true"), + new Claim("UsePasswordManager", "true"), + new Claim("UseSecretsManager", "true"), + new Claim("SmSeats", int.MaxValue.ToString()), + new Claim("SmServiceAccounts", int.MaxValue.ToString()), + new Claim("UseRiskInsights", "true"), + new Claim("UseAdminSponsoredFamilies", "true"), + new Claim("UseOrganizationDomains", "true"), + new Claim("UseAutomaticUserConfirmation", "true"), + new Claim("UseDisableSmAdsForUsers", "true"), + new Claim("UsePhishingBlocker", "true"), + new Claim("UseMyItems", "true"), + new Claim("LimitCollectionCreationDeletion", "true"), + new Claim("AllowAdminAccessToAllCollectionItems", "true"), + new Claim("Trial", "false"), + new Claim("Issued", now.ToString("o")), + new Claim("Expires", expires.ToString("o")), + new Claim("Refresh", now.AddYears(100).AddMonths(-1).ToString("o")), + new Claim("ExpirationWithoutGracePeriod", expires.ToString("o")), + }; + + var handler = new JwtSecurityTokenHandler(); + var token = new JwtSecurityToken( + issuer: "bitwarden", + audience: $"organization:{orgId}", + claims: claims, + notBefore: now, + expires: expires, + signingCredentials: creds); + + return handler.WriteToken(token); + } } } diff --git a/src/licenseGen/licenseGen.csproj b/src/licenseGen/licenseGen.csproj index 2bff89f..4a11363 100644 --- a/src/licenseGen/licenseGen.csproj +++ b/src/licenseGen/licenseGen.csproj @@ -10,6 +10,7 @@ +