From b3a90a8ad6cd8044d44ef3a3b43a0921233df9e8 Mon Sep 17 00:00:00 2001
From: decentral1se <lukewm@riseup.net>
Date: Mon, 15 Mar 2021 18:27:23 +0000
Subject: [PATCH] Add djangoldp_crypto app

See https://git.startinblox.com/djangoldp-packages/djangoldp/issues/236.
---
 .gitlab-ci.yml                                | 11 ++++++
 djangoldp_crypto/README.md                    | 26 +++++++++++++
 djangoldp_crypto/__init__.py                  |  0
 djangoldp_crypto/admin.py                     | 13 +++++++
 djangoldp_crypto/management/__init__.py       |  0
 .../management/commands/__init__.py           |  0
 .../management/commands/creatersakey.py       | 19 ++++++++++
 djangoldp_crypto/migrations/0001_initial.py   | 25 +++++++++++++
 djangoldp_crypto/migrations/__init__.py       |  0
 djangoldp_crypto/models.py                    | 37 +++++++++++++++++++
 djangoldp_crypto/tests/runner.py              | 23 ++++++++++++
 djangoldp_crypto/tests/settings_default.py    | 12 ++++++
 djangoldp_crypto/tests/tests_rsakey.py        | 12 ++++++
 setup.cfg                                     |  2 +
 14 files changed, 180 insertions(+)
 create mode 100644 djangoldp_crypto/README.md
 create mode 100644 djangoldp_crypto/__init__.py
 create mode 100644 djangoldp_crypto/admin.py
 create mode 100644 djangoldp_crypto/management/__init__.py
 create mode 100644 djangoldp_crypto/management/commands/__init__.py
 create mode 100644 djangoldp_crypto/management/commands/creatersakey.py
 create mode 100644 djangoldp_crypto/migrations/0001_initial.py
 create mode 100644 djangoldp_crypto/migrations/__init__.py
 create mode 100644 djangoldp_crypto/models.py
 create mode 100644 djangoldp_crypto/tests/runner.py
 create mode 100644 djangoldp_crypto/tests/settings_default.py
 create mode 100644 djangoldp_crypto/tests/tests_rsakey.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8814aaca..bd3470f0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,6 +16,17 @@ test:
   tags:
     - test
 
+crypto-test:
+  stage: test
+  script:
+    - pip install .[crypto]
+    - python -m unittest djangoldp_crypto.tests.runner
+  except:
+    - master
+    - tags
+  tags:
+    - test
+
 publish:
   stage: release
   before_script:
