diff --git a/Dockerfile b/Dockerfile index da0d9c1..8e74a57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.9-alpine3.12 +FROM python:3.10.4-alpine3.15 LABEL maintainer="graham@grahamgilbert.com" diff --git a/README.md b/README.md index 50bef3c..afc2459 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,45 @@ -Crypt-Server -============ -__[Crypt][1]__ is a tool for securely storing secrets such as FileVault 2 recovery keys. It is made up of a client app, and a Django web app for storing the keys. +# Crypt-Server + +**[Crypt][1]** is a tool for securely storing secrets such as FileVault 2 recovery keys. It is made up of a client app, and a Django web app for storing the keys. This Docker image contains the fully configured Crypt Django web app. A default admin user has been preconfigured, use admin/password to login. If you intend on using the server for anything semi-serious it is a good idea to change the password or add a new admin user and delete the default one. -__Features__ -======= +## Features + - Secrets are encrypted in the database - All access is audited - all reasons for retrieval and approval are logged along side the users performing the actions - Two step approval for retrieval of secrets is enabled by default - Approval permission can be given to all users (so just any two users need to approve the retrieval) or a specific group of users - [1]: https://github.com/grahamgilbert/Crypt ## Installation instructions + It is recommended that you use [Docker](https://github.com/grahamgilbert/Crypt-Server/blob/master/docs/Docker.md) to run this, but if you wish to run directly on a host, installation instructions are over in the [docs directory](https://github.com/grahamgilbert/Crypt-Server/blob/master/docs/Installation_on_Ubuntu_1404.md) +### Migrating from versions earlier than Crypt 3.0 + +Crypt 3 changed it's encryption backend, so when migrating from versions earlier than Crypt 3.0, you should first run Crypt 3.2.0 to perform the migration, and then upgrade to the latest version. The last version to support legacy migrations was Crypt 3.2. + ## Settings All settings that would be entered into `settings.py` can also be passed into the Docker container as environment variables. -* ``FIELD_ENCRYPTION_KEY`` - The key to use when encrypting the secrets. This is required. +- `FIELD_ENCRYPTION_KEY` - The key to use when encrypting the secrets. This is required. -* ``SEND_EMAIL`` - Crypt Server can send email notifcations when secrets are requested and approved. Set ``SEND_EMAIL`` to True, and set ``HOST_NAME`` to your server's host and URL scheme (e.g. ``https://crypt.example.com``). For configuring your email settings, see the [Django documentation](https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-EMAIL_HOST). +- `SEND_EMAIL` - Crypt Server can send email notifcations when secrets are requested and approved. Set `SEND_EMAIL` to True, and set `HOST_NAME` to your server's host and URL scheme (e.g. `https://crypt.example.com`). For configuring your email settings, see the [Django documentation](https://docs.djangoproject.com/en/3.1/ref/settings/#std:setting-EMAIL_HOST). -* ``EMAIL_SENDER`` - The email address to send emaiil notifications from when secrets are requests and approved. Ensure this is verified if you are using SES. Does nothing unless ``SEND_EMAIIL`` is True. +- `EMAIL_SENDER` - The email address to send emaiil notifications from when secrets are requests and approved. Ensure this is verified if you are using SES. Does nothing unless `SEND_EMAIIL` is True. -* ``APPROVE_OWN`` - By default, users with approval permissons can approve their own key requests. By setting this to False in settings.py (or by using the `APPROVE_OWN` environment variable with Docker), users cannot approve their own requests. +- `APPROVE_OWN` - By default, users with approval permissons can approve their own key requests. By setting this to False in settings.py (or by using the `APPROVE_OWN` environment variable with Docker), users cannot approve their own requests. -* ``ALL_APPROVE`` - By default, users need to be explicitly given approval permissions to approve key retrieval requests. By setting this to True in `settings.py`, all users are given this permission when they log in. - -* ``ROTATE_VIEWED_SECRETS`` - With a compatible client (such as Crypt 3.2.0 and greater), Crypt Server can instruct the client to rotate the secret and re-escrow it when the secret has been viewed. Enable by setting this to `True` or by using `ROTATE_VIEWED_SECRETS` and setting to `true`. +- `ALL_APPROVE` - By default, users need to be explicitly given approval permissions to approve key retrieval requests. By setting this to True in `settings.py`, all users are given this permission when they log in. +- `ROTATE_VIEWED_SECRETS` - With a compatible client (such as Crypt 3.2.0 and greater), Crypt Server can instruct the client to rotate the secret and re-escrow it when the secret has been viewed. Enable by setting this to `True` or by using `ROTATE_VIEWED_SECRETS` and setting to `true`. ## Screenshots + Main Page: ![Crypt Main Page](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/home.png) diff --git a/docker/run.sh b/docker/run.sh index 7a8d0b0..ddd7128 100755 --- a/docker/run.sh +++ b/docker/run.sh @@ -4,7 +4,7 @@ set -e cd $APP_DIR ADMIN_PASS=${ADMIN_PASS:-} -python3 generate_keyczart.py +# python3 generate_keyczart.py python3 manage.py migrate --noinput if [ ! -z "$ADMIN_PASS" ] ; then diff --git a/docker/run_docker.sh b/docker/run_docker.sh index 7f0882b..7c9dcfa 100755 --- a/docker/run_docker.sh +++ b/docker/run_docker.sh @@ -1,5 +1,6 @@ CWD=`pwd` docker rm -f crypt + docker build -t macadmins/crypt . docker run -d \ -e ADMIN_PASS=pass \ @@ -8,7 +9,6 @@ docker run -d \ --name=crypt \ --restart="always" \ -v "$CWD/crypt.db":/home/docker/crypt/crypt.db \ - -v "$CWD/keyset":/home/docker/crypt/keyset \ -e FIELD_ENCRYPTION_KEY=jKAv1Sde8m6jCYFnmps0iXkUfAilweNVjbvoebBrDwg= \ -p 8000-8050:8000-8050 \ macadmins/crypt diff --git a/docker/settings_import.py b/docker/settings_import.py index 2f3135d..e498e1b 100644 --- a/docker/settings_import.py +++ b/docker/settings_import.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python from os import getenv import locale diff --git a/fvserver/context_processors.py b/fvserver/context_processors.py index 7f8ccde..ddfca9c 100644 --- a/fvserver/context_processors.py +++ b/fvserver/context_processors.py @@ -5,5 +5,8 @@ def crypt_version(request): # return the value you want as a dictionary. you may add multiple values in there. current_dir = os.path.dirname(os.path.realpath(__file__)) - version = plistlib.readPlist(os.path.join(current_dir, "version.plist")) + with open( + os.path.join(os.path.dirname(current_dir), "fvserver", "version.plist"), "rb" + ) as f: + version = plistlib.load(f) return {"CRYPT_VERSION": version["version"]} diff --git a/fvserver/version.plist b/fvserver/version.plist index 5a1be97..29d6c63 100644 --- a/fvserver/version.plist +++ b/fvserver/version.plist @@ -3,6 +3,6 @@ version - 3.2.0.343 + 3.3.0.355 diff --git a/remote_build.py b/remote_build.py index d626f37..ae10abe 100644 --- a/remote_build.py +++ b/remote_build.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import subprocess import requests diff --git a/server/migrations/0001_squashed_0017_merge_20181217_1829.py b/server/migrations/0001_squashed_0017_merge_20181217_1829.py new file mode 100644 index 0000000..cd1addf --- /dev/null +++ b/server/migrations/0001_squashed_0017_merge_20181217_1829.py @@ -0,0 +1,333 @@ +# Generated by Django 2.2.27 on 2022-03-28 14:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +from django.shortcuts import get_object_or_404 + + +# Functions from the following migrations need manual copying. +# Move them and any dependencies into this file, then update the +# RunPython operations to refer to the local versions: +# server.migrations.0003_auto_20150713_1215 +# server.migrations.0007_auto_20150714_0822 +# server.migrations.0010_auto_20180726_1700 + + +def move_keys_and_requests(apps, schema_editor): + seen_serials = [("dummy_serial", "dummy_id")] + Computer = apps.get_model("server", "Computer") + Secret = apps.get_model("server", "Secret") + Request = apps.get_model("server", "Request") + for computer in Computer.objects.all(): + # if we've seen the serial before, get the computer that we saw before + target_id = None + for serial, id in seen_serials: + if computer.serial == serial: + target_id = id + break + if target_id == None: + target_id = computer.id + + target_computer = get_object_or_404(Computer, pk=target_id) + # create a new secret + secret = Secret( + computer=target_computer, + secret=computer.recovery_key, + date_escrowed=computer.last_checkin, + ) + secret.save() + + requests = Request.objects.filter(computer=computer) + for request in requests: + request.secret = secret + request.save() + + if target_computer.id != computer.id: + # Dupe computer, bin it + computer.delete() + + +class Migration(migrations.Migration): + + replaces = [ + ("server", "0001_initial"), + ("server", "0002_auto_20150713_1214"), + ("server", "0003_auto_20150713_1215"), + ("server", "0004_auto_20150713_1216"), + ("server", "0005_auto_20150713_1754"), + ("server", "0006_auto_20150714_0821"), + ("server", "0007_auto_20150714_0822"), + ("server", "0008_auto_20150814_2140"), + ("server", "0009_secret_rotation_required"), + ("server", "0010_auto_20180726_1700"), + ("server", "0011_manual_unique_serials"), + ("server", "0012_auto_20181128_2038"), + ("server", "0016_auto_20181213_2145"), + ("server", "0009_auto_20180430_2024"), + ("server", "0017_merge_20181217_1829"), + ] + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Computer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "recovery_key", + models.CharField(max_length=200, verbose_name=b"Recovery Key"), + ), + ( + "serial", + models.CharField(max_length=200, verbose_name=b"Serial Number"), + ), + ( + "username", + models.CharField(max_length=200, verbose_name=b"User Name"), + ), + ( + "computername", + models.CharField(max_length=200, verbose_name=b"Computer Name"), + ), + ("last_checkin", models.DateTimeField(blank=True, null=True)), + ], + options={ + "ordering": ["serial"], + "permissions": ( + ("can_approve", "Can approve requests to see encryption keys"), + ), + }, + ), + migrations.CreateModel( + name="Secret", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("secret", models.CharField(max_length=256)), + ( + "secret_type", + models.CharField( + choices=[ + (b"recovery_key", b"Recovery Key"), + (b"password", b"Password"), + ], + default=b"recovery_key", + max_length=256, + ), + ), + ("date_escrowed", models.DateTimeField(auto_now_add=True)), + ( + "computer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="server.Computer", + ), + ), + ], + ), + migrations.CreateModel( + name="Request", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("approved", models.NullBooleanField(verbose_name=b"Approved?")), + ("reason_for_request", models.TextField()), + ( + "reason_for_approval", + models.TextField( + blank=True, null=True, verbose_name=b"Approval Notes" + ), + ), + ("date_requested", models.DateTimeField(auto_now_add=True)), + ("date_approved", models.DateTimeField(blank=True, null=True)), + ("current", models.BooleanField(default=True)), + ( + "auth_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="auth_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "computer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="computers", + to="server.Computer", + ), + ), + ( + "requesting_user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="requesting_user", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "secret", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="secrets", + to="server.Secret", + ), + ), + ], + ), + migrations.RunPython( + code=move_keys_and_requests, + ), + migrations.RemoveField( + model_name="computer", + name="recovery_key", + ), + migrations.RemoveField( + model_name="request", + name="computer", + ), + migrations.AlterField( + model_name="request", + name="secret", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="server.Secret" + ), + ), + migrations.AlterModelOptions( + name="secret", + options={"ordering": ["-date_escrowed"]}, + ), + migrations.AddField( + model_name="secret", + name="rotation_required", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="computer", + name="serial", + field=models.CharField( + max_length=200, unique=True, verbose_name=b"Serial Number" + ), + ), + migrations.AlterField( + model_name="computer", + name="computername", + field=models.CharField(max_length=200, verbose_name="Computer Name"), + ), + migrations.AlterField( + model_name="computer", + name="serial", + field=models.CharField( + max_length=200, unique=True, verbose_name="Serial Number" + ), + ), + migrations.AlterField( + model_name="computer", + name="username", + field=models.CharField(max_length=200, verbose_name="User Name"), + ), + migrations.AlterField( + model_name="request", + name="approved", + field=models.NullBooleanField(verbose_name="Approved?"), + ), + migrations.AlterField( + model_name="request", + name="reason_for_approval", + field=models.TextField( + blank=True, null=True, verbose_name="Approval Notes" + ), + ), + migrations.AlterField( + model_name="secret", + name="secret_type", + field=models.CharField( + choices=[("recovery_key", "Recovery Key"), ("password", "Password")], + default="recovery_key", + max_length=256, + ), + ), + migrations.AlterField( + model_name="request", + name="auth_user", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="auth_user", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="request", + name="secret", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="server.Secret" + ), + ), + migrations.AlterField( + model_name="computer", + name="computername", + field=models.CharField(max_length=200, verbose_name="Computer Name"), + ), + migrations.AlterField( + model_name="computer", + name="serial", + field=models.CharField(max_length=200, verbose_name="Serial Number"), + ), + migrations.AlterField( + model_name="computer", + name="username", + field=models.CharField(max_length=200, verbose_name="User Name"), + ), + migrations.AlterField( + model_name="request", + name="approved", + field=models.NullBooleanField(verbose_name="Approved?"), + ), + migrations.AlterField( + model_name="request", + name="reason_for_approval", + field=models.TextField( + blank=True, null=True, verbose_name="Approval Notes" + ), + ), + migrations.AlterField( + model_name="secret", + name="secret_type", + field=models.CharField( + choices=[("recovery_key", "Recovery Key"), ("password", "Password")], + default="recovery_key", + max_length=256, + ), + ), + ] diff --git a/server/migrations/0006_auto_20150714_0821.py b/server/migrations/0006_auto_20150714_0821.py index a1b7e7c..bf51afa 100644 --- a/server/migrations/0006_auto_20150714_0821.py +++ b/server/migrations/0006_auto_20150714_0821.py @@ -2,7 +2,8 @@ from __future__ import unicode_literals from django.db import models, migrations -import django_extensions.db.fields.encrypted + +# import django_extensions.db.fields.encrypted class Migration(migrations.Migration): @@ -10,11 +11,11 @@ class Migration(migrations.Migration): dependencies = [("server", "0005_auto_20150713_1754")] operations = [ - migrations.AlterField( - model_name="secret", - name="secret", - field=django_extensions.db.fields.encrypted.EncryptedCharField( - max_length=256 - ), - ) + # migrations.AlterField( + # model_name="secret", + # name="secret", + # field=django_extensions.db.fields.encrypted.EncryptedCharField( + # max_length=256 + # ), + # ) ] diff --git a/server/migrations/0013_secret_new_secret.py b/server/migrations/0013_secret_new_secret.py deleted file mode 100644 index b3ec5ff..0000000 --- a/server/migrations/0013_secret_new_secret.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-11-30 17:59 -from __future__ import unicode_literals - -from django.db import migrations -import encrypted_model_fields.fields - - -class Migration(migrations.Migration): - - dependencies = [("server", "0012_auto_20181128_2038")] - - operations = [ - migrations.AddField( - model_name="secret", - name="new_secret", - field=encrypted_model_fields.fields.EncryptedCharField(default=""), - preserve_default=False, - ) - ] diff --git a/server/migrations/0014_migrate_secrets.py b/server/migrations/0014_migrate_secrets.py deleted file mode 100644 index 2475a7c..0000000 --- a/server/migrations/0014_migrate_secrets.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from server.models import * -from django.db import migrations, models -from django.conf import settings - - -def migrate_secrets(apps, schema_editor): - """ - Migrate the secrets to the new field - """ - - if not hasattr(settings, "FIELD_ENCRYPTION_KEY"): - raise Exception("FIELD_ENCRYPTION_KEY not set in settings.py") - - if settings.FIELD_ENCRYPTION_KEY == "": - raise Exception("FIELD_ENCRYPTION_KEY not configured correctly in settings.py") - - Secret = apps.get_model("server", "Secret") - secrets_to_update = Secret.objects.all() - for secret_to_update in secrets_to_update: - secret_to_update.new_secret = secret_to_update.secret - secret_to_update.save() - - -class Migration(migrations.Migration): - - dependencies = [("server", "0013_secret_new_secret")] - - operations = [migrations.RunPython(migrate_secrets)] diff --git a/server/migrations/0015_secret_remove_old_secret.py b/server/migrations/0015_secret_remove_old_secret.py deleted file mode 100644 index 937d3ab..0000000 --- a/server/migrations/0015_secret_remove_old_secret.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2018-11-30 17:59 -from __future__ import unicode_literals - -from django.db import migrations -import encrypted_model_fields.fields - - -class Migration(migrations.Migration): - - dependencies = [("server", "0014_migrate_secrets")] - - operations = [ - migrations.RemoveField(model_name="secret", name="secret"), - migrations.RenameField( - model_name="secret", old_name="new_secret", new_name="secret" - ), - ] diff --git a/server/migrations/0016_auto_20181213_2145.py b/server/migrations/0016_auto_20181213_2145.py index 51d1e2d..3a5f9b9 100644 --- a/server/migrations/0016_auto_20181213_2145.py +++ b/server/migrations/0016_auto_20181213_2145.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): - dependencies = [("server", "0015_secret_remove_old_secret")] + dependencies = [("server", "0012_auto_20181128_2038")] operations = [ migrations.AlterField( diff --git a/server/models.py b/server/models.py index 9cf2c28..b8dac82 100644 --- a/server/models.py +++ b/server/models.py @@ -19,7 +19,7 @@ def __str__(self): class Meta: ordering = ["serial"] permissions = ( - ("can_approve", (u"Can approve requests to see encryption keys")), + ("can_approve", ("Can approve requests to see encryption keys")), ) diff --git a/server/views.py b/server/views.py index d80a863..d954561 100644 --- a/server/views.py +++ b/server/views.py @@ -36,9 +36,11 @@ def cleanup(): def get_server_version(): current_dir = os.path.dirname(os.path.realpath(__file__)) - version = plistlib.readPlist( - os.path.join(os.path.dirname(current_dir), "fvserver", "version.plist") - ) + + with open( + os.path.join(os.path.dirname(current_dir), "fvserver", "version.plist"), "rb" + ) as f: + version = plistlib.load(f) return version["version"] diff --git a/set_build_no.py b/set_build_no.py index da3c045..6ad8310 100755 --- a/set_build_no.py +++ b/set_build_no.py @@ -4,7 +4,7 @@ import plistlib import subprocess -current_version = "3.2.1" +current_version = "3.3.0" script_path = os.path.dirname(os.path.realpath(__file__)) diff --git a/setup/requirements.txt b/setup/requirements.txt index 49be29d..8f787ac 100644 --- a/setup/requirements.txt +++ b/setup/requirements.txt @@ -3,7 +3,7 @@ asn1crypto==0.24.0 astroid==2.0.4 attrs==18.2.0 black==21.7b0 -cffi==1.12 +cffi==1.15 Click==8.0.1 cryptography==3.3.2 Django==2.2.27 @@ -29,7 +29,6 @@ pycparser==2.19 pycrypto==2.6.1 pyflakes==2.0.0 pylint==2.1.1 -python3-keyczar==0.71rc0 pytz==2018.7 regex==2021.8.3 six==1.9.0