From 287b6a4d343b1e270a94e8902837ef99e5509425 Mon Sep 17 00:00:00 2001 From: Michiel Hazelhof Date: Fri, 5 Jun 2026 20:09:27 +0200 Subject: [PATCH] Fixes for newer containers (#280) * Find the new file Next extract Core.dll, patch, reinsert * Prepare Linux script too * Extract and patch Core.dll again * Make build script dynamic * Cleanup after ourselves * Build, then delete file * Attempt to deconstruct the new file * Add missing quotes * Add missing file * Rectify dll location * Fix dumb extra character matching * Dynamically find LicensingService and improve error reporting * Upgrade package * Implement new code * Filter out new lines * Force the runtime config * Use correct external .NET library * Update to .NET 10 in build.sh Copy .NET 10 runtime from aspnet10.0-alpine3.23 to the bitwarden-lite container * Update to .NET 10 in build.ps1 Copy .NET 10 runtime from aspnet:10.0-alpine3.23 to bitwarden-lite container * Update generateKeys.sh to OpenSSL 3.x-compatible Update to OpenSSL 3.x-compatible cipher generation * Update generateKeys.ps1 to OpenSSL 3.x-compatible Update to OpenSSL 3.x-compatible cipher * Update bitBetter Dockerfile to .NET 10 * Update bitBetter.csproj to .NET 10 * Switch to X509CertificateLoader, switch to patching multiple thumbprint certificates Update Program.cs in bitBetter to switch from deprecated X509Certificate to X509CertificateLoader Patch multiple thumbprint certificate instances * Update licenseGen Dockerfile to .NET 10 * Update licenseGen.csproj to .NET 10 * Remove extra line * Get rid of extra line * Update deprecated X509Certificate2 in LicenseGen * Cleanup * Fix tabbing --------- Co-authored-by: Michiel Hazelhof Co-authored-by: huntb4646 <94577767+huntb4646@users.noreply.github.com> --- build.ps1 | 20 ++- build.sh | 20 ++- generateKeys.ps1 | 2 +- generateKeys.sh | 2 +- src/bitBetter/Dockerfile | 6 +- src/bitBetter/Dockerfile-bitwarden-patch | 3 - src/bitBetter/Program.cs | 121 +++++++++++++---- src/bitBetter/bitBetter.csproj | 5 +- src/licenseGen/Dockerfile | 6 +- src/licenseGen/Program.cs | 159 ++++++++++++++++++++--- src/licenseGen/licenseGen.csproj | 7 +- 11 files changed, 287 insertions(+), 64 deletions(-) delete mode 100644 src/bitBetter/Dockerfile-bitwarden-patch diff --git a/build.ps1 b/build.ps1 index 8a494d3..cc23b05 100644 --- a/build.ps1 +++ b/build.ps1 @@ -92,7 +92,8 @@ New-item -ItemType Directory -Path $tempdirectory # extract the files that need to be patched from the services that need to be patched into our temporary directory foreach ($component in $components) { New-item -itemtype Directory -path "$tempdirectory\$component" - docker cp $patchinstance`:/app/$component/Core.dll "$tempdirectory\$component\Core.dll" + docker cp $patchinstance`:/app/$component/$component "$tempdirectory\$component\$component" + docker cp $patchinstance`:/etc/supervisor.d/$($component.ToLower()).ini "$tempdirectory\$($component.ToLower()).ini" } # stop and remove our temporary container @@ -103,12 +104,25 @@ docker rm bitwarden-extract docker run -v "$tempdirectory`:/app/mount" --rm bitbetter/bitbetter # create a new image with the patched files -docker build . --tag bitwarden-patched --file "$pwd\src\bitBetter\Dockerfile-bitwarden-patch" +if (Test-Path -Path "$pwd\Dockerfile-bitwarden-patch" -PathType Leaf) { + Remove-Item "$pwd\Dockerfile-bitwarden-patch" -Force +} +$dockerFile = "FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine3.23" +$dockerFile = -join($dockerFile, "FROM ghcr.io/bitwarden/lite:latest") +$dockerFile = -join($dockerFile, "COPY --from=0 /usr/share/dotnet /usr/share/dotnet") +foreach ($component in $components) { + $dockerFile = -join($dockerFile, "`n`nCOPY ./temp/$component/ /app/$component/") + $dockerFile = -join($dockerFile, "`nCOPY ./temp/$($component.ToLower()).ini /etc/supervisor.d/$($component.ToLower()).ini") + $dockerFile = -join($dockerFile, "`nRUN rm -f /app/$component/$component") +} +[System.IO.File]::WriteAllLines("$pwd\Dockerfile-bitwarden-patch", $dockerFile) +docker build . --tag bitwarden-patched --file "$pwd\Dockerfile-bitwarden-patch" +Remove-Item "$pwd\Dockerfile-bitwarden-patch" -Force # start all user requested instances if (Test-Path -Path "$pwd\.servers\serverlist.txt" -PathType Leaf) { foreach($line in Get-Content "$pwd\.servers\serverlist.txt") { - if (!($line.StartsWith("#"))) { + if ((-not ($line.StartsWith("#"))) -and (-not [string]::IsNullOrWhiteSpace($line))) { Invoke-Expression "& $line" } } diff --git a/build.sh b/build.sh index 3adeba7..688a54e 100755 --- a/build.sh +++ b/build.sh @@ -93,7 +93,8 @@ mkdir $TEMPDIRECTORY # extract the files that need to be patched from the services that need to be patched into our temporary directory for COMPONENT in ${COMPONENTS[@]}; do mkdir "$TEMPDIRECTORY/$COMPONENT" - docker cp $PATCHINSTANCE:/app/$COMPONENT/Core.dll "$TEMPDIRECTORY/$COMPONENT/Core.dll" + docker cp $PATCHINSTANCE:/app/$COMPONENT/$COMPONENT "$TEMPDIRECTORY/$COMPONENT/$COMPONENT" + docker cp $PATCHINSTANCE:/etc/supervisor.d/${COMPONENT,,}.ini "$TEMPDIRECTORY/${COMPONENT,,}.ini" done # stop and remove our temporary container @@ -104,14 +105,27 @@ docker rm bitwarden-extract docker run -v "$TEMPDIRECTORY:/app/mount" --rm bitbetter/bitbetter # create a new image with the patched files -docker build . --tag bitwarden-patched --file "$PWD/src/bitBetter/Dockerfile-bitwarden-patch" +if [ -f "$PWD/Dockerfile-bitwarden-patch" ]; then + rm -f "$PWD/Dockerfile-bitwarden-patch" +fi +echo "FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine3.23" >> "$PWD/Dockerfile-bitwarden-patch" +echo "FROM ghcr.io/bitwarden/lite:latest" >> "$PWD/Dockerfile-bitwarden-patch" +echo "COPY --from=0 /usr/share/dotnet /usr/share/dotnet" >> "$PWD/Dockerfile-bitwarden-patch" +for COMPONENT in ${COMPONENTS[@]}; do + echo "" >> "$PWD/Dockerfile-bitwarden-patch" + echo "RUN rm -f /app/$COMPONENT/$COMPONENT" >> "$PWD/Dockerfile-bitwarden-patch" + echo "COPY ./temp/${COMPONENT,,}.ini /etc/supervisor.d/${COMPONENT,,}.ini" >> "$PWD/Dockerfile-bitwarden-patch" + echo "COPY ./temp/$COMPONENT/ /app/$COMPONENT/" >> "$PWD/Dockerfile-bitwarden-patch" +done +docker build . --tag bitwarden-patched --file "$PWD/Dockerfile-bitwarden-patch" +rm -f "$PWD/Dockerfile-bitwarden-patch" # start all user requested instances if [ -f "$PWD/.servers/serverlist.txt" ]; then # convert line endings to unix sed -i 's/\r$//' "$PWD/.servers/serverlist.txt" cat "$PWD/.servers/serverlist.txt" | while read -r LINE; do - if [[ $LINE != "#"* ]]; then + if [[ $LINE != "#"* && -n $LINE ]]; then bash -c "$LINE" fi done diff --git a/generateKeys.ps1 b/generateKeys.ps1 index fafbf95..ad34b21 100644 --- a/generateKeys.ps1 +++ b/generateKeys.ps1 @@ -22,4 +22,4 @@ New-item -ItemType Directory -Path "$pwd\.keys" # generate actual keys Invoke-Expression "& '$opensslbinary' req -x509 -newkey rsa:4096 -keyout `"$pwd\.keys\key.pem`" -out `"$pwd\.keys\cert.cer`" -days 36500 -subj '/CN=www.mydom.com/O=My Company Name LTD./C=US' -outform DER -passout pass:test" Invoke-Expression "& '$opensslbinary' x509 -inform DER -in `"$pwd\.keys\cert.cer`" -out `"$pwd\.keys\cert.pem`"" -Invoke-Expression "& '$opensslbinary' pkcs12 -export -out `"$pwd\.keys\cert.pfx`" -inkey `"$pwd\.keys\key.pem`" -in `"$pwd\.keys\cert.pem`" -passin pass:test -passout pass:test" \ No newline at end of file +Invoke-Expression "& '$opensslbinary' pkcs12 -export -out `"$pwd\.keys\cert.pfx`" -inkey `"$pwd\.keys\key.pem`" -in `"$pwd\.keys\cert.pem`" -passin pass:test -passout pass:test -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256" diff --git a/generateKeys.sh b/generateKeys.sh index a700ce6..4de2e0d 100755 --- a/generateKeys.sh +++ b/generateKeys.sh @@ -17,4 +17,4 @@ mkdir "$DIR" # Generate new keys openssl req -x509 -newkey rsa:4096 -keyout "$DIR/key.pem" -out "$DIR/cert.cer" -days 36500 -subj '/CN=www.mydom.com/O=My Company Name LTD./C=US' -outform DER -passout pass:test openssl x509 -inform DER -in "$DIR/cert.cer" -out "$DIR/cert.pem" -openssl pkcs12 -export -out "$DIR/cert.pfx" -inkey "$DIR/key.pem" -in "$DIR/cert.pem" -passin pass:test -passout pass:test +openssl pkcs12 -export -out "$DIR/cert.pfx" -inkey "$DIR/key.pem" -in "$DIR/cert.pem" -passin pass:test -passout pass:test -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256 diff --git a/src/bitBetter/Dockerfile b/src/bitBetter/Dockerfile index e93b428..f26ba15 100644 --- a/src/bitBetter/Dockerfile +++ b/src/bitBetter/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /bitBetter COPY . /bitBetter @@ -7,8 +7,8 @@ COPY cert.cer /app/ RUN dotnet restore RUN dotnet publish -c Release -o /app --no-restore -FROM mcr.microsoft.com/dotnet/sdk:8.0 +FROM mcr.microsoft.com/dotnet/sdk:10.0 WORKDIR /app COPY --from=build /app . -ENTRYPOINT ["dotnet", "/app/bitBetter.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "/app/bitBetter.dll"] diff --git a/src/bitBetter/Dockerfile-bitwarden-patch b/src/bitBetter/Dockerfile-bitwarden-patch deleted file mode 100644 index a3f7360..0000000 --- a/src/bitBetter/Dockerfile-bitwarden-patch +++ /dev/null @@ -1,3 +0,0 @@ -FROM ghcr.io/bitwarden/lite:latest - -COPY ./temp/ /app/ \ No newline at end of file diff --git a/src/bitBetter/Program.cs b/src/bitBetter/Program.cs index 902479e..a4ae7cc 100644 --- a/src/bitBetter/Program.cs +++ b/src/bitBetter/Program.cs @@ -1,12 +1,13 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; +using System.Text; using dnlib.DotNet; using dnlib.DotNet.Emit; using dnlib.DotNet.Writer; using dnlib.IO; +using SingleFileExtractor.Core; namespace bitBetter; @@ -15,12 +16,46 @@ internal class Program private static Int32 Main() { const String certFile = "/app/cert.cer"; - String[] files = Directory.GetFiles("/app/mount", "Core.dll", SearchOption.AllDirectories); - foreach (String file in files) + foreach (String iniFile in Directory.GetFiles("/app/mount/", "*.ini", SearchOption.TopDirectoryOnly)) { - Console.WriteLine(file); - ModuleDefMD moduleDefMd = ModuleDefMD.Load(file); + Console.WriteLine("Patching: " + iniFile); + + String[] lines = File.ReadAllLines(iniFile); + for (Int32 i = 0; i < lines.Length; i++) + { + String line = lines[i]; + if (!line.StartsWith("command=", StringComparison.Ordinal)) continue; + + String appNameAndPath = line[(line.LastIndexOf('=') + 1)..]; + lines[i] = "command=/usr/bin/dotnet \"" + appNameAndPath + ".dll\" --runtimeconfig \"" + appNameAndPath + ".runtimeconfig.json\""; + break; + } + File.WriteAllText(iniFile, String.Join("\n", lines), new UTF8Encoding(false)); + } + + foreach (String singleFile in Directory.GetFiles("/app/mount/", "*", SearchOption.AllDirectories)) + { + if (Path.HasExtension(singleFile)) continue; + + Console.WriteLine("Extracting: " + singleFile); + + ExecutableReader reader1 = new(singleFile); + String currentDirectory = Path.GetDirectoryName(singleFile); + String newCoreDll = Path.Combine(currentDirectory, "Core.dll"); + reader1.ExtractToDirectory(currentDirectory); + reader1.Dispose(); + + File.Delete(singleFile); + + if (!File.Exists(newCoreDll)) + { + Console.WriteLine("Could not extract Core.dll for " + singleFile); + Environment.Exit(-1); + } + + Console.WriteLine("Extracted: " + newCoreDll); + ModuleDefMD moduleDefMd = ModuleDefMD.Load(newCoreDll); Byte[] cert = File.ReadAllBytes(certFile); EmbeddedResource embeddedResourceToRemove = moduleDefMd.Resources.OfType().First(r => r.Name.Equals("Bit.Core.licensing.cer")); @@ -29,39 +64,79 @@ internal class Program moduleDefMd.Resources.Remove(embeddedResourceToRemove); DataReader reader = embeddedResourceToRemove.CreateReader(); - X509Certificate2 existingCert = new(reader.ReadRemainingBytes()); - Console.WriteLine($"Existing Cert Thumbprint: {existingCert.Thumbprint}"); - X509Certificate2 certificate = new(cert); + X509Certificate2 existingCert = X509CertificateLoader.LoadCertificate(reader.ReadRemainingBytes()); + X509Certificate2 certificate = X509CertificateLoader.LoadCertificate(cert); + Console.WriteLine($"Existing certificate Thumbprint: {existingCert.Thumbprint}"); + Console.WriteLine($"New certificate Thumbprint: {certificate.Thumbprint}"); - Console.WriteLine($"New Cert Thumbprint: {certificate.Thumbprint}"); - - IEnumerable services = moduleDefMd.Types.Where(t => t.Namespace == "Bit.Core.Billing.Services"); - TypeDef type = services.First(t => t.Name == "LicensingService"); - MethodDef constructor = type.FindConstructors().First(); - - Instruction instructionToPatch = constructor.Body.Instructions.FirstOrDefault(i => i.OpCode == OpCodes.Ldstr && String.Equals((String)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase)); - - if (instructionToPatch != null) + // Find LicensingService by class name (namespace-agnostic to handle renames) + TypeDef type = moduleDefMd.Types.FirstOrDefault(t => String.Equals(t.Name, "LicensingService", StringComparison.OrdinalIgnoreCase)); + if (type == null) { - instructionToPatch.Operand = certificate.Thumbprint; + Console.Error.WriteLine("ERROR: LicensingService class not found"); + return -1; + } + Console.WriteLine($"Found: {type.FullName}"); + + MethodDef constructor = type.FindConstructors().First(); + + if (constructor == null) + { + Console.Error.WriteLine("ERROR: Cannot find constructor"); + return -1; + } + + Instruction[] instructionToPatch = constructor.Body.Instructions + .Where(i => i.OpCode == OpCodes.Ldstr) + .Where(i => ((String)i.Operand) + .Contains(existingCert.Thumbprint, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (instructionToPatch.Length > 0) + { + Console.WriteLine($"Found {instructionToPatch.Length} thumbprint Ldstr instruction(s) to replace"); + foreach (Instruction inst in instructionToPatch) + { + Console.WriteLine($" Replacing: '{inst.Operand}'"); + inst.Operand = certificate.Thumbprint; + } } else { - Console.WriteLine("Can't find constructor to patch"); + Console.WriteLine("ERROR: Can't find instruction to patch"); + return -1; } + Console.WriteLine("Writing: " + newCoreDll); + ModuleWriterOptions moduleWriterOptions = new(moduleDefMd); moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.KeepOldMaxStack; moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveAll; moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveRids; - moduleDefMd.Write(file + ".new"); + moduleDefMd.Write(newCoreDll + ".new"); moduleDefMd.Dispose(); - File.Delete(file); - File.Move(file + ".new", file); + File.Delete(newCoreDll); + File.Move(newCoreDll + ".new", newCoreDll); + } + + foreach (String runtimeConfigFile in Directory.GetFiles("/app/mount/", "*.runtimeconfig.json", SearchOption.AllDirectories)) + { + Console.WriteLine("Patching: " + runtimeConfigFile); + + String[] lines = File.ReadAllLines(runtimeConfigFile); + for (Int32 i = 0; i < lines.Length; i++) + { + String line = lines[i]; + if (!line.Contains("includedFrameworks", StringComparison.Ordinal)) continue; + + lines[i] = lines[i].Replace("includedFrameworks", "frameworks", StringComparison.Ordinal); + break; + } + File.WriteAllText(runtimeConfigFile, String.Join("\n", lines), new UTF8Encoding(false)); } return 0; } -} \ No newline at end of file +} diff --git a/src/bitBetter/bitBetter.csproj b/src/bitBetter/bitBetter.csproj index 8ddb699..6efcb26 100644 --- a/src/bitBetter/bitBetter.csproj +++ b/src/bitBetter/bitBetter.csproj @@ -1,9 +1,10 @@ Exe - net8.0 + net10.0 + - \ No newline at end of file + diff --git a/src/licenseGen/Dockerfile b/src/licenseGen/Dockerfile index 601bf87..8227a43 100644 --- a/src/licenseGen/Dockerfile +++ b/src/licenseGen/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /licenseGen COPY . /licenseGen @@ -8,8 +8,8 @@ COPY cert.pfx /app/ RUN dotnet restore RUN dotnet publish -c Release -o /app --no-restore -FROM mcr.microsoft.com/dotnet/sdk:8.0 +FROM mcr.microsoft.com/dotnet/sdk:10.0 WORKDIR /app COPY --from=build /app . -ENTRYPOINT ["dotnet", "/app/licenseGen.dll", "--cert=/app/cert.pfx", "--core=/app/Core.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "/app/licenseGen.dll", "--cert=/app/cert.pfx", "--core=/app/Core.dll"] diff --git a/src/licenseGen/Program.cs b/src/licenseGen/Program.cs index 6b2dfd1..d66c5d3 100644 --- a/src/licenseGen/Program.cs +++ b/src/licenseGen/Program.cs @@ -1,10 +1,14 @@ +using McMaster.Extensions.CommandLineUtils; using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.IO; -using System.Text.Json; using System.Reflection; using System.Runtime.Loader; +using System.Security.Claims; using System.Security.Cryptography.X509Certificates; -using McMaster.Extensions.CommandLineUtils; +using System.Text.Json; +using Microsoft.IdentityModel.Tokens; namespace licenseGen; @@ -127,7 +131,7 @@ internal class Program buff = Console.ReadLine(); if (buff is "" or "y" or "Y") { - GenerateUserLicense(new X509Certificate2(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, guid, null); + GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, guid, null); } else { @@ -143,7 +147,7 @@ internal class Program buff = Console.ReadLine(); if (buff is "" or "y" or "Y") { - GenerateOrgLicense(new X509Certificate2(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, installid, businessName, null); + GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, installid, businessName, null); } else { @@ -197,7 +201,7 @@ internal class Program storageShort = (Int16) parsedStorage; } - GenerateUserLicense(new X509Certificate2(Cert.Value()!, "test"), CoreDll.Value(), name.Value, email.Value, storageShort, userId, key.Value); + GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value()!, "test"), CoreDll.Value(), name.Value, email.Value, storageShort, userId, key.Value); return 0; }); @@ -243,7 +247,7 @@ internal class Program storageShort = (Int16)parsedStorage; } - GenerateOrgLicense(new X509Certificate2(Cert.Value()!, "test"), CoreDll.Value(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value); + GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value()!, "test"), CoreDll.Value(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value); return 0; }); @@ -375,24 +379,28 @@ internal class Program return; } - Set(type, license, "LicenseKey", String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key); + String licenseKey = String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; + Set(type, license, "LicenseKey", licenseKey); Set(type, license, "Id", userId); Set(type, license, "Name", userName); Set(type, license, "Email", email); Set(type, license, "Premium", true); Set(type, license, "MaxStorageGb", storage == 0 ? Int16.MaxValue : storage); Set(type, license, "Version", 1); - Set(type, license, "Issued", DateTime.UtcNow); - Set(type, license, "Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1)); - Set(type, license, "Expires", DateTime.UtcNow.AddYears(100)); + DateTime issued = DateTime.UtcNow; + Set(type, license, "Issued", issued); + Set(type, license, "Refresh", issued.AddYears(100).AddMonths(-1)); + Set(type, license, "Expires", issued.AddYears(100)); Set(type, license, "Trial", false); Set(type, license, "LicenseType", Enum.Parse(licenseTypeEnum, "User")); + + Set(type, license, "Token", GenerateUserToken(cert, userId, licenseKey, userName, email, storage, issued)); Set(type, license, "Hash", Convert.ToBase64String(((Byte[])computeHash.Invoke(license, []))!)); Set(type, license, "Signature", Convert.ToBase64String((Byte[])sign.Invoke(license, [cert])!)); Console.WriteLine(JsonSerializer.Serialize(license, JsonOptions)); } - private static void GenerateOrgLicense(X509Certificate2 cert, String corePath, String userName, String email, Int16 storage, Guid instalId, String businessName, String key) + private static void GenerateOrgLicense(X509Certificate2 cert, String corePath, String userName, String email, Int16 storage, Guid installId, String businessName, String key) { Assembly core = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(corePath)); Type type = core.GetType("Bit.Core.Billing.Organizations.Models.OrganizationLicense"); @@ -431,12 +439,14 @@ internal class Program return; } - Set(type, license, "LicenseKey", String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key); - Set(type, license, "InstallationId", instalId); + String licenseKey = String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; + String businessNameFinal = String.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName; + Set(type, license, "LicenseKey", licenseKey); + Set(type, license, "InstallationId", installId); Set(type, license, "Id", Guid.NewGuid()); Set(type, license, "Name", userName); Set(type, license, "BillingEmail", email); - Set(type, license, "BusinessName", String.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName); + Set(type, license, "BusinessName", businessNameFinal); Set(type, license, "Enabled", true); Set(type, license, "Plan", "Enterprise (Annually)"); Set(type, license, "PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually")); @@ -458,10 +468,11 @@ internal class Program Set(type, license, "UsersGetPremium", true); Set(type, license, "UseCustomPermissions", true); Set(type, license, "Version", 16); - Set(type, license, "Issued", DateTime.UtcNow); - Set(type, license, "Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1)); - Set(type, license, "Expires", DateTime.UtcNow.AddYears(100)); - Set(type, license, "ExpirationWithoutGracePeriod", DateTime.UtcNow.AddYears(100)); + DateTime issued = DateTime.UtcNow; + Set(type, license, "Issued", issued); + Set(type, license, "Refresh", issued.AddYears(100).AddMonths(-1)); + Set(type, license, "Expires", issued.AddYears(100)); + Set(type, license, "ExpirationWithoutGracePeriod", issued.AddYears(100)); Set(type, license, "UsePasswordManager", true); Set(type, license, "UseSecretsManager", true); Set(type, license, "SmSeats", Int32.MaxValue); @@ -472,6 +483,13 @@ internal class Program Set(type, license, "UseOrganizationDomains", true); Set(type, license, "UseAdminSponsoredFamilies", true); Set(type, license, "UsePhishingBlocker", true); + Set(type, license, "UseAutomaticUserConfirmation", true); + Set(type, license, "UseDisableSmAdsForUsers", true); + Set(type, license, "UseMyItems", true); + + Guid orgId = (Guid)type.GetProperty("Id").GetValue(license); + + Set(type, license, "Token", GenerateOrgToken(cert, orgId, installId, licenseKey, email, businessNameFinal, userName, storage, planTypeEnum, issued)); Set(type, license, "Hash", Convert.ToBase64String((Byte[])computeHash.Invoke(license, [])!)); Set(type, license, "Signature", Convert.ToBase64String((Byte[])sign.Invoke(license, [cert])!)); @@ -481,4 +499,107 @@ internal class Program { type.GetProperty(name)?.SetValue(license, value); } -} \ No newline at end of file + + private static String GenerateUserToken(X509Certificate2 cert, Guid userId, String licenseKey, String name, String email, Int16 maxStorageGb, DateTime now) + { + X509SecurityKey x509SecurityKey = new(cert); + SigningCredentials signingCredentials = new(x509SecurityKey, SecurityAlgorithms.RsaSha256); + DateTime expires = now.AddYears(100); + + List claims = + [ + new("LicenseType", "User"), + new("LicenseKey", licenseKey), + new("Id", userId.ToString()), + new("Name", name), + new("Email", email), + new("Premium", "true"), + new("MaxStorageGb", (maxStorageGb == 0 ? Int16.MaxValue : maxStorageGb).ToString()), + new("Trial", "false"), + new("Issued", now.ToString("o")), + new("Expires", expires.ToString("o")), + new("Refresh", now.AddYears(100).AddMonths(-1).ToString("o")) + ]; + + JwtSecurityTokenHandler handler = new(); + JwtSecurityToken token = new( + issuer: "bitwarden", + audience: $"user:{userId}", + claims: claims, + notBefore: now, + expires: expires, + signingCredentials: signingCredentials); + + return handler.WriteToken(token); + } + + private static String GenerateOrgToken(X509Certificate2 cert, Guid orgId, Guid installationId, String licenseKey, String billingEmail, String businessName, String name, Int16 maxStorageGb, Type planTypeEnum, DateTime now) + { + X509SecurityKey x509SecurityKey = new(cert); + SigningCredentials signingCredentials = new(x509SecurityKey, SecurityAlgorithms.RsaSha256); + DateTime expires = now.AddYears(100); + + // Resolve the integer value of EnterpriseAnnually from the runtime enum + Int32 planTypeInt = Convert.ToInt32(Enum.Parse(planTypeEnum, "EnterpriseAnnually")); + + List claims = + [ + new("LicenseType", "Organization"), + new("LicenseKey", licenseKey), + new("InstallationId", installationId.ToString()), + new("Id", orgId.ToString()), + new("Name", name), + new("BillingEmail", billingEmail), + new("BusinessName", businessName), + new("Enabled", "true"), + new("Plan", "Enterprise (Annually)"), + new("PlanType", planTypeInt.ToString()), + new("Seats", Int32.MaxValue.ToString()), + new("MaxCollections", Int16.MaxValue.ToString()), + new("MaxStorageGb", (maxStorageGb == 0 ? Int16.MaxValue : maxStorageGb).ToString()), + new("SelfHost", "true"), + new("UsersGetPremium", "true"), + new("UseGroups", "true"), + new("UseDirectory", "true"), + new("UseEvents", "true"), + new("UseTotp", "true"), + new("Use2fa", "true"), + new("UseApi", "true"), + new("UsePolicies", "true"), + new("UseSso", "true"), + new("UseResetPassword", "true"), + new("UseKeyConnector", "true"), + new("UseScim", "true"), + new("UseCustomPermissions", "true"), + new("UsePasswordManager", "true"), + new("UseSecretsManager", "true"), + new("SmSeats", Int32.MaxValue.ToString()), + new("SmServiceAccounts", Int32.MaxValue.ToString()), + new("UseRiskInsights", "true"), + new("UseAdminSponsoredFamilies", "true"), + new("UseOrganizationDomains", "true"), + new("UseAutomaticUserConfirmation", "true"), + new("UseDisableSmAdsForUsers", "true"), + new("UsePhishingBlocker", "true"), + new("UseMyItems", "true"), + new("LimitCollectionCreationDeletion", "true"), + new("AllowAdminAccessToAllCollectionItems", "true"), + new("Trial", "false"), + new("Issued", now.ToString("o")), + new("Expires", expires.ToString("o")), + new("Refresh", now.AddYears(100).AddMonths(-1).ToString("o")), + new("ExpirationWithoutGracePeriod", expires.ToString("o")) + ]; + + JwtSecurityTokenHandler handler = new(); + JwtSecurityToken token = new( + issuer: "bitwarden", + audience: $"organization:{orgId}", + claims: claims, + notBefore: now, + expires: expires, + signingCredentials: signingCredentials); + + return handler.WriteToken(token); + } +} diff --git a/src/licenseGen/licenseGen.csproj b/src/licenseGen/licenseGen.csproj index ba128a9..4f3e6e1 100644 --- a/src/licenseGen/licenseGen.csproj +++ b/src/licenseGen/licenseGen.csproj @@ -1,10 +1,11 @@ Exe - net8.0 + net10.0 - + + - \ No newline at end of file +