diff --git a/djangoldp_crypto/README.md b/djangoldp_crypto/README.md
new file mode 100644
index 00000000..d493c3d2
--- /dev/null
+++ b/djangoldp_crypto/README.md
@@ -0,0 +1,26 @@
+# djangoldp-crypto
+
+Packages like [djangoldp](https://git.startinblox.com/djangoldp-packages/djangoldp) and [django-webidoidc-provider](https://git.startinblox.com/djangoldp-packages/django-webidoidc-provider) have some models and utilities which make use of cryptography. In general, we want to re-use that code in a supporting package to avoid duplication of effort. However, until it is more clear what ca be re-used, we are using this separate django app in this package. See [this ticket](https://git.startinblox.com/djangoldp-packages/djangoldp/issues/236) for more.
+
+## Install
+
+```bash
+$ python -m pip install 'djangoldp[crypto]'
+```
+
+Tnen add the app to your `settings.yml` like so:
+
+```yaml
+INSTALLED_APPS:
+  - djangoldp_crypto
+```
+
+## Management commands
+
+- `creatersakey`: Randomly generate a new RSA key for the DjangoLDP server
+
+## Test
+
+```bash
+$ python -m unittest djangoldp_crypto.tests.runner
+```
diff --git a/djangoldp_crypto/__init__.py b/djangoldp_crypto/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangoldp_crypto/admin.py b/djangoldp_crypto/admin.py
new file mode 100644
index 00000000..9bd35f63
--- /dev/null
+++ b/djangoldp_crypto/admin.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+
+from djangoldp_crypto.models import RSAKey
+
+
+@admin.register(RSAKey)
+class RSAKeyAdmin(admin.ModelAdmin):
+
+    readonly_fields = ['kid', 'pub_key']
+
+    def save_model(self, request, obj, form, change):
+        obj.priv_key.replace('\r', '')
+        super().save_model(request, obj, form, change)
diff --git a/djangoldp_crypto/management/__init__.py b/djangoldp_crypto/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangoldp_crypto/management/commands/__init__.py b/djangoldp_crypto/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangoldp_crypto/management/commands/creatersakey.py b/djangoldp_crypto/management/commands/creatersakey.py
new file mode 100644
index 00000000..7f56c563
--- /dev/null
+++ b/djangoldp_crypto/management/commands/creatersakey.py
@@ -0,0 +1,19 @@
+from Cryptodome.PublicKey import RSA
+from django.core.management.base import BaseCommand
+from djangoldp_crypto.models import RSAKey
+
+
+class Command(BaseCommand):
+    help = 'Randomly generate a new RSA key for the DjangoLDP server'
+
+    def handle(self, *args, **options):
+        try:
+            key = RSA.generate(2048)
+            rsakey = RSAKey(priv_key=key.exportKey('PEM').decode('utf8'))
+            rsakey.save()
+            self.stdout.write('RSA key successfully created')
+            self.stdout.write(u'Private key: \n{0}'.format(rsakey.priv_key))
+            self.stdout.write(u'Public key: \n{0}'.format(rsakey.pub_key))
+            self.stdout.write(u'Key ID: \n{0}'.format(rsakey.kid))
+        except Exception as e:
+            self.stdout.write('Something goes wrong: {0}'.format(e))
diff --git a/djangoldp_crypto/migrations/0001_initial.py b/djangoldp_crypto/migrations/0001_initial.py
new file mode 100644
index 00000000..e6d6add6
--- /dev/null
+++ b/djangoldp_crypto/migrations/0001_initial.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.19 on 2021-03-15 10:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='RSAKey',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('priv_key', models.TextField(help_text='Paste your private RSA Key here.', unique=True, verbose_name='Key')),
+            ],
+            options={
+                'verbose_name': 'RSA Key',
+                'verbose_name_plural': 'RSA Keys',
+            },
+        ),
+    ]
diff --git a/djangoldp_crypto/migrations/__init__.py b/djangoldp_crypto/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangoldp_crypto/models.py b/djangoldp_crypto/models.py
new file mode 100644
index 00000000..07c64013
--- /dev/null
+++ b/djangoldp_crypto/models.py
@@ -0,0 +1,37 @@
+from hashlib import md5
+
+from Cryptodome.PublicKey import RSA
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class RSAKey(models.Model):
+
+    priv_key = models.TextField(
+        verbose_name=_(u'Key'), unique=True,
+        help_text=_(u'Paste your private RSA Key here.'))
+
+    class Meta:
+        verbose_name = _(u'RSA Key')
+        verbose_name_plural = _(u'RSA Keys')
+
+    def __str__(self):
+        return u'{0}'.format(self.kid)
+
+    def __unicode__(self):
+        return self.__str__()
+
+    @property
+    def kid(self):
+        if not self.priv_key:
+            return ''
+
+        return u'{0}'.format(md5(self.priv_key.encode('utf-8')).hexdigest())
+
+    @property
+    def pub_key(self):
+        if not self.priv_key:
+            return ''
+
+        _pub_key = RSA.importKey(self.priv_key).publickey()
+        return _pub_key.export_key().decode('utf-8')
diff --git a/djangoldp_crypto/tests/runner.py b/djangoldp_crypto/tests/runner.py
new file mode 100644
index 00000000..65ba2d2e
--- /dev/null
+++ b/djangoldp_crypto/tests/runner.py
@@ -0,0 +1,23 @@
+import sys
+
+import django
+import yaml
+from django.conf import settings as django_settings
+from djangoldp.conf.ldpsettings import LDPSettings
+from djangoldp_crypto.tests.settings_default import yaml_config
+
+# load test config
+config = yaml.safe_load(yaml_config)
+ldpsettings = LDPSettings(config)
+django_settings.configure(ldpsettings)
+
+django.setup()
+from django.test.runner import DiscoverRunner
+
+test_runner = DiscoverRunner(verbosity=1)
+
+failures = test_runner.run_tests([
+    'djangoldp_crypto.tests.tests_rsakey',
+])
+if failures:
+    sys.exit(failures)
diff --git a/djangoldp_crypto/tests/settings_default.py b/djangoldp_crypto/tests/settings_default.py
new file mode 100644
index 00000000..da76f6c5
--- /dev/null
+++ b/djangoldp_crypto/tests/settings_default.py
@@ -0,0 +1,12 @@
+"""This module contains YAML configurations for djangoldp_crypto testing."""
+
+yaml_config = """
+dependencies:
+
+ldppackages:
+  - djangoldp_crypto.tests
+
+server:
+  INSTALLED_APPS:
+    - djangoldp_crypto
+"""
diff --git a/djangoldp_crypto/tests/tests_rsakey.py b/djangoldp_crypto/tests/tests_rsakey.py
new file mode 100644
index 00000000..d41e1fa7
--- /dev/null
+++ b/djangoldp_crypto/tests/tests_rsakey.py
@@ -0,0 +1,12 @@
+from Cryptodome.PublicKey import RSA
+from django.db import IntegrityError
+from django.test import TestCase
+from djangoldp_crypto.models import RSAKey
+
+
+class TestRSAKey(TestCase):
+    def test_rsakey_unique(self):
+        priv_key = RSA.generate(2048)
+        RSAKey.objects.create(priv_key=priv_key)
+        with self.assertRaises(IntegrityError):
+            RSAKey.objects.create(priv_key=priv_key)
diff --git a/setup.cfg b/setup.cfg
index 8858c1ae..3dd375f7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,6 +38,8 @@ console_scripts =
 dev =
     validators
     factory_boy >= 2.11.0
+crypto =
+    pycryptodomex~=3.10
 
 [semantic_release]
 version_source = tag
-- 
GitLab