Compare commits

..

2 Commits

Author SHA1 Message Date
Genva
8fbd78dc65 Merge 1a2073cf35 into 676dd7b85c 2025-06-14 15:49:45 +02:00
Genva
1a2073cf35 Build image from source 2025-06-14 15:46:18 +02:00
10 changed files with 197 additions and 467 deletions

View File

@@ -13,12 +13,3 @@ jobs:
- run: - run:
name: Build script name: Build script
command: ./build.sh 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. 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!_ _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 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`. 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. 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 ```yaml
services: services:
api: api:
image: bitbetter/api image: bitbetter/api
pull_policy: never
identity: identity:
image: bitbetter/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`. 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. 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: 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 ```bash
./update-bitwarden.sh param1 param2 param3 ./update-bitwarden.sh param1 param2 param3

View File

@@ -6,88 +6,23 @@ BW_VERSION=$(curl -sL https://go.btwrdn.co/bw-sh-versions | grep '^ *"'coreVersi
echo "Building BitBetter for BitWarden version $BW_VERSION" 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. # If there aren't any keys, generate them first.
[ -e "$DIR/.keys/cert.cert" ] || "$DIR/.keys/generate-keys.sh" [ -e "$DIR/.keys/cert.cert" ] || "$DIR/.keys/generate-keys.sh"
if [ "${BITBETTER_BUILD_FROM_SOURCE:-0}" = "1" ]; then # Prepare Bitwarden repository
echo "--- Source build mode ---" rm -rf $DIR/server
git clone https://github.com/bitwarden/server.git
git -C $DIR/server checkout tags/v${BW_VERSION}
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 docker build --no-cache --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Api/Dockerfile -t bitbetter/api
rm -rf $DIR/server docker build --no-cache --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Identity/Dockerfile -t bitbetter/identity
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 tag bitbetter/api bitbetter/api:latest docker tag bitbetter/api bitbetter/api:latest
docker tag bitbetter/identity bitbetter/identity:latest docker tag bitbetter/identity bitbetter/identity:latest
@@ -95,5 +30,5 @@ docker tag bitbetter/api bitbetter/api:$BW_VERSION
docker tag bitbetter/identity bitbetter/identity:$BW_VERSION docker tag bitbetter/identity bitbetter/identity:$BW_VERSION
# Remove old instances of the image after a successful build. # 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 [ -n "$ids" ] && docker rmi $ids || true

View File

@@ -5,9 +5,7 @@ COPY bin/Release/net8.0/publish/* /bitBetter/
COPY ./.keys/cert.cert /newLicensing.cer COPY ./.keys/cert.cert /newLicensing.cer
RUN set -e; set -x; \ RUN set -e; set -x; \
if [ -f /app/Api ]; then EXEC=Api; else EXEC=Identity; fi && \ dotnet /bitBetter/bitBetter.dll && \
dotnet /bitBetter/bitBetter.dll /newLicensing.cer /app/$EXEC && \ mv /app/Core.dll /app/Core.orig.dll && \
rm -f /app/$EXEC && \ mv /app/modified.dll /app/Core.dll && \
printf '#!/bin/sh\nexec dotnet /app/%s.dll "$@"\n' "$EXEC" > /app/$EXEC && \ rm -rf /bitBetter && rm -rf /newLicensing.cer
chmod +x /app/$EXEC && \
rm -rf /bitBetter /newLicensing.cer

View File

@@ -1,147 +1,93 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text.Json.Nodes;
using Mono.Cecil; using Mono.Cecil;
using Mono.Cecil.Cil; using Mono.Cecil.Cil;
using Mono.Cecil.Rocks; using Mono.Cecil.Rocks;
using SingleFileExtractor.Core;
namespace BitwardenSelfLicensor namespace bitwardenSelfLicensor
{ {
class Program class Program
{ {
static int Main(string[] args) static int Main(string[] args)
{ {
string cerFile = args.Length >= 1 ? args[0] : "/newLicensing.cer"; string cerFile;
string inputPath = args.Length >= 2 ? args[1] : "/app/Api"; string corePath;
string coreDllPath; if(args.Length >= 2) {
string extractDir = Path.GetDirectoryName(Path.GetFullPath(inputPath)); cerFile = args[0];
corePath = args[1];
if (string.Equals(Path.GetExtension(inputPath), ".dll", StringComparison.OrdinalIgnoreCase)) } else if (args.Length == 1) {
{ cerFile = args[0];
// Input is already a direct Core.dll path — skip bundle extraction corePath = "/app/Core.dll";
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;
} }
else {
cerFile = "/newLicensing.cer";
corePath = "/app/Core.dll";
} }
Console.WriteLine($"Patching: {coreDllPath}");
var certBytes = File.ReadAllBytes(cerFile); var module = ModuleDefinition.ReadModule(new MemoryStream(File.ReadAllBytes(corePath)));
var module = ModuleDefinition.ReadModule(new MemoryStream(File.ReadAllBytes(coreDllPath))); var cert = File.ReadAllBytes(cerFile);
// Replace embedded certificate resource var x = module.Resources.OfType<EmbeddedResource>()
var existingRes = module.Resources .Where(r => r.Name.Equals("Bit.Core.licensing.cer"))
.OfType<EmbeddedResource>() .First();
.FirstOrDefault(r => r.Name == "Bit.Core.licensing.cer");
if (existingRes == null) Console.WriteLine(x.Name);
{
Console.Error.WriteLine("ERROR: Embedded resource 'Bit.Core.licensing.cer' not found in Core.dll");
return 1;
}
Console.WriteLine($"Found resource: {existingRes.Name}"); var e = new EmbeddedResource("Bit.Core.licensing.cer", x.Attributes, cert);
module.Resources.Add(new EmbeddedResource("Bit.Core.licensing.cer", existingRes.Attributes, certBytes));
module.Resources.Remove(existingRes);
var existingCert = new X509Certificate2(existingRes.GetResourceData()); module.Resources.Add(e);
var newCert = new X509Certificate2(certBytes); module.Resources.Remove(x);
Console.WriteLine($"Old thumbprint: {existingCert.Thumbprint}");
Console.WriteLine($"New thumbprint: {newCert.Thumbprint}"); var services = module.Types.Where(t => t.Namespace == "Bit.Core.Services");
var type = services.First(t => t.Name == "LicensingService");
var licensingType = type.Resolve();
var existingCert = new X509Certificate2(x.GetResourceData());
Console.WriteLine($"Existing Cert Thumbprint: {existingCert.Thumbprint}");
X509Certificate2 certificate = new X509Certificate2(cert);
Console.WriteLine($"New Cert Thumbprint: {certificate.Thumbprint}");
var ctor = licensingType.GetConstructors().Single();
// 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(); var rewriter = ctor.Body.GetILProcessor();
// Use Contains() to handle the hidden Unicode LRM character (\u200E) that Bitwarden var instToReplace =
// prepends to the production thumbprint string literal in LicensingService.cs ctor.Body.Instructions.Where(i => i.OpCode == OpCodes.Ldstr
var instToReplace = ctor.Body.Instructions && string.Equals((string)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase))
.Where(i => i.OpCode == OpCodes.Ldstr) .FirstOrDefault();
.FirstOrDefault(i => ((string)i.Operand)
.Contains(existingCert.Thumbprint, StringComparison.OrdinalIgnoreCase));
if (instToReplace != null) if(instToReplace != null) {
{ rewriter.Replace(instToReplace, Instruction.Create(OpCodes.Ldstr, certificate.Thumbprint));
Console.WriteLine($"Replacing thumbprint Ldstr: '{instToReplace.Operand}'");
rewriter.Replace(instToReplace, Instruction.Create(OpCodes.Ldstr, newCert.Thumbprint));
} }
else else {
{ Console.WriteLine("Cant find inst");
Console.WriteLine("WARNING: Thumbprint Ldstr not found — cert resource replaced anyway");
} }
module.Write(coreDllPath); // foreach (var inst in ctor.Body.Instructions)
Console.WriteLine("Done."); // {
// Console.Write(inst.OpCode.Name + " " + inst.Operand?.GetType() + " = ");
// if(inst.OpCode.FlowControl == FlowControl.Call) {
// Console.WriteLine(inst.Operand);
// }
// else if(inst.OpCode == OpCodes.Ldstr) {
// Console.WriteLine(inst.Operand);
// }
// else {Console.WriteLine();}
// }
module.Write("modified.dll");
return 0; 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

@@ -7,7 +7,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.2" /> <PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,4 +4,4 @@ set -e
set -x set -x
dotnet restore dotnet restore
dotnet publish -c Release -o bin/Release/net8.0/publish dotnet publish

View File

@@ -14,4 +14,4 @@ FROM bitbetter/api
COPY --from=build /licenseGen/bin/Release/net8.0/publish/* /app/ 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 System;
{ using System.IO;
using Microsoft.Extensions.CommandLineUtils; using System.Linq;
using Microsoft.IdentityModel.Tokens; using System.Runtime.Loader;
using Newtonsoft.Json; using System.Security.Cryptography.X509Certificates;
using SingleFileExtractor.Core; using Microsoft.Extensions.CommandLineUtils;
using System; using Newtonsoft.Json;
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 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 cert = app.Option("--cert", "cert file", CommandOptionType.SingleValue);
var coreDll = app.Option("--core", "path to core dll", 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()
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()
{ {
var coreDllPath = Path.Combine("extract", "Core.dll"); return File.Exists(cert.Value());
var reader = new ExecutableReader(exec.Value()); }
reader.ExtractToDirectory("extract");
var fileInfo = new FileInfo(coreDllPath); bool coreExists()
return fileInfo.FullName; {
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 => app.Command("interactive", config =>
{ {
@@ -49,11 +43,11 @@ namespace BitwardenSelfLicensor
config.OnExecute(() => 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()) config.Error.WriteLine($"Cant find core dll at: {coreDll.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 (!CertExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
config.ShowHelp(); config.ShowHelp();
return 1; return 1;
} }
@@ -98,7 +92,7 @@ namespace BitwardenSelfLicensor
WriteLineOver("Please enter a business name, default is BitBetter. [Business Name]:"); WriteLineOver("Please enter a business name, default is BitBetter. [Business Name]:");
buff = Console.ReadLine(); buff = Console.ReadLine();
if (buff == "") businessname = "BitBetter"; if (buff == "") businessname = "BitBetter";
else if (CheckBusinessName(buff)) businessname = buff; else if (checkBusinessName(buff)) businessname = buff;
} }
} }
else else
@@ -111,14 +105,14 @@ namespace BitwardenSelfLicensor
{ {
WriteLineOver("Please provide the username this license will be registered to. [username]:"); WriteLineOver("Please provide the username this license will be registered to. [username]:");
buff = Console.ReadLine(); buff = Console.ReadLine();
if ( CheckUsername(buff) ) name = buff; if ( checkUsername(buff) ) name = buff;
} }
while (email == "") while (email == "")
{ {
WriteLineOver("Please provide the email address for the user " + name + ". [email]"); WriteLineOver("Please provide the email address for the user " + name + ". [email]");
buff = Console.ReadLine(); buff = Console.ReadLine();
if ( CheckEmail(buff) ) email = buff; if ( checkEmail(buff) ) email = buff;
} }
while (storage == 0) while (storage == 0)
@@ -131,7 +125,7 @@ namespace BitwardenSelfLicensor
} }
else else
{ {
if (CheckStorage(buff)) storage = short.Parse(buff); if (checkStorage(buff)) storage = short.Parse(buff);
} }
} }
@@ -141,7 +135,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine(); buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" ) 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 else
{ {
@@ -155,7 +149,7 @@ namespace BitwardenSelfLicensor
buff = Console.ReadLine(); buff = Console.ReadLine();
if ( buff == "" || buff == "y" || buff == "Y" ) 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 else
{ {
@@ -179,11 +173,17 @@ namespace BitwardenSelfLicensor
config.OnExecute(() => 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())
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()}"); config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
}
if (!certExists())
{
config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
}
config.ShowHelp(); config.ShowHelp();
return 1; return 1;
} }
@@ -214,7 +214,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage; 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; return 0;
}); });
@@ -231,11 +231,17 @@ namespace BitwardenSelfLicensor
config.OnExecute(() => 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())
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()}"); config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}");
}
if (!certExists())
{
config.Error.WriteLine($"Cant find certificate at: {cert.Value()}");
}
config.ShowHelp(); config.ShowHelp();
return 1; return 1;
} }
@@ -269,7 +275,7 @@ namespace BitwardenSelfLicensor
storageShort = (short) parsedStorage; 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; return 0;
}); });
@@ -295,7 +301,7 @@ namespace BitwardenSelfLicensor
} }
// checkUsername Checks that the username is a valid username // checkUsername Checks that the username is a valid username
private static bool CheckUsername(string s) static bool checkUsername(string s)
{ {
if ( string.IsNullOrWhiteSpace(s) ) { if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The username provided doesn't appear to be valid.\n"); 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 // 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) ) { if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The Business Name provided doesn't appear to be valid.\n"); 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 // 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) ) { if ( string.IsNullOrWhiteSpace(s) ) {
WriteLineOver("The email provided doesn't appear to be valid.\n"); 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 // 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)) if (string.IsNullOrWhiteSpace(s))
{ {
@@ -341,20 +347,23 @@ namespace BitwardenSelfLicensor
} }
// WriteLineOver Writes a new line to console over last line. // 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.SetCursorPosition(0, Console.CursorTop -1);
Console.WriteLine(s); Console.WriteLine(s);
} }
// WriteLine This wrapper is just here so that console writes all look similar. // 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 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 licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType");
var license = Activator.CreateInstance(type); var license = Activator.CreateInstance(type);
@@ -364,34 +373,30 @@ namespace BitwardenSelfLicensor
type.GetProperty(name).SetValue(license, value); type.GetProperty(name).SetValue(license, value);
} }
var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
set("LicenseKey", licenseKey);
set("Id", userId); set("Id", userId);
set("Name", userName); set("Name", userName);
set("Email", email); set("Email", email);
set("Premium", true); set("Premium", true);
set("MaxStorageGb", storage == 0 ? short.MaxValue : storage); set("MaxStorageGb", storage == 0 ? short.MaxValue : storage);
set("Version", 1); set("Version", 1);
var now = DateTime.UtcNow; set("Issued", DateTime.UtcNow);
set("Issued", now); set("Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1));
set("Refresh", now.AddYears(100).AddMonths(-1)); set("Expires", DateTime.UtcNow.AddYears(100));
set("Expires", now.AddYears(100));
set("Trial", false); set("Trial", false);
set("LicenseType", Enum.Parse(licenseTypeEnum, "User")); 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("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 }))); set("Signature", Convert.ToBase64String((byte[])type.GetMethod("Sign").Invoke(license, new object[] { cert })));
Console.WriteLine(JsonConvert.SerializeObject(license, Formatting.Indented)); 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 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 licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType");
var planTypeEnum = core.GetType("Bit.Core.Billing.Enums.PlanType"); var planTypeEnum = core.GetType("Bit.Core.Billing.Enums.PlanType");
@@ -402,15 +407,12 @@ namespace BitwardenSelfLicensor
type.GetProperty(name).SetValue(license, value); type.GetProperty(name).SetValue(license, value);
} }
var licenseKey = string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key; set("LicenseKey", string.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
var businessNameFinal = string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName;
set("LicenseKey", licenseKey);
set("InstallationId", instalId); set("InstallationId", instalId);
set("Id", Guid.NewGuid()); set("Id", Guid.NewGuid());
set("Name", userName); set("Name", userName);
set("BillingEmail", email); set("BillingEmail", email);
set("BusinessName", businessNameFinal); set("BusinessName", string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName);
set("Enabled", true); set("Enabled", true);
set("Plan", "Enterprise (Annually)"); set("Plan", "Enterprise (Annually)");
set("PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually")); set("PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually"));
@@ -435,134 +437,19 @@ namespace BitwardenSelfLicensor
set("UseSecretsManager", true); set("UseSecretsManager", true);
set("SmSeats", int.MaxValue); set("SmSeats", int.MaxValue);
set("SmServiceAccounts", int.MaxValue); set("SmServiceAccounts", int.MaxValue);
set("Version", 16); set("Version", 15); //This is set to 15 to use AllowAdminAccessToAllCollectionItems can be changed to 13 to just use Secrets Manager
var now = DateTime.UtcNow; set("Issued", DateTime.UtcNow);
set("Issued", now); set("Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1));
set("Refresh", now.AddYears(100).AddMonths(-1)); set("Expires", DateTime.UtcNow.AddYears(100));
set("Expires", now.AddYears(100));
set("ExpirationWithoutGracePeriod", now.AddYears(100));
set("Trial", false); set("Trial", false);
set("LicenseType", Enum.Parse(licenseTypeEnum, "Organization")); 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("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("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 }))); set("Signature", Convert.ToBase64String((byte[])type.GetMethod("Sign").Invoke(license, new object[] { cert })));
Console.WriteLine(JsonConvert.SerializeObject(license, Formatting.Indented)); 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> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
@@ -7,10 +7,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" /> <PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>