From d4abc9e5b733ea44bc18fdd89cf2de55aa40d769 Mon Sep 17 00:00:00 2001 From: Michiel Hazelhof Date: Mon, 16 Jan 2023 21:13:43 +0100 Subject: [PATCH] Full Unified support including Linux and Windows (#155 / #154) * Initial work * Fix typo * Fix typo * Fix stupid issue * Add comments and fix minor issues * Add extra information * Add Linux script for generating keys * Add circleci * Add comments * Add extra option * Add missing permissions and empty script for now * Fix line endings * Add missing mount point * Simplify patch * Fix scripts * Reduce complexity * Fix circleci * Remove useless line * Move to src folder and improve image creation --- .circleci/config.yml | 30 +- .servers/serverlist.txt | 3 + README.md | 134 ++-- build.ps1 | 104 +++ build.sh | 116 +++- generateKeys.ps1 | 22 + .keys/generate-keys.sh => generateKeys.sh | 19 +- licenseGen.ps1 | 14 + licenseGen.sh | 16 + src/bitBetter/Dockerfile | 21 +- src/bitBetter/Dockerfile-bitwarden-patch | 4 + src/bitBetter/Program.cs | 124 ++-- src/bitBetter/bitBetter.csproj | 4 +- src/bitBetter/build.sh | 7 - src/licenseGen/Dockerfile | 18 +- src/licenseGen/Program.cs | 761 +++++++++++----------- src/licenseGen/build.sh | 6 - src/licenseGen/licenseGen.csproj | 2 +- src/licenseGen/run.sh | 26 - update-bitwarden.sh | 85 --- 20 files changed, 784 insertions(+), 732 deletions(-) create mode 100644 .servers/serverlist.txt create mode 100644 build.ps1 create mode 100644 generateKeys.ps1 rename .keys/generate-keys.sh => generateKeys.sh (58%) create mode 100644 licenseGen.ps1 create mode 100755 licenseGen.sh create mode 100644 src/bitBetter/Dockerfile-bitwarden-patch delete mode 100755 src/bitBetter/build.sh delete mode 100755 src/licenseGen/build.sh delete mode 100755 src/licenseGen/run.sh delete mode 100755 update-bitwarden.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 34a1058..6bbf697 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,15 +1,15 @@ -version: 2 -jobs: - build: - machine: true - steps: - - checkout - - run: - name: Print the Current Time - command: date - - run: - name: Generate Keys - command: ./.keys/generate-keys.sh - - run: - name: Build script - command: ./build.sh +version: 2.1 +jobs: + build: + machine: true + steps: + - checkout + - run: + name: Print the Current Time + command: date + - run: + name: Generate Keys + command: ./generateKeys.sh + - run: + name: Build script + command: ./build.sh y \ No newline at end of file diff --git a/.servers/serverlist.txt b/.servers/serverlist.txt new file mode 100644 index 0000000..b586cd1 --- /dev/null +++ b/.servers/serverlist.txt @@ -0,0 +1,3 @@ +docker run -d --name bitwarden -v \logs:/var/log/bitwarden -v \bwdata:/etc/bitwarden -p 80:8080 --env-file \settings.env bitwarden-patch + +docker-compose -f /docker-compose.yml up -d \ No newline at end of file diff --git a/README.md b/README.md index 7ded320..e700898 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # BitBetter -BitBetter is is a tool to modify Bitwarden's core dll to allow you to generate your own individual and organisation licenses. **You must have an existing installation of Bitwarden for BitBetter to modify.** +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. -_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 some semi 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 @@ -25,55 +25,70 @@ Credit to https://github.com/h44z/BitBetter and https://github.com/jakeswenson/B - [Footnotes](#footnotes) # Getting Started -The following instructions are for unix-based systems (Linux, BSD, macOS), it is possible to use a Windows systems assuming you are able to enable and install [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). +The following instructions are for unix-based systems (Linux, BSD, macOS) and Windows, just choose the correct script extension (.sh or .ps1 respectively). ## Dependencies Aside from docker, which you also need for Bitwarden, BitBetter requires the following: * 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) +* 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: -```bash +``` git clone https://github.com/jakeswenson/BitBetter.git ``` ## Building BitBetter -Now that you've set up your build environment, you can **run the main build script** to generate a modified version of the `bitwarden/api` and `bitwarden/identity` docker images. +Now that you've set up your build environment, we need to specify which servers to start after the work is done. +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 `bitwarden/self-host` docker image and the license generator. From the BitBetter directory, simply run: -```bash -./build.sh +``` +./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 `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/self-host` image called `bitwarden-patch`. -You may now simply create the file `/path/to/bwdata/docker/docker-compose.override.yml` with the following contents to utilize the modified images. +Afterwards it will automatically generate the license generator and start all previously specified containers which are **now ready to accept self-issued licenses.** -```yaml -version: '3' - -services: - api: - image: bitbetter/api - - identity: - image: bitbetter/identity -``` - -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`. - -> Replace `dockerComposePull`
with `#dockerComposePull` - -You can now start or restart Bitwarden as normal and the modified api will be used. **It is now ready to accept self-issued licenses.** --- + +## Updating Bitwarden and BitBetter + +To update Bitwarden, the same `build.[sh|ps1]` script can be used. It will rebuild the BitBetter image and automatically update Bitwarden before doing so. + +## Generating Signed Licenses + +There is a tool included in the directory `licenseGen/` that will generate new individual and organization licenses. These licenses will be accepted by the modified Bitwarden because they will be signed by the certificate you generated in earlier steps. + +In order to run the tool and generate a license you'll need to get a **user's GUID** in order to generate an **invididual license** or the server's **install ID** to generate an **Organization license**. These can be retrieved most easily through the Bitwarden [Admin Portal](https://help.bitwarden.com/article/admin-portal/). + +**The user must have a verified email address at the time of license import, otherwise Bitwarden will reject the license key. Nevertheless, the license key can be generated even before the user's email is verified.** + +If you ran the build script, you can **simply run the license gen in interactive mode** from the `Bitbetter` directory and **follow the prompts to generate your license**. + +``` +./licenseGen.[sh|ps1] interactive +``` + +**The license generator will spit out a JSON-formatted license which can then be used within the Bitwarden web front-end to license your user or org!** + + +--- + ### Note: Manually generating Certificate & Key If you wish to generate your self-signed cert & key manually, you can run the following commands. +Note that you should never have to do this yourself, but can also be triggered by running the generateKeys.[sh|ps1] script. ```bash openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.cert -days 36500 -outform DER -passout pass:test @@ -81,69 +96,11 @@ openssl x509 -inform DER -in cert.cert -out cert.pem openssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem -passin pass:test -passout pass:test ``` -> Note that the password here must be `test`.[1](#f1) +> Note that the password here must be `test`.[2](#f1) + --- -## Updating Bitwarden and BitBetter - -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. - -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 -``` -`param1`: The path to the directory containing your bwdata directory - -`param2`: If you want the docker-compose file to be overwritten (either `y` or `n`) - -`param3`: If you want the bitbetter images to be rebuild (either `y` or `n`) - -If you are updating from versions <= 1.46.2, you may need to run `update-bitwarden.sh` twice to complete the update process. - -## Generating Signed Licenses - -There is a tool included in the directory `src/licenseGen/` that will generate new individual and organization licenses. These licenses will be accepted by the modified Bitwarden because they will be signed by the certificate you generated in earlier steps. - - -First, from the `BitBetter/src/licenseGen` directory, **build the license generator**.[2](#f2) - -```bash -./build.sh -``` - -In order to run the tool and generate a license you'll need to get a **user's GUID** in order to generate an **invididual license** or the server's **install ID** to generate an **Organization license**. These can be retrieved most easily through the Bitwarden [Admin Portal](https://help.bitwarden.com/article/admin-portal/). - -**The user must have a verified email address at the time of license import, otherwise Bitwarden will reject the license key. Nevertheless, the license key can be generated even before the user's email is verified.** - -If you generated your keys in the default `BitBetter/.keys` directory, you can **simply run the license gen in interactive mode** from the `Bitbetter` directory and **follow the prompts to generate your license**. - -```bash -./src/licenseGen/run.sh interactive -``` - -**The license generator will spit out a JSON-formatted license which can then be used within the Bitwarden web front-end to license your user or org!** - ---- - -### Note: Alternative Ways to Generate License - -If you wish to run the license gen from a directory aside from the root `BitBetter` one, you'll have to provide the absolute path to your cert.pfx. - -```bash -./src/licenseGen/run.sh /Absolute/Path/To/BitBetter/.keys/cert.pfx interactive -``` - -Additional, instead of interactive mode, you can also pass the parameters directly to the command as follows. - -```bash -./src/licenseGen/run.sh /Absolute/Path/To/BitBetter/.keys/cert.pfx user "Name" "E-Mail" "User-GUID" ["Storage Space in GB"] ["Custom LicenseKey"] -./src/licenseGen/run.sh /Absolute/Path/To/BitBetter/.keys/cert.pfx org "Name" "E-Mail" "Install-ID used to install the server" ["Storage Space in GB"] ["Custom LicenseKey"] -``` - ---- - - # FAQ: Questions you might have. ## Why build a license generator for open source software? @@ -161,7 +118,6 @@ UPDATE: Bitwarden now offers a cheap license called [Families Organization](http # Footnotes -1 If you wish to change this you'll need to change the value that `src/licenseGen/Program.cs` uses for its `GenerateUserLicense` and `GenerateOrgLicense` calls. Remember, this is really unnecessary as this certificate does not represent any type of security-related certificate. - -2This tool builds on top of the `bitbetter/api` container image so make sure you've built that above using the root `./build.sh` script. +1This tool builds on top of the `bitbetter/api` container image so make sure you've built that above using the root `./build.sh` script. +2 If you wish to change this you'll need to change the value that `licenseGen/Program.cs` uses for its `GenerateUserLicense` and `GenerateOrgLicense` calls. Remember, this is really unnecessary as this certificate does not represent any type of security-related certificate. diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..37c1c48 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,104 @@ +# define temporary directory +$tempdirectory = "$pwd\temp" +# define services to patch +$components = "Api","Identity" + +# delete old directories / files if applicable +if (Test-Path "$tempdirectory") { + Remove-Item "$tempdirectory" -Recurse -Force +} + +if (Test-Path -Path "$pwd\src\licenseGen\Core.dll" -PathType Leaf) { + Remove-Item "$pwd\src\licenseGen\Core.dll" -Force +} + +if (Test-Path -Path "$pwd\src\licenseGen\cert.pfx" -PathType Leaf) { + Remove-Item "$pwd\src\licenseGen\cert.pfx" -Force +} + +if (Test-Path -Path "$pwd\src\bitBetter\cert.cert" -PathType Leaf) { + Remove-Item "$pwd\src\bitBetter\cert.cert" -Force +} + +# generate keys if none are available +if (!(Test-Path "$pwd\.keys")) { + .\generateKeys.ps1 +} + +# copy the key to bitBetter and licenseGen +Copy-Item "$pwd\.keys\cert.cert" -Destination "$pwd\src\bitBetter" +Copy-Item "$pwd\.keys\cert.pfx" -Destination "$pwd\src\licenseGen" + +# build bitBetter and clean the source directory after +docker build -t bitbetter/bitbetter "$pwd\src\bitBetter" +Remove-Item "$pwd\src\bitBetter\cert.cert" -Force + +# gather all running instances +$oldinstances = docker container ps --all -f Name=bitwarden --format '{{.ID}}' + +# stop all running instances +foreach ($instance in $oldinstances) { + docker stop $instance + docker rm $instance +} + +# update bitwarden itself +if ($args[0] -eq 'y') +{ + docker pull bitwarden/self-host:beta +} +else +{ + $confirmation = Read-Host "Update (or get) bitwarden source container" + if ($confirmation -eq 'y') { + docker pull bitwarden/self-host:beta + } +} + +# stop and remove previous existing patch(ed) container +docker stop bitwarden-patch +docker rm bitwarden-patch +docker image rm bitwarden-patch + +# start a new bitwarden instance so we can patch it +$patchinstance = docker run -d --name bitwarden-patch bitwarden/self-host:beta + +# create our temporary directory +New-item -ItemType Directory -Path $tempdirectory + +# extract the files that need to be patched from the services that need to be patched into our temporary directory +foreach ($component in $components) { + New-item -itemtype Directory -path "$tempdirectory\$component" + docker cp $patchinstance`:/app/$component/Core.dll "$tempdirectory\$component\Core.dll" +} + +# run bitBetter, this applies our patches to the required files +docker run -v "$tempdirectory`:/app/mount" --rm bitbetter/bitbetter + +# create a new image with the patched files +docker build . --tag bitwarden-patch --file "$pwd\src\bitBetter\Dockerfile-bitwarden-patch" + +# stop and remove our temporary container +docker stop bitwarden-patch +docker rm bitwarden-patch + +# copy our patched library to the licenseGen source directory +Copy-Item "$tempdirectory\Identity\Core.dll" -Destination "$pwd\src\licenseGen" + +# remove our temporary directory +Remove-Item "$tempdirectory" -Recurse -Force + +# start all user requested instances +foreach($line in Get-Content "$pwd\.servers\serverlist.txt") { + Invoke-Expression "& $line" +} + +# remove our bitBetter image +docker image rm bitbetter/bitbetter + +# build the licenseGen +docker build -t bitbetter/licensegen "$pwd\src\licenseGen" + +# clean the licenseGen source directory +Remove-Item "$pwd\src\licenseGen\Core.dll" -Force +Remove-Item "$pwd\src\licenseGen\cert.pfx" -Force \ No newline at end of file diff --git a/build.sh b/build.sh index 4371c0c..2be0c79 100755 --- a/build.sh +++ b/build.sh @@ -1,28 +1,106 @@ -#!/bin/sh +#!/bin/bash -DIR=`dirname "$0"` -DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd` -BW_VERSION=$(curl -sL https://go.btwrdn.co/bw-sh-versions | grep '^ *"'coreVersion'":' | awk -F\: '{ print $2 }' | sed -e 's/,$//' -e 's/^"//' -e 's/"$//') +# define temporary directory +TEMPDIRECTORY="$PWD/temp" -echo "Building BitBetter for BitWarden version $BW_VERSION" +# define services to patch +COMPONENTS=("Api" "Identity") -# If there aren't any keys, generate them first. -[ -e "$DIR/.keys/cert.cert" ] || "$DIR/.keys/generate-keys.sh" +# delete old directories / files if applicable +if [ -d "$TEMPDIRECTORY" ]; then + rm -rf "$TEMPDIRECTORY" +fi -[ -e "$DIR/src/bitBetter/.keys" ] || mkdir "$DIR/src/bitBetter/.keys" +if [ -f "$PWD/src/licenseGen/Core.dll" ]; then + rm -f "$PWD/src/licenseGen/Core.dll" +fi -cp "$DIR/.keys/cert.cert" "$DIR/src/bitBetter/.keys" +if [ -f "$PWD/src/licenseGen/cert.pfx" ]; then + rm -f "$PWD/src/licenseGen/cert.pfx" +fi -docker run --rm -v "$DIR/src/bitBetter:/bitBetter" -w=/bitBetter mcr.microsoft.com/dotnet/sdk:6.0 sh build.sh +if [ -f "$PWD/src/bitBetter/cert.cert" ]; then + rm -f "$PWD/src/bitBetter/cert.cert" +fi -docker build --no-cache --build-arg BITWARDEN_TAG=bitwarden/api:$BW_VERSION --label com.bitwarden.product="bitbetter" -t bitbetter/api "$DIR/src/bitBetter" # --squash -docker build --no-cache --build-arg BITWARDEN_TAG=bitwarden/identity:$BW_VERSION --label com.bitwarden.product="bitbetter" -t bitbetter/identity "$DIR/src/bitBetter" # --squash +# generate keys if none are available +if [ ! -d "$PWD/.keys" ]; then + ./generateKeys.sh +fi -docker tag bitbetter/api bitbetter/api:latest -docker tag bitbetter/identity bitbetter/identity:latest -docker tag bitbetter/api bitbetter/api:$BW_VERSION -docker tag bitbetter/identity bitbetter/identity:$BW_VERSION +# copy the key to bitBetter and licenseGen +cp -f "$PWD/.keys/cert.cert" "$PWD/src/bitBetter" +cp -f "$PWD/.keys/cert.pfx" "$PWD/src/licenseGen" -# Remove old instances of the image after a successful build. -ids=$( docker images bitbetter/* | grep -E -v -- "CREATED|latest|${BW_VERSION}" | awk '{ print $3 }' ) -[ -n "$ids" ] && docker rmi $ids || true +# build bitBetter and clean the source directory after +docker build -t bitbetter/bitbetter "$PWD/src/bitBetter" +rm -f "$PWD/src/bitBetter/cert.cert" + +# gather all running instances +OLDINSTANCES=$(docker container ps --all -f Name=bitwarden --format '{{.ID}}') + +# stop all running instances +for INSTANCE in ${OLDINSTANCES[@]}; do + docker stop $INSTANCE + docker rm $INSTANCE +done + +# update bitwarden itself +if [ "$1" = "y" ]; then + docker pull bitwarden/self-host:beta +else + read -p "Update (or get) bitwarden source container: " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]] + then + docker pull bitwarden/self-host:beta + fi +fi + +# stop and remove previous existing patch(ed) container +docker stop bitwarden-patch +docker rm bitwarden-patch +docker image rm bitwarden-patch + +# start a new bitwarden instance so we can patch it +PATCHINSTANCE=$(docker run -d --name bitwarden-patch bitwarden/self-host:beta) + +# create our temporary directory +mkdir $TEMPDIRECTORY + +# extract the files that need to be patched from the services that need to be patched into our temporary directory +for COMPONENT in ${COMPONENTS[@]}; do + mkdir "$TEMPDIRECTORY/$COMPONENT" + docker cp $PATCHINSTANCE:/app/$COMPONENT/Core.dll "$TEMPDIRECTORY/$COMPONENT/Core.dll" +done + +# run bitBetter, this applies our patches to the required files +docker run -v "$TEMPDIRECTORY:/app/mount" --rm bitbetter/bitbetter + +# create a new image with the patched files +docker build . --tag bitwarden-patch --file "$PWD/src/bitBetter/Dockerfile-bitwarden-patch" + +# stop and remove our temporary container +docker stop bitwarden-patch +docker rm bitwarden-patch + +# copy our patched library to the licenseGen source directory +cp -f "$TEMPDIRECTORY/Identity/Core.dll" "$PWD/src/licenseGen" + +# remove our temporary directory +rm -rf "$TEMPDIRECTORY" + +# start all user requested instances +cat "$PWD/.servers/serverlist.txt" | while read LINE; do + bash -c "$LINE" +done + +# remove our bitBetter image +docker image rm bitbetter/bitbetter + +# build the licenseGen +docker build -t bitbetter/licensegen "$PWD/src/licenseGen" + +# clean the licenseGen source directory +rm -f "$PWD/src/licenseGen/Core.dll" +rm -f "$PWD/src/licenseGen/cert.pfx" \ No newline at end of file diff --git a/generateKeys.ps1 b/generateKeys.ps1 new file mode 100644 index 0000000..690143f --- /dev/null +++ b/generateKeys.ps1 @@ -0,0 +1,22 @@ +# get the basic openssl binary path +$opensslbinary = "$Env:Programfiles\OpenSSL-Win64\bin\openssl.exe" + +# if openssl is not installed attempt to install it +if (!(Get-Command $opensslbinary -errorAction SilentlyContinue)) +{ + winget install openssl +} + +# if previous keys exist, remove them +if (Test-Path "$pwd\.keys") +{ + Remove-Item "$pwd\.keys" -Recurse -Force +} + +# create new directory +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.cert`" -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.cert`" -out `"$pwd\.keys\cert.pem`"" +Invoke-Expression "& '$opensslbinary' pkcs12 -export -out `"$pwd\.keys\cert.pfx`" -inkey `"$pwd\.keys\key.pem`" -in `"$pwd\.keys\cert.pem`" -passin pass:test -passout pass:test" \ No newline at end of file diff --git a/.keys/generate-keys.sh b/generateKeys.sh similarity index 58% rename from .keys/generate-keys.sh rename to generateKeys.sh index d460c13..4850fc2 100755 --- a/.keys/generate-keys.sh +++ b/generateKeys.sh @@ -1,20 +1,19 @@ -#!/bin/sh +#!/bin/bash # Check for openssl command -v openssl >/dev/null 2>&1 || { echo >&2 "openssl required but not found. Aborting."; exit 1; } -DIR=`dirname "$0"` -DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd` +DIR="$PWD/.keys" -# Remove any existing key files -[ ! -e "$DIR/cert.pem" ] || rm "$DIR/cert.pem" -[ ! -e "$DIR/key.pem" ] || rm "$DIR/key.pem" -[ ! -e "$DIR/cert.cert" ] || rm "$DIR/cert.cert" -[ ! -e "$DIR/cert.pfx" ] || rm "$DIR/cert.pfx" +# if previous keys exist, remove them +if [ -d "$DIR" ]; then + rm -rf "$DIR" +fi + +# create new directory +mkdir "$DIR" # Generate new keys openssl req -x509 -newkey rsa:4096 -keyout "$DIR/key.pem" -out "$DIR/cert.cert" -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.cert" -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 - -ls diff --git a/licenseGen.ps1 b/licenseGen.ps1 new file mode 100644 index 0000000..3907946 --- /dev/null +++ b/licenseGen.ps1 @@ -0,0 +1,14 @@ +if ($($args.Count) -lt 1) { + echo "USAGE: [License Gen args...]" + echo "ACTIONS:" + echo " interactive" + echo " user" + echo " org" + Exit 1 +} + +if ($args[0] = "interactive") { + docker run -it --rm bitbetter/licensegen interactive +} else { + docker run bitbetter/licensegen $args +} diff --git a/licenseGen.sh b/licenseGen.sh new file mode 100755 index 0000000..29c9e62 --- /dev/null +++ b/licenseGen.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [ $# -lt 1 ]; then + echo "USAGE: [License Gen args...]" + echo "ACTIONS:" + echo " interactive" + echo " user" + echo " org" + exit 1 +fi + +if [ "$1" = "interactive" ]; then + docker run -it --rm bitbetter/licensegen interactive +else + docker run --rm bitbetter/licensegen "$@" +fi diff --git a/src/bitBetter/Dockerfile b/src/bitBetter/Dockerfile index 07bf05c..00555b7 100644 --- a/src/bitBetter/Dockerfile +++ b/src/bitBetter/Dockerfile @@ -1,11 +1,14 @@ -ARG BITWARDEN_TAG -FROM ${BITWARDEN_TAG} +FROM mcr.microsoft.com/dotnet/sdk:6.0 as build +WORKDIR /bitBetter -COPY bin/Debug/netcoreapp6.0/publish/* /bitBetter/ -COPY ./.keys/cert.cert /newLicensing.cer +COPY . /bitBetter +COPY cert.cert /app/ -RUN set -e; set -x; \ - dotnet /bitBetter/bitBetter.dll && \ - mv /app/Core.dll /app/Core.orig.dll && \ - mv /app/modified.dll /app/Core.dll && \ - rm -rf /bitBetter && rm -rf /newLicensing.cer +RUN dotnet restore +RUN dotnet publish -c Release -o /app --no-restore + +FROM mcr.microsoft.com/dotnet/sdk:6.0 +WORKDIR /app +COPY --from=build /app . + +ENTRYPOINT [ "/app/bitBetter" ] \ No newline at end of file diff --git a/src/bitBetter/Dockerfile-bitwarden-patch b/src/bitBetter/Dockerfile-bitwarden-patch new file mode 100644 index 0000000..cd443b5 --- /dev/null +++ b/src/bitBetter/Dockerfile-bitwarden-patch @@ -0,0 +1,4 @@ +FROM bitwarden/self-host:beta + +COPY ./temp/Api/Core.dll /app/Api/Core.dll +COPY ./temp/Identity/Core.dll /app/Identity/Core.dll \ No newline at end of file diff --git a/src/bitBetter/Program.cs b/src/bitBetter/Program.cs index 0c7b93e..d17fcbb 100644 --- a/src/bitBetter/Program.cs +++ b/src/bitBetter/Program.cs @@ -1,93 +1,75 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography.X509Certificates; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Cecil.Rocks; +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using dnlib.DotNet.Writer; +using dnlib.IO; -namespace bitwardenSelfLicensor +namespace bitBetter; + +internal class Program { - class Program + private static Int32 Main(String[] args) { - static int Main(string[] args) + const String certFile = "/app/cert.cert"; + String[] files = Directory.GetFiles("/app/mount", "Core.dll", SearchOption.AllDirectories); + + foreach (String file in files) { - string cerFile; - string corePath; + Console.WriteLine(file); + ModuleDefMD moduleDefMd = ModuleDefMD.Load(file); + Byte[] cert = File.ReadAllBytes(certFile); - if(args.Length >= 2) { - cerFile = args[0]; - corePath = args[1]; - } else if (args.Length == 1) { - cerFile = args[0]; - corePath = "/app/Core.dll"; - } - else { - cerFile = "/newLicensing.cer"; - corePath = "/app/Core.dll"; - } + EmbeddedResource embeddedResourceToRemove = moduleDefMd.Resources + .OfType() + .First(r => r.Name.Equals("Bit.Core.licensing.cer")); + Console.WriteLine(embeddedResourceToRemove.Name); - var module = ModuleDefinition.ReadModule(new MemoryStream(File.ReadAllBytes(corePath))); - var cert = File.ReadAllBytes(cerFile); + EmbeddedResource embeddedResourceToAdd = new("Bit.Core.licensing.cer", cert) {Attributes = embeddedResourceToRemove.Attributes }; + moduleDefMd.Resources.Add(embeddedResourceToAdd); + moduleDefMd.Resources.Remove(embeddedResourceToRemove); - var x = module.Resources.OfType() - .Where(r => r.Name.Equals("Bit.Core.licensing.cer")) - .First(); - - Console.WriteLine(x.Name); - - var e = new EmbeddedResource("Bit.Core.licensing.cer", x.Attributes, cert); - - module.Resources.Add(e); - module.Resources.Remove(x); - - var services = module.Types.Where(t => t.Namespace == "Bit.Core.Services"); + DataReader reader = embeddedResourceToRemove.CreateReader(); + X509Certificate2 existingCert = new(reader.ReadRemainingBytes()); - - 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); + X509Certificate2 certificate = new(cert); Console.WriteLine($"New Cert Thumbprint: {certificate.Thumbprint}"); - var ctor = licensingType.GetConstructors().Single(); - - - var rewriter = ctor.Body.GetILProcessor(); - - var instToReplace = - ctor.Body.Instructions.Where(i => i.OpCode == OpCodes.Ldstr - && string.Equals((string)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase)) - .FirstOrDefault(); - - if(instToReplace != null) { - rewriter.Replace(instToReplace, Instruction.Create(OpCodes.Ldstr, certificate.Thumbprint)); + IEnumerable services = moduleDefMd.Types.Where(t => t.Namespace == "Bit.Core.Services"); + TypeDef type = services.First(t => t.Name == "LicensingService"); + MethodDef constructor = type.FindConstructors().First(); + + Instruction instructionToPatch = + constructor.Body.Instructions + .FirstOrDefault(i => i.OpCode == OpCodes.Ldstr + && String.Equals((String)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase)); + + if (instructionToPatch != null) + { + instructionToPatch.Operand = certificate.Thumbprint; } - else { - Console.WriteLine("Cant find inst"); + else + { + Console.WriteLine("Can't find constructor to patch"); } - // foreach (var inst in ctor.Body.Instructions) - // { - // 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();} - // } + ModuleWriterOptions moduleWriterOptions = new(moduleDefMd); + moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.KeepOldMaxStack; + moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveAll; + moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveRids; - module.Write("modified.dll"); - - return 0; + moduleDefMd.Write(file + ".new"); + moduleDefMd.Dispose(); + File.Delete(file); + File.Move(file + ".new", file); } + + return 0; } -} +} \ No newline at end of file diff --git a/src/bitBetter/bitBetter.csproj b/src/bitBetter/bitBetter.csproj index 43d79ed..8dd1aff 100644 --- a/src/bitBetter/bitBetter.csproj +++ b/src/bitBetter/bitBetter.csproj @@ -5,8 +5,8 @@ netcoreapp6.0 - - + + diff --git a/src/bitBetter/build.sh b/src/bitBetter/build.sh deleted file mode 100755 index ea95081..0000000 --- a/src/bitBetter/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -e -set -x - -dotnet restore -dotnet publish diff --git a/src/licenseGen/Dockerfile b/src/licenseGen/Dockerfile index 2ca6a37..f23d3fd 100644 --- a/src/licenseGen/Dockerfile +++ b/src/licenseGen/Dockerfile @@ -1,17 +1,15 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 as build - WORKDIR /licenseGen COPY . /licenseGen +COPY Core.dll /app/ +COPY cert.pfx /app/ -RUN set -e; set -x; \ - dotnet add package Newtonsoft.Json --version 13.0.1 \ - && dotnet restore \ - && dotnet publish +RUN dotnet restore +RUN dotnet publish -c Release -o /app --no-restore +FROM mcr.microsoft.com/dotnet/sdk:6.0 +WORKDIR /app +COPY --from=build /app . -FROM bitbetter/api - -COPY --from=build /licenseGen/bin/Debug/netcoreapp6.0/publish/* /app/ - -ENTRYPOINT [ "dotnet", "/app/licenseGen.dll", "--core", "/app/Core.dll", "--cert", "/cert.pfx" ] +ENTRYPOINT [ "dotnet", "/app/licenseGen.dll", "--core", "/app/Core.dll", "--cert", "/app/cert.pfx" ] diff --git a/src/licenseGen/Program.cs b/src/licenseGen/Program.cs index db395f0..05e38b2 100644 --- a/src/licenseGen/Program.cs +++ b/src/licenseGen/Program.cs @@ -1,448 +1,445 @@ using System; using System.IO; -using System.Linq; +using System.Reflection; using System.Runtime.Loader; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.CommandLineUtils; using Newtonsoft.Json; -namespace bitwardenSelfLicensor +namespace licenseGen; + +internal class Program { - class Program + private static Int32 Main(String[] args) { - static int Main(string[] args) + CommandLineApplication app = new(); + CommandOption cert = app.Option("--cert", "cert file", CommandOptionType.SingleValue); + CommandOption coreDll = app.Option("--core", "path to core dll", CommandOptionType.SingleValue); + + Boolean CertExists() { - 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); + return File.Exists(cert.Value()); + } - bool certExists() + Boolean CoreExists() + { + return File.Exists(coreDll.Value()); + } + + Boolean VerifyTopOptions() + { + return !String.IsNullOrWhiteSpace(cert.Value()) && + !String.IsNullOrWhiteSpace(coreDll.Value()) && + CertExists() && CoreExists(); + } + + app.Command("interactive", config => + { + String buff, licensetype="", name="", email="", businessname=""; + Int16 storage = 0; + + Boolean validGuid = false, validInstallid = false; + Guid guid = new(), installid = new(); + + config.OnExecute(() => { - 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(); - } - - app.Command("interactive", config => - { - string buff="", licensetype="", name="", email="", businessname=""; - short storage = 0; - - bool valid_guid = false, valid_installid = false; - Guid guid = new Guid(), installid = new Guid(); - - config.OnExecute(() => + if (!VerifyTopOptions()) { - if (!verifyTopOptions()) + 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; + } + + Console.WriteLine("Interactive license mode..."); + + while (licensetype == "") + { + Console.WriteLine("What would you like to generate, a [u]ser license or an [o]rg license: "); + buff = Console.ReadLine(); + + if(buff == "u") { - if (!coreExists()) config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}"); - if (!certExists()) config.Error.WriteLine($"Cant find certificate at: {cert.Value()}"); + licensetype = "user"; + Console.WriteLine("Okay, we will generate a user license."); - config.ShowHelp(); - return 1; - } - - WriteLine("Interactive license mode..."); - - while (licensetype == "") - { - WriteLine("What would you like to generate, a [u]ser license or an [o]rg license?"); - buff = Console.ReadLine(); - - if(buff == "u") + while (validGuid == false) { - licensetype = "user"; - WriteLineOver("Okay, we will generate a user license."); + Console.WriteLine("Please provide the user's guid — refer to the Readme for details on how to retrieve this. [GUID]: "); + buff = Console.ReadLine(); - while (valid_guid == false) + if (Guid.TryParse(buff, out guid))validGuid = true; + else Console.WriteLine("The user-guid provided does not appear to be valid!"); + } + } + else if (buff == "o") + { + licensetype = "org"; + Console.WriteLine("Okay, we will generate an organization license."); + + while (validInstallid == false) + { + Console.WriteLine("Please provide your Bitwarden Install-ID — refer to the Readme for details on how to retrieve this. [Install-ID]: "); + buff = Console.ReadLine(); + + if (Guid.TryParse(buff, out installid)) validInstallid = true; + else Console.WriteLine("The install-id provided does not appear to be valid."); + } + + while (businessname == "") + { + Console.WriteLine("Please enter a business name, default is BitBetter. [Business Name]: "); + buff = Console.ReadLine(); + if (buff == "") { - WriteLine("Please provide the user's guid — refer to the Readme for details on how to retrieve this. [GUID]:"); - buff = Console.ReadLine(); - - if (Guid.TryParse(buff, out guid))valid_guid = true; - else WriteLineOver("The user-guid provided does not appear to be valid."); + businessname = "BitBetter"; + } + else if (CheckBusinessName(buff)) + { + businessname = buff; } } - else if (buff == "o") - { - licensetype = "org"; - WriteLineOver("Okay, we will generate an organization license."); - - while (valid_installid == false) - { - WriteLine("Please provide your Bitwarden Install-ID — refer to the Readme for details on how to retrieve this. [Install-ID]:"); - buff = Console.ReadLine(); - - if (Guid.TryParse(buff, out installid)) valid_installid = true; - else WriteLineOver("The install-id provided does not appear to be valid."); - } - - while (businessname == "") - { - 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 - { - WriteLineOver("Unrecognized option \'" + buff + "\'. "); - } } - - while (name == "") + else { - WriteLineOver("Please provide the username this license will be registered to. [username]:"); - buff = Console.ReadLine(); - if ( checkUsername(buff) ) name = buff; + Console.WriteLine("Unrecognized option \'" + buff + "\'."); } + } - while (email == "") - { - WriteLineOver("Please provide the email address for the user " + name + ". [email]"); - buff = Console.ReadLine(); - if ( checkEmail(buff) ) email = buff; - } + while (name == "") + { + Console.WriteLine("Please provide the username this license will be registered to. [username]: "); + buff = Console.ReadLine(); + if ( CheckUsername(buff) ) name = buff; + } - while (storage == 0) + while (email == "") + { + Console.WriteLine("Please provide the email address for the user " + name + ". [email]: "); + buff = Console.ReadLine(); + if (CheckEmail(buff)) { - WriteLineOver("Extra storage space for the user " + name + ". (max.: " + short.MaxValue + "). Defaults to maximum value. [storage]"); - buff = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(buff)) - { - storage = short.MaxValue; - } - else - { - if (checkStorage(buff)) storage = short.Parse(buff); - } + email = buff; } + } - if (licensetype == "user") + while (storage == 0) + { + Console.WriteLine("Extra storage space for the user " + name + ". (max.: " + Int16.MaxValue + "). Defaults to maximum value. [storage]"); + buff = Console.ReadLine(); + if (String.IsNullOrWhiteSpace(buff)) { - WriteLineOver("Confirm creation of \"user\" license for username: \"" + name + "\", email: \"" + email + "\", Storage: \"" + storage + " GB\", User-GUID: \"" + guid + "\"? Y/n"); - buff = Console.ReadLine(); - if ( buff == "" || buff == "y" || buff == "Y" ) + storage = Int16.MaxValue; + } + else + { + if (CheckStorage(buff)) { - GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, guid, null); - } - else - { - WriteLineOver("Exiting..."); - return 0; - } - } - else if (licensetype == "org") - { - WriteLineOver("Confirm creation of \"organization\" license for business name: \"" + businessname + "\", username: \"" + name + "\", email: \"" + email + "\", Storage: \"" + storage + " GB\", Install-ID: \"" + installid + "\"? Y/n"); - buff = Console.ReadLine(); - if ( buff == "" || buff == "y" || buff == "Y" ) - { - GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, installid, businessname, null); - } - else - { - WriteLineOver("Exiting..."); - return 0; + storage = Int16.Parse(buff); } } + } - return 0; - }); + if (licensetype == "user") + { + Console.WriteLine("Confirm creation of \"user\" license for username: \"" + name + "\", email: \"" + email + "\", Storage: \"" + storage + " GB\", User-GUID: \"" + guid + "\"? Y/n"); + buff = Console.ReadLine(); + if ( buff is "" or "y" or "Y" ) + { + GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, guid, null); + } + else + { + Console.WriteLine("Exiting..."); + return 0; + } + } + else if (licensetype == "org") + { + Console.WriteLine("Confirm creation of \"organization\" license for business name: \"" + businessname + "\", username: \"" + name + "\", email: \"" + email + "\", Storage: \"" + storage + " GB\", Install-ID: \"" + installid + "\"? Y/n"); + buff = Console.ReadLine(); + if ( buff is "" or "y" or "Y" ) + { + GenerateOrgLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name, email, storage, installid, businessname, null); + } + else + { + Console.WriteLine("Exiting..."); + return 0; + } + } + + return 0; }); + }); - app.Command("user", config => + app.Command("user", config => + { + CommandArgument name = config.Argument("Name", "your name"); + CommandArgument email = config.Argument("Email", "your email"); + CommandArgument userIdArg = config.Argument("User ID", "your user id"); + CommandArgument storage = config.Argument("Storage", "extra storage space in GB. Maximum is " + Int16.MaxValue + " (optional, default = max)"); + CommandArgument key = config.Argument("Key", "your key id (optional)"); + + config.OnExecute(() => { - var name = config.Argument("Name", "your name"); - var email = config.Argument("Email", "your email"); - var userIdArg = config.Argument("User ID", "your user id"); - var storage = config.Argument("Storage", "extra storage space in GB. Maximum is " + short.MaxValue + " (optional, default = max)"); - var key = config.Argument("Key", "your key id (optional)"); - var help = config.HelpOption("--help | -h | -?"); - - config.OnExecute(() => + if (!VerifyTopOptions()) { - if (!verifyTopOptions()) + if (!CoreExists()) { - 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; + config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}"); } - else if (string.IsNullOrWhiteSpace(name.Value) || string.IsNullOrWhiteSpace(email.Value)) + if (!CertExists()) { - config.Error.WriteLine($"Some arguments are missing: Name='{name.Value}' Email='{email.Value}'"); - config.ShowHelp("user"); - return 1; + config.Error.WriteLine($"Cant find certificate at: {cert.Value()}"); } - if (string.IsNullOrWhiteSpace(userIdArg.Value) || !Guid.TryParse(userIdArg.Value, out Guid userId)) - { - config.Error.WriteLine($"User ID not provided"); - config.ShowHelp("user"); - return 1; - } + config.ShowHelp(); + return 1; + } - short storageShort = 0; - if (!string.IsNullOrWhiteSpace(storage.Value)) - { - var parsedStorage = double.Parse(storage.Value); - if (parsedStorage > short.MaxValue || parsedStorage < 0) - { - config.Error.WriteLine("The storage value provided is outside the accepted range of [0-" + short.MaxValue + "]"); - config.ShowHelp("org"); - return 1; - } - storageShort = (short) parsedStorage; - } - - GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name.Value, email.Value, storageShort, userId, key.Value); - - return 0; - }); - }); - app.Command("org", config => - { - var name = config.Argument("Name", "your name"); - var email = config.Argument("Email", "your email"); - var installId = config.Argument("InstallId", "your installation id (GUID)"); - var storage = config.Argument("Storage", "extra storage space in GB. Maximum is " + short.MaxValue + " (optional, default = max)"); - var businessName = config.Argument("BusinessName", "name for the organization (optional)"); - var key = config.Argument("Key", "your key id (optional)"); - var help = config.HelpOption("--help | -h | -?"); - - config.OnExecute(() => + if (String.IsNullOrWhiteSpace(name.Value) || String.IsNullOrWhiteSpace(email.Value)) { - if (!verifyTopOptions()) - { - if (!coreExists()) - { - config.Error.WriteLine($"Cant find core dll at: {coreDll.Value()}"); - } - if (!certExists()) - { - config.Error.WriteLine($"Cant find certificate at: {cert.Value()}"); - } + config.Error.WriteLine($"Some arguments are missing: Name='{name.Value}' Email='{email.Value}'"); + config.ShowHelp("user"); + return 1; + } - config.ShowHelp(); - return 1; - } - else if (string.IsNullOrWhiteSpace(name.Value) || - string.IsNullOrWhiteSpace(email.Value) || - string.IsNullOrWhiteSpace(installId.Value)) + if (String.IsNullOrWhiteSpace(userIdArg.Value) || !Guid.TryParse(userIdArg.Value, out Guid userId)) + { + config.Error.WriteLine("User ID not provided"); + config.ShowHelp("user"); + return 1; + } + + Int16 storageShort = 0; + if (!String.IsNullOrWhiteSpace(storage.Value)) + { + Double parsedStorage = Double.Parse(storage.Value); + if (parsedStorage is > Int16.MaxValue or < 0) { - config.Error.WriteLine($"Some arguments are missing: Name='{name.Value}' Email='{email.Value}' InstallId='{installId.Value}'"); + config.Error.WriteLine("The storage value provided is outside the accepted range of [0-" + Int16.MaxValue + "]"); config.ShowHelp("org"); return 1; } + storageShort = (Int16) parsedStorage; + } - if (!Guid.TryParse(installId.Value, out Guid installationId)) + GenerateUserLicense(new X509Certificate2(cert.Value(), "test"), coreDll.Value(), name.Value, email.Value, storageShort, userId, key.Value); + + return 0; + }); + }); + app.Command("org", config => + { + CommandArgument name = config.Argument("Name", "your name"); + CommandArgument email = config.Argument("Email", "your email"); + CommandArgument installId = config.Argument("InstallId", "your installation id (GUID)"); + CommandArgument storage = config.Argument("Storage", "extra storage space in GB. Maximum is " + Int16.MaxValue + " (optional, default = max)"); + CommandArgument businessName = config.Argument("BusinessName", "name for the organization (optional)"); + CommandArgument key = config.Argument("Key", "your key id (optional)"); + + config.OnExecute(() => + { + if (!VerifyTopOptions()) + { + if (!CoreExists()) { - config.Error.WriteLine("Unable to parse your installation id as a GUID"); - config.Error.WriteLine($"Here's a new guid: {Guid.NewGuid()}"); + 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; + } + + if (String.IsNullOrWhiteSpace(name.Value) || + String.IsNullOrWhiteSpace(email.Value) || + String.IsNullOrWhiteSpace(installId.Value)) + { + config.Error.WriteLine($"Some arguments are missing: Name='{name.Value}' Email='{email.Value}' InstallId='{installId.Value}'"); + config.ShowHelp("org"); + return 1; + } + + if (!Guid.TryParse(installId.Value, out Guid installationId)) + { + config.Error.WriteLine("Unable to parse your installation id as a GUID"); + config.Error.WriteLine($"Here's a new guid: {Guid.NewGuid()}"); + config.ShowHelp("org"); + return 1; + } + + Int16 storageShort = 0; + if (!String.IsNullOrWhiteSpace(storage.Value)) + { + Double parsedStorage = Double.Parse(storage.Value); + if (parsedStorage is > Int16.MaxValue or < 0) + { + config.Error.WriteLine("The storage value provided is outside the accepted range of [0-" + Int16.MaxValue + "]"); config.ShowHelp("org"); return 1; } + storageShort = (Int16) parsedStorage; + } - short storageShort = 0; - if (!string.IsNullOrWhiteSpace(storage.Value)) - { - var parsedStorage = double.Parse(storage.Value); - if (parsedStorage > short.MaxValue || parsedStorage < 0) - { - config.Error.WriteLine("The storage value provided is outside the accepted range of [0-" + short.MaxValue + "]"); - config.ShowHelp("org"); - return 1; - } - storageShort = (short) parsedStorage; - } + GenerateOrgLicense(new X509Certificate2(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; - }); + return 0; }); + }); - app.OnExecute(() => - { - app.ShowHelp(); - return 10; - }); - - app.HelpOption("-? | -h | --help"); - - try - { - return app.Execute(args); - } - catch (Exception e) - { - Console.Error.WriteLine("Oops: {0}", e); - return 100; - } - } - - // checkUsername Checks that the username is a valid username - static bool checkUsername(string s) + app.OnExecute(() => { - if ( string.IsNullOrWhiteSpace(s) ) { - WriteLineOver("The username provided doesn't appear to be valid.\n"); - return false; - } - return true; // TODO: Actually validate - } + app.ShowHelp(); + return 10; + }); - // checkBusinessName Checks that the Business Name is a valid username - static bool checkBusinessName(string s) + app.HelpOption("-? | -h | --help"); + + try { - if ( string.IsNullOrWhiteSpace(s) ) { - WriteLineOver("The Business Name provided doesn't appear to be valid.\n"); - return false; - } - return true; // TODO: Actually validate + return app.Execute(args); } - - // checkEmail Checks that the email address is a valid email address - static bool checkEmail(string s) + catch (Exception e) { - if ( string.IsNullOrWhiteSpace(s) ) { - WriteLineOver("The email provided doesn't appear to be valid.\n"); - return false; - } - return true; // TODO: Actually validate - } - - // checkStorage Checks that the storage is in a valid range - static bool checkStorage(string s) - { - if (string.IsNullOrWhiteSpace(s)) - { - WriteLineOver("The storage provided doesn't appear to be valid.\n"); - return false; - } - if (double.Parse(s) > short.MaxValue || double.Parse(s) < 0) - { - WriteLineOver("The storage value provided is outside the accepted range of [0-" + short.MaxValue + "].\n"); - return false; - } - return true; - } - - // WriteLineOver Writes a new line to console over last line. - 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. - static void WriteLine(string s) - { - Console.WriteLine(s); - } - - 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.Models.Business.UserLicense"); - var licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType"); - - var license = Activator.CreateInstance(type); - - void set(string name, object value) - { - type.GetProperty(name).SetValue(license, value); - } - - 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); - 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("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)); - } - - 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.Models.Business.OrganizationLicense"); - var licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType"); - var planTypeEnum = core.GetType("Bit.Core.Enums.PlanType"); - - var license = Activator.CreateInstance(type); - - void set(string name, object value) - { - type.GetProperty(name).SetValue(license, value); - } - - 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", string.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName); - set("Enabled", true); - set("Plan", "Custom"); - set("PlanType", Enum.Parse(planTypeEnum, "Custom")); - set("Seats", (int)short.MaxValue); - set("MaxCollections", short.MaxValue); - set("UsePolicies", true); - set("UseSso", true); - set("UseKeyConnector", true); - //set("UseScim", true); // available in version 10, which is not released yet - set("UseGroups", true); - set("UseEvents", true); - set("UseDirectory", true); - set("UseTotp", true); - set("Use2fa", true); - set("UseApi", true); - set("UseResetPassword", true); - set("MaxStorageGb", storage == 0 ? short.MaxValue : storage); - set("SelfHost", true); - set("UsersGetPremium", true); - set("Version", 9); - 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("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)); + Console.Error.WriteLine("Oops: {0}", e); + return 100; } } + + // checkUsername Checks that the username is a valid username + private static Boolean CheckUsername(String s) + { + // TODO: Actually validate + if (!String.IsNullOrWhiteSpace(s)) return true; + + Console.WriteLine("The username provided doesn't appear to be valid!"); + return false; + } + + // checkBusinessName Checks that the Business Name is a valid username + private static Boolean CheckBusinessName(String s) + { + // TODO: Actually validate + if (!String.IsNullOrWhiteSpace(s)) return true; + + Console.WriteLine("The Business Name provided doesn't appear to be valid!"); + return false; + } + + // checkEmail Checks that the email address is a valid email address + private static Boolean CheckEmail(String s) + { + // TODO: Actually validate + if (!String.IsNullOrWhiteSpace(s)) return true; + + Console.WriteLine("The email provided doesn't appear to be valid!"); + return false; + } + + // checkStorage Checks that the storage is in a valid range + private static Boolean CheckStorage(String s) + { + if (String.IsNullOrWhiteSpace(s)) + { + Console.WriteLine("The storage provided doesn't appear to be valid!"); + return false; + } + + if (!(Double.Parse(s) > Int16.MaxValue) && !(Double.Parse(s) < 0)) return true; + + Console.WriteLine("The storage value provided is outside the accepted range of [0-" + Int16.MaxValue + "]!"); + return false; + } + + private static void GenerateUserLicense(X509Certificate2 cert, String corePath, String userName, String email, Int16 storage, Guid userId, String key) + { + Assembly core = AssemblyLoadContext.Default.LoadFromAssemblyPath(corePath); + + Type type = core.GetType("Bit.Core.Models.Business.UserLicense"); + Type licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType"); + + Object license = Activator.CreateInstance(type); + + void Set(String name, Object value) + { + type.GetProperty(name).SetValue(license, value); + } + + 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 ? Int16.MaxValue : storage); + Set("Version", 1); + 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("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, Int16 storage, Guid instalId, String businessName, String key) + { + Assembly core = AssemblyLoadContext.Default.LoadFromAssemblyPath(corePath); + + Type type = core.GetType("Bit.Core.Models.Business.OrganizationLicense"); + Type licenseTypeEnum = core.GetType("Bit.Core.Enums.LicenseType"); + Type planTypeEnum = core.GetType("Bit.Core.Enums.PlanType"); + + Object license = Activator.CreateInstance(type); + + void set(String name, Object value) + { + type.GetProperty(name).SetValue(license, value); + } + + 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", String.IsNullOrWhiteSpace(businessName) ? "BitBetter" : businessName); + set("Enabled", true); + set("Plan", "Custom"); + set("PlanType", Enum.Parse(planTypeEnum, "Custom")); + set("Seats", (Int32)Int16.MaxValue); + set("MaxCollections", Int16.MaxValue); + set("UsePolicies", true); + set("UseSso", true); + set("UseKeyConnector", true); + //set("UseScim", true); // available in version 10, which is not released yet + set("UseGroups", true); + set("UseEvents", true); + set("UseDirectory", true); + set("UseTotp", true); + set("Use2fa", true); + set("UseApi", true); + set("UseResetPassword", true); + set("MaxStorageGb", storage == 0 ? Int16.MaxValue : storage); + set("SelfHost", true); + set("UsersGetPremium", true); + set("Version", 9); + 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("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)); + } } diff --git a/src/licenseGen/build.sh b/src/licenseGen/build.sh deleted file mode 100755 index 31d08ac..0000000 --- a/src/licenseGen/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -DIR=`dirname "$0"` -DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd` - -docker build -t bitbetter/licensegen "$DIR" # --squash diff --git a/src/licenseGen/licenseGen.csproj b/src/licenseGen/licenseGen.csproj index 0edfd9a..93611d5 100644 --- a/src/licenseGen/licenseGen.csproj +++ b/src/licenseGen/licenseGen.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/licenseGen/run.sh b/src/licenseGen/run.sh deleted file mode 100755 index ba504c2..0000000 --- a/src/licenseGen/run.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -DIR=`dirname "$0"` -DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd` - -# Grab the absolute path to the default pfx location -cert_path="$DIR/../../.keys/cert.pfx" - -if [ "$#" -lt "2" ]; then - echo "USAGE: $0 [License Gen args...]" - echo "ACTIONS:" - echo " interactive" - echo " user" - echo " org" - exit 1 -fi - -cert_path="$1" -action="$2" -shift - -if [ $action = "interactive" ]; then - docker run -it --rm -v "$cert_path:/cert.pfx" bitbetter/licensegen "$@" -else - docker run --rm -v "$cert_path:/cert.pfx" bitbetter/licensegen "$@" -fi diff --git a/update-bitwarden.sh b/update-bitwarden.sh deleted file mode 100755 index 069f983..0000000 --- a/update-bitwarden.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash -ask () { - local __resultVar=$1 - local __result="$2" - if [ -z "$2" ]; then - read -p "$3" __result - fi - eval $__resultVar="'$__result'" -} - -SCRIPT_BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -BW_VERSION=$(curl -sL https://go.btwrdn.co/bw-sh-versions | grep '^ *"'coreVersion'":' | awk -F\: '{ print $2 }' | sed -e 's/,$//' -e 's/^"//' -e 's/"$//') - -echo "Starting Bitwarden update, newest server version: $BW_VERSION" - -# Default path is the parent directory of the BitBetter location -BITWARDEN_BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/dev/null 2>&1 && pwd )" - -# Get Bitwarden base from user (or keep default value) or use first argument -ask tmpbase "$1" "Enter Bitwarden base directory [$BITWARDEN_BASE]: " -BITWARDEN_BASE=${tmpbase:-$BITWARDEN_BASE} - -# Check if directory exists and is valid -[ -d "$BITWARDEN_BASE" ] || { echo "Bitwarden base directory $BITWARDEN_BASE not found!"; exit 1; } -[ -f "$BITWARDEN_BASE/bitwarden.sh" ] || { echo "Bitwarden base directory $BITWARDEN_BASE is not valid!"; exit 1; } - -# Check if user wants to recreate the docker-compose override file -RECREATE_OV="y" -ask tmprecreate "$2" "Rebuild docker-compose override? [Y/n]: " -RECREATE_OV=${tmprecreate:-$RECREATE_OV} - -if [[ $RECREATE_OV =~ ^[Yy]$ ]] -then - { - echo "version: '3'" - echo "" - echo "services:" - echo " api:" - echo " image: bitbetter/api:$BW_VERSION" - echo "" - echo " identity:" - echo " image: bitbetter/identity:$BW_VERSION" - echo "" - } > $BITWARDEN_BASE/bwdata/docker/docker-compose.override.yml - echo "BitBetter docker-compose override created!" -else - echo "Make sure to check if the docker override contains the correct image version ($BW_VERSION) in $BITWARDEN_BASE/bwdata/docker/docker-compose.override.yml!" -fi - -# Check if user wants to rebuild the bitbetter images -docker images bitbetter/api --format="{{ .Tag }}" | grep -F -- "${BW_VERSION}" > /dev/null -retval=$? -REBUILD_BB="n" -REBUILD_BB_DESCR="[y/N]" -if [ $retval -ne 0 ]; then - REBUILD_BB="y" - REBUILD_BB_DESCR="[Y/n]" -fi -ask tmprebuild "$3" "Rebuild BitBetter images? $REBUILD_BB_DESCR: " -REBUILD_BB=${tmprebuild:-$REBUILD_BB} - -if [[ $REBUILD_BB =~ ^[Yy]$ ]] -then - $SCRIPT_BASE/build.sh - echo "BitBetter images updated to version: $BW_VERSION" -fi - -# Now start the bitwarden update -cd $BITWARDEN_BASE - -./bitwarden.sh updateself - -# Update the bitwarden.sh: automatically patch run.sh to fix docker-compose pull errors for private images -awk '1;/function downloadRunFile/{c=6}c&&!--c{print "sed -i '\''s/dccmd pull/dccmd pull --ignore-pull-failures || true/g'\'' $SCRIPTS_DIR/run.sh"}' $BITWARDEN_BASE/bitwarden.sh > tmp_bw.sh && mv tmp_bw.sh $BITWARDEN_BASE/bitwarden.sh -chmod +x $BITWARDEN_BASE/bitwarden.sh -echo "Patching bitwarden.sh completed..." - -./bitwarden.sh update - -# Prune Docker images without at least one container associated to them. -echo "Pruning Docker images without at least one container associated to them..." -docker image prune -a - -cd $SCRIPT_BASE -echo "Bitwarden update completed!"