diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index f2467c365..895ea5695 100644 --- a/netbox/project-static/js/secrets.js +++ b/netbox/project-static/js/secrets.js @@ -1,38 +1,20 @@ $(document).ready(function() { // Unlocking a secret - $('button.unlock-secret').click(function (event) { + $('button.unlock-secret').click(function() { var secret_id = $(this).attr('secret-id'); - - // If we have an active cookie containing a session key, send the API request. - if (document.cookie.indexOf('session_key') > 0) { - console.log("Retrieving secret..."); - unlock_secret(secret_id); - // Otherwise, prompt the user for a private key so we can request a session key. - } else { - console.log("No session key found. Prompt user for private key."); - $('#privkey_modal').modal('show'); - } - + unlock_secret(secret_id); }); // Locking a secret - $('button.lock-secret').click(function (event) { + $('button.lock-secret').click(function() { var secret_id = $(this).attr('secret-id'); - var secret_div = $('#secret_' + secret_id); - - // Delete the plaintext from the DOM element. - secret_div.html('********'); - $(this).hide(); - $(this).siblings('button.unlock-secret').show(); + lock_secret(secret_id); }); // Retrieve a session key $('#request_session_key').click(function() { var private_key = $('#user_privkey').val(); - - // POST the user's private key to request a temporary session key. - console.log("Requesting a session key..."); get_session_key(private_key); }); @@ -43,23 +25,35 @@ $(document).ready(function() { type: 'GET', dataType: 'json', success: function (response, status) { - console.log("Secret retrieved successfully"); - $('#secret_' + secret_id).html(response.plaintext); - $('button.unlock-secret[secret-id=' + secret_id + ']').hide(); - $('button.lock-secret[secret-id=' + secret_id + ']').show(); + if (response.plaintext) { + console.log("Secret retrieved successfully"); + $('#secret_' + secret_id).html(response.plaintext); + $('button.unlock-secret[secret-id=' + secret_id + ']').hide(); + $('button.lock-secret[secret-id=' + secret_id + ']').show(); + } else { + console.log("Secret was not decrypted. Prompt user for private key."); + $('#privkey_modal').modal('show'); + } }, error: function (xhr, ajaxOptions, thrownError) { console.log("Error: " + xhr.responseText); if (xhr.status == 403) { alert("Permission denied"); } else { - var json = jQuery.parseJSON(xhr.responseText); - alert("Secret retrieval failed: " + json['error']); + alert(xhr.responseText); } } }); } + // Remove secret data from the DOM + function lock_secret(secret_id) { + var secret_div = $('#secret_' + secret_id); + secret_div.html('********'); + $('button.lock-secret[secret-id=' + secret_id + ']').hide(); + $('button.unlock-secret[secret-id=' + secret_id + ']').show(); + } + // Request a session key via the API function get_session_key(private_key) { var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); @@ -74,7 +68,7 @@ $(document).ready(function() { xhr.setRequestHeader("X-CSRFToken", csrf_token); }, success: function (response, status) { - console.log("Received a new session key; valid until " + response.expiration_time); + console.log("Received a new session key"); alert('Session key received! You may now unlock secrets.'); }, error: function (xhr, ajaxOptions, thrownError) { diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index aaf94f998..abd0bb292 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ViewSet, ModelViewSet from extras.api.renderers import FormlessBrowsableAPIRenderer, FreeRADIUSClientsRenderer +from secrets.exceptions import InvalidSessionKey from secrets.filters import SecretFilter from secrets.models import Secret, SecretRole, SessionKey, UserKey from utilities.api import WritableSerializerMixin @@ -53,42 +54,50 @@ class SecretViewSet(WritableSerializerMixin, ModelViewSet): authentication_classes = [BasicAuthentication, SessionAuthentication] permission_classes = [IsAuthenticated] - def _get_master_key(self, request): + def _read_session_key(self, request): # Check for a session key provided as a cookie or header if 'session_key' in request.COOKIES: - session_key = base64.b64decode(request.COOKIES['session_key']) + return base64.b64decode(request.COOKIES['session_key']) elif 'HTTP_X_SESSION_KEY' in request.META: - session_key = base64.b64decode(request.META['HTTP_X_SESSION_KEY']) - else: - return None - - # Retrieve session key cipher (if any) for the current user - try: - sk = SessionKey.objects.get(user=request.user) - except SessionKey.DoesNotExist: - return None - - # Recover master key - # TODO: Exception handling - master_key = sk.get_master_key(session_key) - - return master_key + return base64.b64decode(request.META['HTTP_X_SESSION_KEY']) + return None def retrieve(self, request, *args, **kwargs): - master_key = self._get_master_key(request) - secret = self.get_object() - if master_key is not None: - secret.decrypt(master_key) + secret = self.get_object() + session_key = self._read_session_key(request) + + # Retrieve session key cipher (if any) for the current user + if session_key is not None: + try: + sk = SessionKey.objects.get(user=request.user) + master_key = sk.get_master_key(session_key) + secret.decrypt(master_key) + except SessionKey.DoesNotExist: + return HttpResponseBadRequest("No active session key for current user.") + except InvalidSessionKey: + return HttpResponseBadRequest("Invalid session key.") serializer = self.get_serializer(secret) return Response(serializer.data) def list(self, request, *args, **kwargs): - master_key = self._get_master_key(request) + queryset = self.filter_queryset(self.get_queryset()) + # Attempt to retrieve the master key for decryption + session_key = self._read_session_key(request) + master_key = None + if session_key is not None: + try: + sk = SessionKey.objects.get(user=request.user) + master_key = sk.get_master_key(session_key) + except SessionKey.DoesNotExist: + return HttpResponseBadRequest("No active session key for current user.") + except InvalidSessionKey: + return HttpResponseBadRequest("Invalid session key.") + # Pagination page = self.paginate_queryset(queryset) if page is not None: diff --git a/netbox/secrets/exceptions.py b/netbox/secrets/exceptions.py new file mode 100644 index 000000000..417d135de --- /dev/null +++ b/netbox/secrets/exceptions.py @@ -0,0 +1,5 @@ +class InvalidSessionKey(Exception): + """ + Raised when the a provided session key is invalid. + """ + pass diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index f51fdd664..91e2ad895 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -13,6 +13,7 @@ from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device from utilities.models import CreatedUpdatedModel +from .exceptions import InvalidSessionKey from .hashers import SecretValidationHasher @@ -220,7 +221,7 @@ class SessionKey(models.Model): # Validate the provided session key if not check_password(session_key, self.hash): - raise Exception("Invalid session key") + raise InvalidSessionKey() # Decrypt master key using provided session key master_key = xor_keys(session_key, self.cipher) diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index 00d2455c1..77a214153 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -17,7 +17,7 @@
-