mirror of
https://github.com/jakeswenson/BitBetter.git
synced 2026-06-06 19:43:39 +00:00
Compare commits
50 Commits
lite
...
8bcf9b7699
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bcf9b7699 | ||
|
|
594d1bd4ab | ||
|
|
be9d7396dc | ||
|
|
a6516572f6 | ||
|
|
29883a60f6 | ||
|
|
7007356b9b | ||
|
|
d7430bddd7 | ||
|
|
2cc20501a0 | ||
|
|
7ca7db3932 | ||
|
|
1d268957fd | ||
|
|
b233e55a6e | ||
|
|
f059d756f7 | ||
|
|
08a6b94d47 | ||
|
|
70517634a5 | ||
|
|
99148f6faf | ||
|
|
6fbcf13b7f | ||
|
|
ca2815411f | ||
|
|
4341ad3beb | ||
|
|
1d3bbbcd92 | ||
|
|
5cff265a2a | ||
|
|
a527d425fd | ||
|
|
f9055c711a | ||
|
|
1871df136b | ||
|
|
fe7ac2131e | ||
|
|
874ff182c6 | ||
|
|
b75dfb2512 | ||
|
|
232de042dd | ||
|
|
b12b470656 | ||
|
|
d07f030d9a | ||
|
|
1ee45a327f | ||
|
|
ab8eed492c | ||
|
|
7203354204 | ||
|
|
9ca720af8c | ||
|
|
6ba14a7134 | ||
|
|
d8098cb560 | ||
|
|
c72fbf5b1c | ||
|
|
0b0512570f | ||
|
|
dfc364e7f3 | ||
|
|
48c67fc66e | ||
|
|
80fcf0cfc6 | ||
|
|
e87bc81a9c | ||
|
|
93cae61d66 | ||
|
|
52fabd9a95 | ||
|
|
ddf67ec706 | ||
|
|
7786c4406c | ||
|
|
f360f54e46 | ||
|
|
273ac7b4eb | ||
|
|
d34041c1e3 | ||
|
|
de61195d19 | ||
|
|
a97f6f3e49 |
@@ -1,19 +0,0 @@
|
||||
root=true
|
||||
|
||||
###############################
|
||||
# Core EditorConfig Options #
|
||||
###############################
|
||||
# All files
|
||||
[*]
|
||||
indent_style=tab
|
||||
indent_size=4
|
||||
trim_trailing_whitespace=true
|
||||
end_of_line=lf
|
||||
charset=utf-8
|
||||
|
||||
[*.{cs}]
|
||||
insert_final_newline=false
|
||||
|
||||
[*.{md,mkdn}]
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text eol=lf
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,3 @@ src/bitBetter/.vs/*
|
||||
*.pfx
|
||||
*.cer
|
||||
*.vsidx
|
||||
.DS_Store
|
||||
|
||||
27
README.md
27
README.md
@@ -1,14 +1,12 @@
|
||||
# BitBetter lite
|
||||
# BitBetter
|
||||
|
||||
BitBetter is is a tool to modify Bitwarden's core dll to allow you to generate your own individual and organisation licenses.
|
||||
|
||||
Please see the FAQ below for details on why this software was created.
|
||||
|
||||
Be aware that this branch is **only** for the lite (formerly unified) version of bitwarden. It has been rewritten and works in different ways than the master branch.
|
||||
_Beware! BitBetter does some semi janky stuff to rewrite the bitwarden core dll and allow the installation of a self signed certificate. Use at your own risk!_
|
||||
|
||||
_Beware! BitBetter is a solution that generates a personal certificate and uses that to generate custom licences. This requires (automated) modifying of libraries. Use at your own risk!_
|
||||
|
||||
Credit to https://github.com/h44z/BitBetter and https://github.com/jakeswenson/BitBetter and https://github.com/GieltjE/BitBetter
|
||||
Credit to https://github.com/h44z/BitBetter and https://github.com/jakeswenson/BitBetter
|
||||
|
||||
# Table of Contents
|
||||
- [BitBetter](#bitbetter)
|
||||
@@ -32,14 +30,14 @@ The following instructions are for unix-based systems (Linux, BSD, macOS) and Wi
|
||||
## Dependencies
|
||||
Aside from docker, which you also need for Bitwarden, BitBetter requires the following:
|
||||
|
||||
* Bitwarden (tested with 2025.11.1 might work on lower versions), for safety always stay up to date
|
||||
* Bitwarden (tested with 1.47.1, might work on lower versions)
|
||||
* openssl (probably already installed on most Linux or WSL systems, any version should work, on Windows it will be auto installed using winget)
|
||||
|
||||
## Setting up BitBetter
|
||||
With your dependencies installed, begin the installation of BitBetter by downloading it through Github or using the git command:
|
||||
|
||||
```
|
||||
git clone https://github.com/jakeswenson/BitBetter.git -b lite
|
||||
git clone https://github.com/jakeswenson/BitBetter.git
|
||||
```
|
||||
|
||||
### Optional: Manually generating Certificate & Key
|
||||
@@ -65,14 +63,14 @@ The scripts supports running and patching multi instances.
|
||||
Edit the .servers/serverlist.txt file and fill in the missing values, they can be replaced with existing installation values.
|
||||
This file may be empty, but there will be no containers will be spun up automatically.
|
||||
|
||||
Now it is time to **run the main build script** to generate a modified version of the `ghcr.io/bitwarden/lite` docker image and the license generator.
|
||||
Now it is time to **run the main build script** to generate a modified version of the `ghcr.io/bitwarden/self-host` docker image and the license generator.
|
||||
|
||||
From the BitBetter directory, simply run:
|
||||
```
|
||||
./build.[sh|ps1]
|
||||
```
|
||||
|
||||
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 `ghcr.io/bitwarden/lite` image called `bitwarden-patched`.
|
||||
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 `ghcr.io/bitwarden/self-host` image called `bitwarden-patched`.
|
||||
|
||||
Afterwards it will automatically generate the license generator and start all previously specified containers which are **now ready to accept self-issued licenses.**
|
||||
|
||||
@@ -102,7 +100,7 @@ If you ran the build script, you can **simply run the license gen in interactive
|
||||
|
||||
## Migrating from mssql to a real database
|
||||
|
||||
Prepare a new database and bwdata directory, download and prepare the new settings.env (https://raw.githubusercontent.com/bitwarden/self-host/refs/heads/main/bitwarden-lite/settings.env)
|
||||
Prepare a new database and bwdata directory, download and prepare the new settings.env (https://raw.githubusercontent.com/bitwarden/self-host/refs/heads/main/docker-unified/settings.env)
|
||||
|
||||
Make sure you can get the data from either the backup file or by connecting directly to the mssql database (navicat has a trial).
|
||||
|
||||
@@ -158,15 +156,6 @@ docker exec bitwarden ln -s /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime
|
||||
|
||||
Require a recreation of the docker container, build.sh will suffice too.
|
||||
|
||||
## Migrating from the old unified branch
|
||||
|
||||
```
|
||||
git branch -m unified lite
|
||||
git fetch origin
|
||||
git branch -u origin/lite lite
|
||||
git remote set-head origin -a
|
||||
```
|
||||
|
||||
# Footnotes
|
||||
|
||||
<a name="#f1"><sup>1</sup></a>This tool builds on top of the `bitbetter/api` container image so make sure you've built that above using the root `./build.sh` script.
|
||||
|
||||
30
build.ps1
30
build.ps1
@@ -4,10 +4,6 @@ $PSNativeCommandUseErrorActionPreference = $true
|
||||
# detect buildx, ErrorActionPreference will ensure the script stops execution if not found
|
||||
docker buildx version
|
||||
|
||||
# Enable BuildKit for better build experience and to ensure platform args are populated
|
||||
$env:DOCKER_BUILDKIT=1
|
||||
$env:COMPOSE_DOCKER_CLI_BUILD=1
|
||||
|
||||
# define temporary directory
|
||||
$tempdirectory = "$pwd\temp"
|
||||
# define services to patch
|
||||
@@ -57,11 +53,11 @@ foreach ($instance in $oldinstances) {
|
||||
|
||||
# update bitwarden itself
|
||||
if ($args[0] -eq 'update') {
|
||||
docker pull ghcr.io/bitwarden/lite:latest
|
||||
docker pull ghcr.io/bitwarden/self-host:beta
|
||||
} else {
|
||||
$confirmation = Read-Host "Update (or get) bitwarden source container (y/n)"
|
||||
if ($confirmation -eq 'y') {
|
||||
docker pull ghcr.io/bitwarden/lite:latest
|
||||
docker pull ghcr.io/bitwarden/self-host:beta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +80,7 @@ foreach ($instance in $oldinstances) {
|
||||
}
|
||||
|
||||
# start a new bitwarden instance so we can patch it
|
||||
$patchinstance = docker run -d --name bitwarden-extract ghcr.io/bitwarden/lite:latest
|
||||
$patchinstance = docker run -d --name bitwarden-extract ghcr.io/bitwarden/self-host:beta
|
||||
|
||||
# create our temporary directory
|
||||
New-item -ItemType Directory -Path $tempdirectory
|
||||
@@ -92,8 +88,7 @@ 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/$component "$tempdirectory\$component\$component"
|
||||
docker cp $patchinstance`:/etc/supervisor.d/$($component.ToLower()).ini "$tempdirectory\$($component.ToLower()).ini"
|
||||
docker cp $patchinstance`:/app/$component/Core.dll "$tempdirectory\$component\Core.dll"
|
||||
}
|
||||
|
||||
# stop and remove our temporary container
|
||||
@@ -104,25 +99,12 @@ docker rm bitwarden-extract
|
||||
docker run -v "$tempdirectory`:/app/mount" --rm bitbetter/bitbetter
|
||||
|
||||
# create a new image with the patched files
|
||||
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
|
||||
docker build . --tag bitwarden-patched --file "$pwd\src\bitBetter\Dockerfile-bitwarden-patch"
|
||||
|
||||
# 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 ((-not ($line.StartsWith("#"))) -and (-not [string]::IsNullOrWhiteSpace($line))) {
|
||||
if (!($line.StartsWith("#"))) {
|
||||
Invoke-Expression "& $line"
|
||||
}
|
||||
}
|
||||
|
||||
30
build.sh
30
build.sh
@@ -4,10 +4,6 @@ set -e
|
||||
# detect buildx, set -e will ensure the script stops execution if not found
|
||||
docker buildx version
|
||||
|
||||
# Enable BuildKit for better build experience and to ensure platform args are populated
|
||||
export DOCKER_BUILDKIT=1
|
||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
||||
|
||||
# define temporary directory
|
||||
TEMPDIRECTORY="$PWD/temp"
|
||||
|
||||
@@ -58,11 +54,11 @@ done
|
||||
|
||||
# update bitwarden itself
|
||||
if [ "$1" = "update" ]; then
|
||||
docker pull ghcr.io/bitwarden/lite:latest
|
||||
docker pull ghcr.io/bitwarden/self-host:beta
|
||||
else
|
||||
read -p "Update (or get) bitwarden source container (y/n): "
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
docker pull ghcr.io/bitwarden/lite:latest
|
||||
docker pull ghcr.io/bitwarden/self-host:beta
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -85,7 +81,7 @@ for INSTANCE in ${OLDINSTANCES[@]}; do
|
||||
done
|
||||
|
||||
# start a new bitwarden instance so we can patch it
|
||||
PATCHINSTANCE=$(docker run -d --name bitwarden-extract ghcr.io/bitwarden/lite:latest)
|
||||
PATCHINSTANCE=$(docker run -d --name bitwarden-extract ghcr.io/bitwarden/self-host:beta)
|
||||
|
||||
# create our temporary directory
|
||||
mkdir $TEMPDIRECTORY
|
||||
@@ -93,8 +89,7 @@ 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/$COMPONENT "$TEMPDIRECTORY/$COMPONENT/$COMPONENT"
|
||||
docker cp $PATCHINSTANCE:/etc/supervisor.d/${COMPONENT,,}.ini "$TEMPDIRECTORY/${COMPONENT,,}.ini"
|
||||
docker cp $PATCHINSTANCE:/app/$COMPONENT/Core.dll "$TEMPDIRECTORY/$COMPONENT/Core.dll"
|
||||
done
|
||||
|
||||
# stop and remove our temporary container
|
||||
@@ -105,27 +100,14 @@ docker rm bitwarden-extract
|
||||
docker run -v "$TEMPDIRECTORY:/app/mount" --rm bitbetter/bitbetter
|
||||
|
||||
# create a new image with the patched files
|
||||
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"
|
||||
docker build . --tag bitwarden-patched --file "$PWD/src/bitBetter/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 != "#"* && -n $LINE ]]; then
|
||||
if [[ $LINE != "#"* ]]; then
|
||||
bash -c "$LINE"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -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 -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256"
|
||||
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"
|
||||
@@ -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 -certpbe AES-256-CBC -keypbe AES-256-CBC -macalg SHA256
|
||||
openssl pkcs12 -export -out "$DIR/cert.pfx" -inkey "$DIR/key.pem" -in "$DIR/cert.pem" -passin pass:test -passout pass:test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /bitBetter
|
||||
|
||||
COPY . /bitBetter
|
||||
@@ -7,7 +7,7 @@ COPY cert.cer /app/
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
|
||||
|
||||
3
src/bitBetter/Dockerfile-bitwarden-patch
Normal file
3
src/bitBetter/Dockerfile-bitwarden-patch
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM ghcr.io/bitwarden/self-host:beta
|
||||
|
||||
COPY ./temp/ /app/
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
|
||||
@@ -16,46 +15,12 @@ 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 iniFile in Directory.GetFiles("/app/mount/", "*.ini", SearchOption.TopDirectoryOnly))
|
||||
foreach (String file in files)
|
||||
{
|
||||
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);
|
||||
Console.WriteLine(file);
|
||||
ModuleDefMD moduleDefMd = ModuleDefMD.Load(file);
|
||||
Byte[] cert = File.ReadAllBytes(certFile);
|
||||
|
||||
EmbeddedResource embeddedResourceToRemove = moduleDefMd.Resources.OfType<EmbeddedResource>().First(r => r.Name.Equals("Bit.Core.licensing.cer"));
|
||||
@@ -64,77 +29,37 @@ internal class Program
|
||||
moduleDefMd.Resources.Remove(embeddedResourceToRemove);
|
||||
|
||||
DataReader reader = embeddedResourceToRemove.CreateReader();
|
||||
X509Certificate2 existingCert = new(reader.ReadRemainingBytes());
|
||||
|
||||
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($"Existing Cert Thumbprint: {existingCert.Thumbprint}");
|
||||
X509Certificate2 certificate = new(cert);
|
||||
|
||||
// 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)
|
||||
{
|
||||
Console.Error.WriteLine("ERROR: LicensingService class not found");
|
||||
return -1;
|
||||
}
|
||||
Console.WriteLine($"Found: {type.FullName}");
|
||||
Console.WriteLine($"New Cert Thumbprint: {certificate.Thumbprint}");
|
||||
|
||||
IEnumerable<TypeDef> services = moduleDefMd.Types.Where(t => t.Namespace == "Bit.Core.Billing.Services");
|
||||
TypeDef type = services.First(t => t.Name == "LicensingService");
|
||||
MethodDef constructor = type.FindConstructors().First();
|
||||
|
||||
if (constructor == null)
|
||||
{
|
||||
Console.Error.WriteLine("ERROR: Cannot find constructor");
|
||||
return -1;
|
||||
}
|
||||
Instruction instructionToPatch = constructor.Body.Instructions.FirstOrDefault(i => i.OpCode == OpCodes.Ldstr && String.Equals((String)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
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)
|
||||
if (instructionToPatch != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
instructionToPatch.Operand = certificate.Thumbprint;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("ERROR: Can't find instruction to patch");
|
||||
return -1;
|
||||
Console.WriteLine("Can't find constructor to patch");
|
||||
}
|
||||
|
||||
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(newCoreDll + ".new");
|
||||
moduleDefMd.Write(file + ".new");
|
||||
moduleDefMd.Dispose();
|
||||
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));
|
||||
File.Delete(file);
|
||||
File.Move(file + ".new", file);
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dnlib" Version="4.5.0" />
|
||||
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /licenseGen
|
||||
|
||||
COPY . /licenseGen
|
||||
@@ -8,7 +8,7 @@ COPY cert.pfx /app/
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app --no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
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 System.Text.Json;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using McMaster.Extensions.CommandLineUtils;
|
||||
|
||||
namespace licenseGen;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private static readonly CommandLineApplication App = new();
|
||||
private static readonly CommandOption Cert = App.Option("--cert", "Certificate file", CommandOptionType.SingleValue);
|
||||
private static readonly CommandOption Cert = App.Option("--cert", "Certifcate file", CommandOptionType.SingleValue);
|
||||
private static readonly CommandOption CoreDll = App.Option("--core", "Path to Core.dll", CommandOptionType.SingleValue);
|
||||
|
||||
private static Int32 Main(String[] args)
|
||||
@@ -131,7 +127,7 @@ internal class Program
|
||||
buff = Console.ReadLine();
|
||||
if (buff is "" or "y" or "Y")
|
||||
{
|
||||
GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, guid, null);
|
||||
GenerateUserLicense(new X509Certificate2(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, guid, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -147,7 +143,7 @@ internal class Program
|
||||
buff = Console.ReadLine();
|
||||
if (buff is "" or "y" or "Y")
|
||||
{
|
||||
GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, installid, businessName, null);
|
||||
GenerateOrgLicense(new X509Certificate2(Cert.Value(), "test"), CoreDll.Value(), name, email, storage, installid, businessName, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -201,7 +197,7 @@ internal class Program
|
||||
storageShort = (Int16) parsedStorage;
|
||||
}
|
||||
|
||||
GenerateUserLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value()!, "test"), CoreDll.Value(), 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;
|
||||
});
|
||||
@@ -247,7 +243,7 @@ internal class Program
|
||||
storageShort = (Int16)parsedStorage;
|
||||
}
|
||||
|
||||
GenerateOrgLicense(X509CertificateLoader.LoadPkcs12FromFile(Cert.Value()!, "test"), CoreDll.Value(), 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;
|
||||
});
|
||||
@@ -273,30 +269,23 @@ internal class Program
|
||||
|
||||
private static void Check()
|
||||
{
|
||||
if (Cert == null || String.IsNullOrWhiteSpace(Cert.Value()))
|
||||
{
|
||||
App.Error.WriteLine("No certificate specified");
|
||||
App.ShowHelp();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
else if (CoreDll == null || String.IsNullOrWhiteSpace(CoreDll.Value()))
|
||||
{
|
||||
App.Error.WriteLine("No core dll specified");
|
||||
App.ShowHelp();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
else if (!File.Exists(Cert.Value()))
|
||||
if (!File.Exists(Cert.Value()))
|
||||
{
|
||||
App.Error.WriteLine($"Can't find certificate at: {Cert.Value()}");
|
||||
App.ShowHelp();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
else if (!File.Exists(CoreDll.Value()))
|
||||
if (!File.Exists(CoreDll.Value()))
|
||||
{
|
||||
App.Error.WriteLine($"Can't find core dll at: {CoreDll.Value()}");
|
||||
App.ShowHelp();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
if (Cert == null || String.IsNullOrWhiteSpace(Cert.Value()) || CoreDll == null || String.IsNullOrWhiteSpace(CoreDll.Value()))
|
||||
{
|
||||
App.ShowHelp();
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// checkUsername Checks that the username is a valid username
|
||||
@@ -379,28 +368,24 @@ internal class Program
|
||||
return;
|
||||
}
|
||||
|
||||
String licenseKey = String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key;
|
||||
Set(type, license, "LicenseKey", licenseKey);
|
||||
Set(type, license, "LicenseKey", String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
|
||||
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);
|
||||
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, "Issued", DateTime.UtcNow);
|
||||
Set(type, license, "Refresh", DateTime.UtcNow.AddYears(100).AddMonths(-1));
|
||||
Set(type, license, "Expires", DateTime.UtcNow.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 installId, String businessName, String key)
|
||||
private static void GenerateOrgLicense(X509Certificate2 cert, String corePath, String userName, String email, Int16 storage, Guid instalId, String businessName, String key)
|
||||
{
|
||||
Assembly core = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(corePath));
|
||||
Type type = core.GetType("Bit.Core.Billing.Organizations.Models.OrganizationLicense");
|
||||
@@ -439,14 +424,12 @@ internal class Program
|
||||
return;
|
||||
}
|
||||
|
||||
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, "LicenseKey", String.IsNullOrWhiteSpace(key) ? Guid.NewGuid().ToString("n") : key);
|
||||
Set(type, license, "InstallationId", instalId);
|
||||
Set(type, license, "Id", Guid.NewGuid());
|
||||
Set(type, license, "Name", userName);
|
||||
Set(type, license, "BillingEmail", email);
|
||||
Set(type, license, "BusinessName", businessNameFinal);
|
||||
Set(type, license, "BusinessName", String.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName);
|
||||
Set(type, license, "Enabled", true);
|
||||
Set(type, license, "Plan", "Enterprise (Annually)");
|
||||
Set(type, license, "PlanType", Enum.Parse(planTypeEnum, "EnterpriseAnnually"));
|
||||
@@ -468,28 +451,21 @@ internal class Program
|
||||
Set(type, license, "UsersGetPremium", true);
|
||||
Set(type, license, "UseCustomPermissions", true);
|
||||
Set(type, license, "Version", 16);
|
||||
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, "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));
|
||||
Set(type, license, "UsePasswordManager", true);
|
||||
Set(type, license, "UseSecretsManager", true);
|
||||
Set(type, license, "SmSeats", Int32.MaxValue);
|
||||
Set(type, license, "SmServiceAccounts", Int32.MaxValue);
|
||||
Set(type, license, "UseRiskInsights", true);
|
||||
Set(type, license, "LimitCollectionCreationDeletion", true);
|
||||
Set(type, license, "AllowAdminAccessToAllCollectionItems", true);
|
||||
Set(type, license, "Trial", false);
|
||||
Set(type, license, "LicenseType", Enum.Parse(licenseTypeEnum, "Organization"));
|
||||
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])!));
|
||||
|
||||
@@ -499,107 +475,4 @@ internal class Program
|
||||
{
|
||||
type.GetProperty(name)?.SetValue(license, value);
|
||||
}
|
||||
|
||||
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<Claim> 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<Claim> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="5.1.0" />
|
||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user