Compare commits

..

4 Commits

Author SHA1 Message Date
Genva
c820c1e741 Merge 5b5d1301c9 into 676dd7b85c 2025-06-16 10:22:49 +02:00
Genva
5b5d1301c9 remove obsolete project 2025-06-16 10:21:24 +02:00
Genva
317af2994b Clone only current version tag 2025-06-16 10:16:27 +02:00
Genva
1a2073cf35 Build image from source 2025-06-14 15:46:18 +02:00
10 changed files with 98 additions and 492 deletions

View File

@@ -13,12 +13,3 @@ jobs:
- run:
name: Build script
command: ./build.sh
- run:
name: Build licenseGen
command: ./src/licenseGen/build.sh
- run:
name: Test generating user license
command: ./src/licenseGen/run.sh ./.keys/cert.pfx user TestName TestEmail@example.com 4a619d4a-522d-4c70-8596-affb5b607c23
- run:
name: Test generating organization license
command: ./src/licenseGen/run.sh ./.keys/cert.pfx org TestName TestEmail@example.com 4a619d4a-522d-4c70-8596-affb5b607c23

View File

@@ -4,8 +4,6 @@ BitBetter is is a tool to modify Bitwarden's core dll to allow you to generate y
Please see the FAQ below for details on why this software was created.
Looking for a solution to the Lite (formerly unified) version of bitwarden, [go to the Lite branch](../../tree/lite).
_Beware! BitBetter does janky stuff to rewrite the bitwarden core dll and allow the installation of a self signed certificate. Use at your own risk!_
Credit to https://github.com/h44z/BitBetter and https://github.com/jakeswenson/BitBetter
@@ -68,27 +66,15 @@ From the BitBetter directory, simply run:
This will create a new self-signed certificate in the `.keys` directory if one does not already exist and then create a modified version of the official `bitwarden/api` called `bitbetter/api` and a modified version of the `bitwarden/identity` called `bitbetter/identity`.
By default, the build script runs in **fast patch mode**: it pulls the official Bitwarden images from the GitHub Container Registry and patches the license certificate directly inside them — no local compilation required.
To instead **build from source**, set the `BITBETTER_BUILD_FROM_SOURCE` environment variable to `1` before running the script:
```bash
BITBETTER_BUILD_FROM_SOURCE=1 ./build.sh
```
In source build mode the script clones the Bitwarden server repository at the matching version tag, replaces the embedded license certificate, and compiles the `api` and `identity` images from scratch. This takes considerably longer but gives you full control over the build.
You may now simply create the file `/path/to/bwdata/docker/docker-compose.override.yml` with the following contents to utilize the modified images.
```yaml
services:
api:
image: bitbetter/api
pull_policy: never
identity:
image: bitbetter/identity
pull_policy: never
```
You'll also want to edit the `/path/to/bwdata/scripts/run.sh` file. In the `function restart()` block, comment out the call to `dockerComposePull`.
@@ -103,16 +89,6 @@ You can now start or restart Bitwarden as normal and the modified api will be us
To update Bitwarden, the provided `update-bitwarden.sh` script can be used. It will rebuild the BitBetter images and automatically update Bitwarden afterwards. Docker pull errors can be ignored for api and identity images.
By default, the images are built in **fast patch mode**: the official Bitwarden images are pulled from the GitHub Container Registry and the license certificate are directly patched inside them.
To instead **build from source**, set the `BITBETTER_BUILD_FROM_SOURCE` environment variable to `1` before running the script:
```bash
BITBETTER_BUILD_FROM_SOURCE=1 ./update-bitwarden.sh
```
In source build mode the script clones the Bitwarden server repository at the matching version tag, replaces the embedded license certificate, and compiles the `api` and `identity` images from scratch. This takes considerably longer but gives you full control over the build.
You can either run this script without providing any parameters in interactive mode (`./update-bitwarden.sh`) or by setting the parameters as follows, to run the script in non-interactive mode:
```bash
./update-bitwarden.sh param1 param2 param3

View File

@@ -6,88 +6,22 @@ BW_VERSION=$(curl -sL https://go.btwrdn.co/bw-sh-versions | grep '^ *"'coreVersi
echo "Building BitBetter for BitWarden version $BW_VERSION"
# Enable BuildKit for better build experience and to ensure platform args are populated
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Determine host architecture to use as default BUILDPLATFORM / TARGETPLATFORM if not supplied.
# Allow override via environment variables when invoking the script.
HOST_UNAME_ARCH=$(uname -m 2>/dev/null || echo unknown)
case "$HOST_UNAME_ARCH" in
x86_64|amd64) DEFAULT_ARCH=amd64 ;;
aarch64|arm64) DEFAULT_ARCH=arm64 ;;
armv7l|armv7) DEFAULT_ARCH=arm/v7 ;;
*) DEFAULT_ARCH=amd64 ;;
esac
: "${BUILDPLATFORM:=linux/${DEFAULT_ARCH}}"
: "${TARGETPLATFORM:=linux/${DEFAULT_ARCH}}"
echo "Using BUILDPLATFORM=$BUILDPLATFORM TARGETPLATFORM=$TARGETPLATFORM"
# If there aren't any keys, generate them first.
[ -e "$DIR/.keys/cert.cert" ] || "$DIR/.keys/generate-keys.sh"
if [ "${BITBETTER_BUILD_FROM_SOURCE:-0}" = "1" ]; then
echo "--- Source build mode ---"
# Prepare Bitwarden repository
rm -rf $DIR/server
git clone --branch "v${BW_VERSION}" --depth 1 https://github.com/bitwarden/server.git $DIR/server
old_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/server/src/Core/licensing.cer | cut -d= -f2 | tr -d ':')
new_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/.keys/cert.cert | cut -d= -f2 | tr -d ':')
cp $DIR/.keys/cert.cert $DIR/server/src/Core/licensing.cer
# Optional, has actually no effect
sed -i -e "s/$old_thumbprint/$new_thumbprint/g" $DIR/server/src/Core/Services/Implementations/LicensingService.cs
# Enable loose files for API, so Core.dll is accessible
sed -i -e 's/PublishSingleFile=true/PublishSingleFile=false/g' $DIR/server/src/Api/Dockerfile
# Prepare Bitwarden server repository
rm -rf $DIR/server
git clone --branch "v${BW_VERSION}" --depth 1 https://github.com/bitwarden/server.git $DIR/server
# Replace certificate file and thumbprint
old_thumbprint=$(openssl x509 -inform DER -fingerprint -noout -in $DIR/server/src/Core/licensing.cer | cut -d= -f2 | tr -d ':')
new_thumbprint=$(openssl x509 -inform DER -fingerprint -noout -in $DIR/.keys/cert.cert | cut -d= -f2 | tr -d ':')
sed -i -e "s/$old_thumbprint/$new_thumbprint/g" $DIR/server/src/Core/Billing/Services/Implementations/LicensingService.cs
cp $DIR/.keys/cert.cert $DIR/server/src/Core/licensing.cer
docker build \
--no-cache \
--platform "$TARGETPLATFORM" \
--build-arg BUILDPLATFORM="$BUILDPLATFORM" \
--build-arg TARGETPLATFORM="$TARGETPLATFORM" \
--label com.bitwarden.product="bitbetter" \
-f $DIR/server/src/Api/Dockerfile \
-t bitbetter/api \
$DIR/server
docker build \
--no-cache \
--platform "$TARGETPLATFORM" \
--build-arg BUILDPLATFORM="$BUILDPLATFORM" \
--build-arg TARGETPLATFORM="$TARGETPLATFORM" \
--label com.bitwarden.product="bitbetter" \
-f $DIR/server/src/Identity/Dockerfile \
-t bitbetter/identity \
$DIR/server
else
echo "--- Fast patch mode ---"
mkdir -p "$DIR/src/bitBetter/.keys"
cp "$DIR/.keys/cert.cert" "$DIR/src/bitBetter/.keys/cert.cert"
# Build the patcher tool inside the SDK container
docker run --rm \
-v "$DIR/src/bitBetter:/bitBetter" \
-w /bitBetter \
mcr.microsoft.com/dotnet/sdk:8.0 sh build.sh
docker build \
--no-cache \
--platform "$TARGETPLATFORM" \
--label com.bitwarden.product="bitbetter" \
--build-arg BITWARDEN_TAG="ghcr.io/bitwarden/api:$BW_VERSION" \
-t bitbetter/api \
"$DIR/src/bitBetter"
docker build \
--no-cache \
--platform "$TARGETPLATFORM" \
--label com.bitwarden.product="bitbetter" \
--build-arg BITWARDEN_TAG="ghcr.io/bitwarden/identity:$BW_VERSION" \
-t bitbetter/identity \
"$DIR/src/bitBetter"
fi
docker build --no-cache --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Api/Dockerfile -t bitbetter/api
docker build --no-cache --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Identity/Dockerfile -t bitbetter/identity
docker tag bitbetter/api bitbetter/api:latest
docker tag bitbetter/identity bitbetter/identity:latest
@@ -95,5 +29,5 @@ docker tag bitbetter/api bitbetter/api:$BW_VERSION
docker tag bitbetter/identity bitbetter/identity:$BW_VERSION
# Remove old instances of the image after a successful build.
ids=$( docker image ls --format="{{ .ID }} {{ .Tag }}" 'bitbetter/*' | grep -E -v -- "CREATED|latest|${BW_VERSION}" | awk '{ ids = (ids ? ids FS $1 : $1) } END { print ids }' )
ids=$( docker images bitbetter/* | grep -E -v -- "CREATED|latest|${BW_VERSION}" | awk '{ print $3 }' )
[ -n "$ids" ] && docker rmi $ids || true

View File

@@ -1,13 +0,0 @@
ARG BITWARDEN_TAG
FROM ${BITWARDEN_TAG}
COPY bin/Release/net8.0/publish/* /bitBetter/
COPY ./.keys/cert.cert /newLicensing.cer
RUN set -e; set -x; \
if [ -f /app/Api ]; then EXEC=Api; else EXEC=Identity; fi && \
dotnet /bitBetter/bitBetter.dll /newLicensing.cer /app/$EXEC && \
rm -f /app/$EXEC && \
printf '#!/bin/sh\nexec dotnet /app/%s.dll "$@"\n' "$EXEC" > /app/$EXEC && \
chmod +x /app/$EXEC && \
rm -rf /bitBetter /newLicensing.cer

View File

@@ -1,147 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using SingleFileExtractor.Core;
namespace BitwardenSelfLicensor
{
class Program
{
static int Main(string[] args)
{
string cerFile = args.Length >= 1 ? args[0] : "/newLicensing.cer";
string inputPath = args.Length >= 2 ? args[1] : "/app/Api";
string coreDllPath;
string extractDir = Path.GetDirectoryName(Path.GetFullPath(inputPath));
if (string.Equals(Path.GetExtension(inputPath), ".dll", StringComparison.OrdinalIgnoreCase))
{
// Input is already a direct Core.dll path — skip bundle extraction
coreDllPath = inputPath;
}
else
{
try
{
var reader = new ExecutableReader(inputPath);
reader.ExtractToDirectory(extractDir);
Console.WriteLine($"Extracted bundle to {extractDir}");
coreDllPath = Path.Combine(extractDir, "Core.dll");
// The extracted runtimeconfig.json is in self-contained format (no "framework" key).
// Running "dotnet App.dll" requires framework-dependent format; without it .NET looks
// for libhostpolicy.so in the app dir (which isn't there) and crashes.
FixRuntimeConfig(extractDir, Path.GetFileNameWithoutExtension(inputPath));
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: Failed to extract single-file bundle '{inputPath}': {ex.Message}");
return 1;
}
}
Console.WriteLine($"Patching: {coreDllPath}");
var certBytes = File.ReadAllBytes(cerFile);
var module = ModuleDefinition.ReadModule(new MemoryStream(File.ReadAllBytes(coreDllPath)));
// Replace embedded certificate resource
var existingRes = module.Resources
.OfType<EmbeddedResource>()
.FirstOrDefault(r => r.Name == "Bit.Core.licensing.cer");
if (existingRes == null)
{
Console.Error.WriteLine("ERROR: Embedded resource 'Bit.Core.licensing.cer' not found in Core.dll");
return 1;
}
Console.WriteLine($"Found resource: {existingRes.Name}");
module.Resources.Add(new EmbeddedResource("Bit.Core.licensing.cer", existingRes.Attributes, certBytes));
module.Resources.Remove(existingRes);
var existingCert = new X509Certificate2(existingRes.GetResourceData());
var newCert = new X509Certificate2(certBytes);
Console.WriteLine($"Old thumbprint: {existingCert.Thumbprint}");
Console.WriteLine($"New thumbprint: {newCert.Thumbprint}");
// Find LicensingService by class name (namespace-agnostic to handle renames)
var type = module.Types.FirstOrDefault(t => t.Name == "LicensingService");
if (type == null)
{
Console.Error.WriteLine("ERROR: LicensingService not found in Core.dll");
return 1;
}
Console.WriteLine($"Found: {type.FullName}");
var ctor = type.Resolve().GetConstructors().First();
var rewriter = ctor.Body.GetILProcessor();
// Use Contains() to handle the hidden Unicode LRM character (\u200E) that Bitwarden
// prepends to the production thumbprint string literal in LicensingService.cs
var instToReplace = ctor.Body.Instructions
.Where(i => i.OpCode == OpCodes.Ldstr)
.FirstOrDefault(i => ((string)i.Operand)
.Contains(existingCert.Thumbprint, StringComparison.OrdinalIgnoreCase));
if (instToReplace != null)
{
Console.WriteLine($"Replacing thumbprint Ldstr: '{instToReplace.Operand}'");
rewriter.Replace(instToReplace, Instruction.Create(OpCodes.Ldstr, newCert.Thumbprint));
}
else
{
Console.WriteLine("WARNING: Thumbprint Ldstr not found — cert resource replaced anyway");
}
module.Write(coreDllPath);
Console.WriteLine("Done.");
return 0;
}
// Converts a self-contained runtimeconfig.json to framework-dependent so the app can be
// launched with "dotnet App.dll" using the system-installed ASP.NET Core runtime.
static void FixRuntimeConfig(string dir, string appName)
{
var path = Path.Combine(dir, $"{appName}.runtimeconfig.json");
if (!File.Exists(path))
{
Console.WriteLine($"runtimeconfig not found at {path}, skipping");
return;
}
var root = JsonNode.Parse(File.ReadAllText(path))!;
var opts = root["runtimeOptions"]!.AsObject();
// Derive framework name/version from the self-contained includedFrameworks before removing it
string fwName = "Microsoft.AspNetCore.App";
string fwVersion = "8.0.0";
if (opts["includedFrameworks"] is JsonArray included && included.Count > 0)
{
var first = included[0]!.AsObject();
fwName = first["name"]?.GetValue<string>() ?? fwName;
fwVersion = first["version"]?.GetValue<string>() ?? fwVersion;
}
// Remove self-contained markers
opts.Remove("includedFrameworks");
// Add framework-dependent reference (LatestPatch allows compatibility across patch releases only)
opts["framework"] = new JsonObject
{
["name"] = fwName,
["version"] = fwVersion
};
opts["rollForward"] = "LatestPatch";
File.WriteAllText(path, root.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine($"Fixed runtimeconfig: {path}");
}
}
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
</ItemGroup>
</Project>

View File

@@ -1,7 +0,0 @@
#!/bin/bash
set -e
set -x
dotnet restore
dotnet publish -c Release -o bin/Release/net8.0/publish

View File

@@ -14,4 +14,4 @@ FROM bitbetter/api
COPY --from=build /licenseGen/bin/Release/net8.0/publish/* /app/
ENTRYPOINT [ "dotnet", "/app/licenseGen.dll", "--core", "/app/Core.dll", "--executable", "/app/Api", "--cert", "/cert.pfx" ]
ENTRYPOINT [ "dotnet", "/app/licenseGen.dll", "--core", "/app/Core.dll", "--cert", "/cert.pfx" ]

View File

@@ -1,43 +1,37 @@
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;
using System;
using System.IO;
using System.Linq;
using System.Runtime.Loader;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json;
public static class Program
namespace bitwardenSelfLicensor
{
class Program
{
public static int Main(string[] args)
static int Main(string[] args)
{
var app = new CommandLineApplication();
var app = new Microsoft.Extensions.CommandLineUtils.CommandLineApplication();
var cert = app.Option("--cert", "cert file", CommandOptionType.SingleValue);
var coreDll = app.Option("--core", "path to core dll", CommandOptionType.SingleValue);
var exec = app.Option("--executable", "path to Bitwarden single file executable", CommandOptionType.SingleValue);
bool ExecExists() => File.Exists(exec.Value());
bool CertExists() => File.Exists(cert.Value());
bool CoreExists() => File.Exists(coreDll.Value());
bool VerifyTopOptions() =>
!string.IsNullOrWhiteSpace(cert.Value()) &&
(!string.IsNullOrWhiteSpace(coreDll.Value()) || !string.IsNullOrWhiteSpace(exec.Value())) &&
CertExists() &&
(CoreExists() || ExecExists());
string GetExtractedDll()
bool certExists()
{
var coreDllPath = Path.Combine("extract", "Core.dll");
var reader = new ExecutableReader(exec.Value());
reader.ExtractToDirectory("extract");
var fileInfo = new FileInfo(coreDllPath);
return fileInfo.FullName;
return File.Exists(cert.Value());
}
bool coreExists()
{
return File.Exists(coreDll.Value());
}
bool verifyTopOptions()
{
return !string.IsNullOrWhiteSpace(cert.Value()) &&
!string.IsNullOrWhiteSpace(coreDll.Value()) &&
certExists() && coreExists();
}
string GetCoreDllPath() => CoreExists() ? coreDll.Value() : GetExtractedDll();
app.Command("interactive", config =>
{
@@ -49,11 +43,11 @@ namespace BitwardenSelfLicensor
config.OnExecute(() =>
{
if (!VerifyTopOptions())
if (!verifyTopOptions())
{
if (!ExecExists() && !string.IsNullOrWhiteSpace(exec.Value())) config.Error.WriteLine($"Cant find single file executable at: {exec.Value()}");
if (!CoreExists() && !string.IsNullOrWhiteSpace(coreDll.Value())) config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
if (!CertExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
if (!coreExists()) config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
if (!certExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
config.ShowHelp();
return 1;
}
@@ -98,7 +92,7 @@ namespace BitwardenSelfLicensor
WriteLineOver("Please enter a business name, default is BitBetter. [Business Name]:");
buff = Console.ReadLine();
if (buff == "") businessname = "BitBetter";
else if (CheckBusinessName(buff)) businessname = buff;
else if (checkBusinessName(buff)) businessname = buff;
}
}
else
@@ -111,14 +105,14 @@ namespace BitwardenSelfLicensor
{
WriteLineOver("Please provide the username this license will be registered to. [username]:");
buff = Console.ReadLine();
if ( CheckUsername(buff) ) name = buff;
if ( checkUsername(buff) ) name = buff;
}
while (email == "")
{
WriteLineOver("Please provide the email address for the user " + name + ". [email]");
buff = Console.ReadLine();
if ( CheckEmail(buff) ) email = buff;
if ( checkEmail(buff) ) email = buff;
}
while (storage == 0)
@@ -131,7 +125,7 @@ namespace BitwardenSelfLicensor
}
else
{
if (CheckStorage(buff)) storage = short.Parse(buff);
if (checkStorage(buff)) storage = short.Parse(buff);
}
}
@@ -141,7 +135,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" )
{
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, guid, null);
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, guid, null);
}
else
{
@@ -155,7 +149,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" )
{
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name, email, storage, installid, businessname, null);
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, installid, businessname, null);
}
else
{
@@ -179,11 +173,17 @@ namespace BitwardenSelfLicensor
config.OnExecute(() =>
{
if (!VerifyTopOptions())
if (!verifyTopOptions())
{
if (!ExecExists() && !string.IsNullOrWhiteSpace(exec.Value())) config.Error.WriteLine($"Cant find single file executable at: {exec.Value()}");
if (!CoreExists() && !string.IsNullOrWhiteSpace(coreDll.Value())) config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
if (!CertExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
if (!coreExists())
{
config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
}
if (!certExists())
{
config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
}
config.ShowHelp();
return 1;
}
@@ -214,7 +214,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage;
}
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, userId, key.Value);
GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name.Value, email.Value, storageShort, userId, key.Value);
return 0;
});
@@ -231,11 +231,17 @@ namespace BitwardenSelfLicensor
config.OnExecute(() =>
{
if (!VerifyTopOptions())
if (!verifyTopOptions())
{
if (!ExecExists() && !string.IsNullOrWhiteSpace(exec.Value())) config.Error.WriteLine($"Cant find single file executable at: {exec.Value()}");
if (!CoreExists() && !string.IsNullOrWhiteSpace(coreDll.Value())) config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
if (!CertExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
if (!coreExists())
{
config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
}
if (!certExists())
{
config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
}
config.ShowHelp();
return 1;
}
@@ -269,7 +275,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage;
}
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), GetCoreDllPath(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value);
GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name.Value, email.Value, storageShort, installationId, businessName.Value, key.Value);
return 0;
});
@@ -295,7 +301,7 @@ namespace BitwardenSelfLicensor
}
// checkUsername Checks that the username is a valid username
private static bool CheckUsername(string s)
static bool checkUsername(string s)
{
if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The username provided doesn't appear to be valid.\n");
@@ -305,7 +311,7 @@ namespace BitwardenSelfLicensor
}
// checkBusinessName Checks that the Business Name is a valid username
private static bool CheckBusinessName(string s)
static bool checkBusinessName(string s)
{
if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The Business Name provided doesn't appear to be valid.\n");
@@ -315,7 +321,7 @@ namespace BitwardenSelfLicensor
}
// checkEmail Checks that the email address is a valid email address
private static bool CheckEmail(string s)
static bool checkEmail(string s)
{
if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The email provided doesn't appear to be valid.\n");
@@ -325,7 +331,7 @@ namespace BitwardenSelfLicensor
}
// checkStorage Checks that the storage is in a valid range
private static bool CheckStorage(string s)
static bool checkStorage(string s)
{
if (string.IsNullOrWhiteSpace(s))
{
@@ -341,20 +347,23 @@ namespace BitwardenSelfLicensor
}
// WriteLineOver Writes a new line to console over last line.
private static void WriteLineOver(string s)
static void WriteLineOver(string s)
{
Console.SetCursorPosition(0, Console.CursorTop -1);
Console.WriteLine(s);
}
// WriteLine This wrapper is just here so that console writes all look similar.
private static void WriteLine(string s) => Console.WriteLine(s);
static void WriteLine(string s)
{
Console.WriteLine(s);
}
private static void GenerateUserLicense(X509Certificate2 cert, string corePath, string userName, string email, short storage, Guid userId, string key)
static void GenerateUserLicense(X509Certificate2 cert, string corePath, string userName, string email, short storage, Guid userId, string key)
{
var core = AssemblyLoadContext.Default.LoadFromAssemblyPath(corePath);
var type = core.GetType("Bit.Core.Billing.Models.Business.UserLicense");
var type = core.GetType("Bit.Core.Models.Business.UserLicense");
var licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType");
var license = Activator.CreateInstance(type);
@@ -364,34 +373,30 @@ namespace BitwardenSelfLicensor
type.GetProperty(name).SetValue(license, value);
}
var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key;
set("LicenseKey", licenseKey);
set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
set("Id", userId);
set("Name", userName);
set("Email", email);
set("Premium", true);
set("MaxStorageGb", storage == 0 ? short.MaxValue : storage);
set("Version", 1);
var now = DateTime.UtcNow;
set("Issued", now);
set("Refresh", now.AddYears(100).AddMonths(-1));
set("Expires", now.AddYears(100));
set("Issued", DateTime.UtcNow);
set("Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1));
set("Expires", DateTime.UtcNow.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 })));
Console.WriteLine(JsonConvert.SerializeObject(license, Formatting.Indented));
}
private static void GenerateOrgLicense(X509Certificate2 cert, string corePath, string userName, string email, short storage, Guid instalId, string businessName, string key)
static void GenerateOrgLicense(X509Certificate2 cert, string corePath, string userName, string email, short storage, Guid instalId, string businessName, string key)
{
var core = AssemblyLoadContext.Default.LoadFromAssemblyPath(corePath);
var type = core.GetType("Bit.Core.Billing.Organizations.Models.OrganizationLicense");
var type = core.GetType("Bit.Core.Models.Business.OrganizationLicense");
var licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType");
var planTypeEnum = core.GetType("Bit.Core.Billing.Enums.PlanType");
@@ -402,15 +407,12 @@ namespace BitwardenSelfLicensor
type.GetProperty(name).SetValue(license, value);
}
var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key;
var businessNameFinal = string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName;
set("LicenseKey", licenseKey);
set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
set("InstallationId", instalId);
set("Id", Guid.NewGuid());
set("Name", userName);
set("BillingEmail", email);
set("BusinessName", businessNameFinal);
set("BusinessName", string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName);
set("Enabled", true);
set("Plan", "Enterprise (Annually)");
set("PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually"));
@@ -435,134 +437,19 @@ namespace BitwardenSelfLicensor
set("UseSecretsManager", true);
set("SmSeats", int.MaxValue);
set("SmServiceAccounts", int.MaxValue);
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("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("Trial", false);
set("LicenseType", Enum.Parse(licenseTypeEnum, "Organization"));
set("LimitCollectionCreationDeletion", true);
set("LimitCollectionCreationDeletion", true); //This will be used in the new version of BitWarden but can be applied now
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<Claim>
{
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<Claim>
{
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);
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -7,10 +7,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
</ItemGroup>
</Project>