diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index d3fd3ab2..7656396c 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -156,7 +156,7 @@ def get_client_alg_keys(client): if client.jwt_alg == 'RS256': keys = [] for rsakey in RSAKey.objects.all(): - keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid)) + keys.append(jwk_RSAKey(key=importKey(rsakey.pem), kid=rsakey.kid)) if not keys: raise Exception('You must add at least one RSA Key.') elif client.jwt_alg == 'HS256': diff --git a/oidc_provider/management/commands/creatersakey.py b/oidc_provider/management/commands/creatersakey.py index 9d609c56..4f478f39 100644 --- a/oidc_provider/management/commands/creatersakey.py +++ b/oidc_provider/management/commands/creatersakey.py @@ -1,16 +1,32 @@ from Cryptodome.PublicKey import RSA from django.core.management.base import BaseCommand from oidc_provider.models import RSAKey +from oidc_provider import settings + + +encrypt = settings.import_hook('OIDC_RSA_ENCRYPT_HOOK') +decrypt = settings.import_hook('OIDC_RSA_DECRYPT_HOOK') class Command(BaseCommand): help = 'Randomly generate a new RSA key for the OpenID server' - def handle(self, *args, **options): - try: - key = RSA.generate(2048) - rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8')) - rsakey.save() - self.stdout.write(u'RSA key successfully created with kid: {0}'.format(rsakey.kid)) - except Exception as e: - self.stdout.write('Something goes wrong: {0}'.format(e)) + def add_arguments(self, parser): + if encrypt is None: + return + + parser.add_argument( + '--encrypted', + '-e', + action='store_true', + help='Encrypt key', + ) + + def handle(self, *_, **options): + key = RSA.generate(2048) + rsakey = RSAKey( + key=key.exportKey('PEM').decode('utf8'), + encrypted=options.get('encrypted', False), + ) + rsakey.save() + self.stdout.write(u'RSA key successfully created with kid: {0}'.format(rsakey.kid)) diff --git a/oidc_provider/migrations/0027_add_rsakey_encrypted.py b/oidc_provider/migrations/0027_add_rsakey_encrypted.py new file mode 100644 index 00000000..7e100ead --- /dev/null +++ b/oidc_provider/migrations/0027_add_rsakey_encrypted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2024-04-05 16:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_provider', '0026_client_multiple_response_types'), + ] + + operations = [ + migrations.AddField( + model_name='rsakey', + name='encrypted', + field=models.BooleanField(default=False), + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 3f66f7e4..02914895 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -4,10 +4,12 @@ from hashlib import sha256 import json +from django.core.exceptions import ValidationError from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.conf import settings +from oidc_provider import settings as oidc_settings CLIENT_TYPE_CHOICES = [ @@ -30,6 +32,10 @@ ] +rsa_encrypt = oidc_settings.import_hook("OIDC_RSA_ENCRYPT_HOOK") +rsa_decrypt = oidc_settings.import_hook("OIDC_RSA_DECRYPT_HOOK") + + class ResponseTypeManager(models.Manager): def get_by_natural_key(self, value): return self.get(value=value) @@ -251,6 +257,7 @@ class RSAKey(models.Model): key = models.TextField( verbose_name=_(u'Key'), help_text=_(u'Paste your private RSA Key here.')) + encrypted = models.BooleanField(default=False) class Meta: verbose_name = _(u'RSA Key') @@ -262,10 +269,38 @@ def __str__(self): def __unicode__(self): return self.__str__() + def clean(self): + super().clean() + if self.encrypted and rsa_encrypt is None: + raise ValidationError( + "Could not encrypt key value. settings.OIDC_RSA_ENCRYPT_HOOK is not defined." + ) + + def save(self, *args, **kwargs): + if self.encrypted: + encrypted = rsa_encrypt(self.key.encode()) + self.key = base64.b64encode(encrypted).decode() + super().save(*args, **kwargs) + + @property + def pem(self): + if not self.encrypted: + return self.key + + if rsa_decrypt is None: + raise AttributeError( + "Could not decrypt key value. settings.OIDC_RSA_DECRYPT_HOOK is not defined." + ) + + encrypted = base64.b64decode(self.key) + return rsa_decrypt(encrypted).decode() + @property def kid(self): - return u'{0}'.format( - hashlib.new("md5", self.key.encode('utf-8'), usedforsecurity=False).hexdigest() + return "{0}".format( + hashlib.new( + "md5", self.pem.encode("utf-8"), usedforsecurity=False + ).hexdigest() if self.key else '' ) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 9d30759f..ac15f52f 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -214,6 +214,20 @@ def OIDC_INTROSPECTION_RESPONSE_SCOPE_ENABLE(self): """ return False + @property + def OIDC_RSA_ENCRYPT_HOOK(self): + """ + OPTIONAL. A string with the location of a function used to encrypt RSA keys. + """ + return None + + @property + def OIDC_RSA_DECRYPT_HOOK(self): + """ + OPTIONAL. A string with the location of a function used to decrypt RSA keys. + """ + return None + default_settings = DefaultSettings() @@ -234,6 +248,8 @@ def import_from_str(value): def import_hook(hook_name): hook_path = get(hook_name.upper()) + if hook_path is None: + return None return import_from_str(hook_path) diff --git a/oidc_provider/version.py b/oidc_provider/version.py index 45a85473..26bc3471 100644 --- a/oidc_provider/version.py +++ b/oidc_provider/version.py @@ -1 +1 @@ -__version__ = '0.8.3+orm' +__version__ = '0.8.4+orm' diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 7801444d..9a0e23f8 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -327,7 +327,7 @@ def get(self, request, *args, **kwargs): dic = dict(keys=[]) for rsakey in RSAKey.objects.all(): - public_key = RSA.importKey(rsakey.key).publickey() + public_key = RSA.importKey(rsakey.pem).publickey() dic['keys'].append({ 'kty': 'RSA', 'alg': 'RS256',