Ejemplo n.º 1
0
 def post(self, request: HttpRequest):
     """
     Set the new secret
     """
     form = NewOtpSecretForm(request.POST)
     if not form.is_valid():
         return self._send_otp_form(request, form)
     secret = request.session.get('encrypted_new_totp_secret', None)
     if secret is None:
         form.add_error(
             None, _('No Secret set. Please create a new secret first.'))
         return self._send_otp_form(request, form)
     secret = SymmetricCrypt().decrypt(b64decode(secret.encode('us-ascii')))
     if form.cleaned_data['otp_confirm'] not in get_possible_otps(secret):
         form.add_error('otp_confirm', _('One-time password invalid'))
         return self._send_otp_form(request, form)
     del request.session['encrypted_new_totp_secret']
     try:
         user = HubUser.objects.get(id=request.user.id)
         user.set_totp_secret(secret)
         user.save()
     except DatabaseError:  # pragma: no cover  # Database Safeguard
         form.add_error(
             None,
             _('We are currently unable to update your OTP Secret. Please try again later.'
               ))
         return self._send_otp_form(request, form)
     return self._send_otp_form(request, success=True)
Ejemplo n.º 2
0
 def test_encrypted_secret_generator_default_length(self):
     """
     Test if a encrypted random secret has the required length
     """
     self.assertEqual(
         72,
         len(SymmetricCrypt().decrypt(
             create_encrypted_random_totp_secret())))
 def _check_new_otp(self, link: str):
     """
     Check the new OTP setup
     """
     extracted_link = self.__extract_link(link)
     user_id = PendingCredentialRecovery.objects.get(uuid=extracted_link['uuid']).user.id
     self.__open_action(extracted_link)
     # The bad ones
     test_items = (
         'doctored-auth', ''
     )
     for auth in test_items:
         self.client.post(
             '/hub/auth/forgot-credentials/step-3/{}/reveal-new-otp-secret'.format(extracted_link['uuid']),
             data={
                 'auth': auth,
             },
             follow=True
         )
     otp_ready_response = self.client.post(
         '/hub/auth/forgot-credentials/step-3/{}/reveal-new-otp-secret'.format(extracted_link['uuid']),
         data={
             'auth': extracted_link['auth'],
         },
         follow=True
     )
     self.assertEqual(200, otp_ready_response.status_code)
     internal_session_data = str(b64decode(self.client.session.model.objects.first().session_data).decode('utf-8'))
     internal_session_data = internal_session_data[internal_session_data.index(':')+1:]
     internal_session_data = loads(internal_session_data)
     encrypted_otp_secret = b64decode(internal_session_data['encrypted_temporary_otp_secret'])
     new_otp_secret = SymmetricCrypt().decrypt(encrypted_otp_secret)
     # And the bad OTPs fist
     test_items = (
         # auth, otp
         (extracted_link['auth'], ''),
         (extracted_link['auth'], '999999'),
     )
     for auth, otp in test_items:
         self.client.post(
             '/hub/auth/forgot-credentials/step-3/{}/reveal-new-otp-secret/confirm'.format(extracted_link['uuid']),
             data={
                 'auth': auth,
                 'otp': otp
             },
             follow=True
         )
     otp_confirm_response = self.client.post(
         '/hub/auth/forgot-credentials/step-3/{}/reveal-new-otp-secret/confirm'.format(extracted_link['uuid']),
         data={
             'auth': extracted_link['auth'],
             'otp': get_otp(new_otp_secret)
         },
         follow=True
     )
     self.assertEqual(200, otp_confirm_response.status_code)
     self.assertEqual(0, PendingCredentialRecovery.objects.count())
     self.assertEqual(new_otp_secret, HubUser.objects.get(id=user_id).get_totp_secret())
Ejemplo n.º 4
0
 def test_in_out(self):
     """
     Simply encrypt and decrypt
     """
     test_items = (
         b'DSJFBSHDFBSMDBFMJERFSMDNBVMSESjhdfkshdfhjkeskjedfnskdj',
         b'SUPER-SECRET-SUPER-SECRET-MEGA-SECRET-MORE-SECRET-EVEN-MORE',
         b'JustAString',
     )
     for test_item in test_items:
         with self.subTest(msg='Testing with "{}"'.format(test_item)):
             encrypted_test_item = SymmetricCrypt().encrypt(test_item)
             decrypted_test_item = SymmetricCrypt().decrypt(
                 encrypted_test_item)
             self.assertEqual(test_item, decrypted_test_item)
             self.assertNotEqual(test_item, encrypted_test_item)
             self.assertIsNotNone(encrypted_test_item)
             self.assertIsNotNone(decrypted_test_item)
Ejemplo n.º 5
0
def create_encrypted_random_totp_secret(secret_length: int = 72):
    """
    Generate a random TOTP secret, encrypted

    :param int secret_length: How long should the unencrypted secret be?
    :rtype: bytes
    :returns: A random secret, but encrypted
    """
    return SymmetricCrypt().encrypt(create_random_totp_secret(secret_length))
Ejemplo n.º 6
0
 def _send_otp_form(self,
                    request: HttpRequest,
                    form: NewOtpSecretForm = NewOtpSecretForm(),
                    success: bool = False):
     """
     Send the OTP form
     """
     secret = request.session.get('encrypted_new_totp_secret', None)
     if secret is not None:
         secret = SymmetricCrypt().decrypt(
             b64decode(secret.encode('us-ascii')))
     return render(request,
                   self.template_name, {
                       'form': form,
                       'new_secret': secret,
                       'username': request.user.username,
                       'success': success,
                   },
                   content_type=self.content_type)
Ejemplo n.º 7
0
 def test_encrypted_secret_generator_lengths(self):
     """
     Test different lengths
     """
     for test_item in self.generator_lengths:
         with self.subTest(
                 msg='Testing with a length of {}'.format(test_item)):
             self.assertEqual(
                 test_item,
                 len(SymmetricCrypt().decrypt(
                     create_encrypted_random_totp_secret(test_item))))
Ejemplo n.º 8
0
    def get_totp_secret(self) -> Optional[bytes]:
        """
        Get the TOTP Secret

        This method gives you the unencrypted (decrypted) TOTP secret

        :rtype: Optional[bytes]
        :returns: The unencrypted secret or None if no secret is set
        """
        if self.totp_secret is None:
            return None
        return SymmetricCrypt().decrypt(self.totp_secret)
Ejemplo n.º 9
0
    def set_totp_secret(self, secret: bytes):
        """
        Set the TOTP Secret

        The TOTP Secret will be encrypted before it is saved to the database.

        :param bytes secret: The TOTP secret to be encrypted and saved to the database
        """
        if len(secret) < 32:
            raise ValueError(_('Secret must be at least 32 bytes long'))
        if len(secret) > 96:
            raise ValueError(_('Secret must not be larger than 96 bytes'))
        self.totp_secret = SymmetricCrypt().encrypt(secret)
Ejemplo n.º 10
0
 def test_encrypted_secret_generator_randomness(self):
     """
     Test the randomness of the encrypted secret generator
     """
     for length, amount in self.random_sets:
         with self.subTest(
                 msg='Testing with a length of {} in {} iterations'.format(
                     length, amount)):
             secrets = []
             for _ in range(amount):
                 secret = SymmetricCrypt().decrypt(
                     create_encrypted_random_totp_secret(length))
                 self.assertNotIn(secret, secrets)
                 secrets.append(secret)
Ejemplo n.º 11
0
 def get_context_data(self, **kwargs):
     context = super().get_context_data(**kwargs)
     user_id = kwargs.get('user_id', None)
     if not user_id:  # pragma: no cover
         raise Http404()
     try:
         user_obj = HubUser.objects.get(id=user_id)  # type: HubUser
     except HubUser.DoesNotExist:
         raise Http404()
     context['username'] = user_obj.username
     has_secret = user_obj.totp_secret is not None and len(
         user_obj.totp_secret) > 0
     context['has_secret'] = has_secret
     if has_secret:
         context['otp_secret'] = b32encode(SymmetricCrypt().decrypt(
             user_obj.totp_secret)).decode('us-ascii')
     return context
Ejemplo n.º 12
0
 def get(self, request, user_id: int, file_type: str):
     """
     Handle GET
     """
     (method, content_type) = self.actions[file_type]
     try:
         user_obj = HubUser.objects.get(id=user_id)  # type: HubUser
     except HubUser.DoesNotExist:
         raise Http404()
     if not user_obj.totp_secret:
         raise Http404()
     qr_code = method(
         b32encode(SymmetricCrypt().decrypt(user_obj.totp_secret)),
         user_obj.username)
     response = HttpResponse(content_type=content_type)
     qr_code.save(response)
     return response
Ejemplo n.º 13
0
 def post(self, request: HttpRequest, recovery: UUID):
     """
     Set the new secret
     """
     encrypted_secret = request.session.get(
         'encrypted_temporary_otp_secret', None)
     if encrypted_secret is None:  # pragma: no cover  # Safeguard for tinkered session
         return deny_step(request, 'EA10')
     auth_form = ForgottenCredentialsStep3BaseForm(request.POST)
     if not auth_form.is_valid(
     ):  # pragma: no cover  # Safeguard Client Manipulation
         return deny_step(request, 'EA11')
     new_secret = SymmetricCrypt().decrypt(b64decode(encrypted_secret))
     recovery_str = str(recovery)
     try:
         auth = Signer(salt=recovery_str).unsign(
             auth_form.cleaned_data['auth'])
         recovery_data = PendingCredentialRecovery.objects.filter(
             user__is_active=True,
             valid_until__gte=now(),
             recovery_type='otp-secret').get(uuid=recovery, key=auth)
         user = recovery_data.user
     except PendingCredentialRecovery.DoesNotExist:  # pragma: no cover  # Database Safeguard
         return deny_step(request, 'EA12')
     except (ValueError,
             BadSignature):  # pragma: no cover  # Manipulation Safeguard
         return deny_step(request, 'EA13')
     form = ForgottenCredentialsStep3ConfirmOtpForm(request.POST)
     if not form.is_valid():
         return self.show_form(request, new_secret, user.username,
                               recovery_str, form)
     if form.cleaned_data['otp'] not in get_possible_otps(new_secret):
         form.add_error('otp', _('This one time password is not valid.'))
         return self.show_form(request, new_secret, user.username,
                               recovery_str, form)
     with transaction.atomic():
         tx_id = transaction.savepoint()
         user.set_totp_secret(new_secret)
         user.save()
         recovery_data.delete()
         transaction.savepoint_commit(tx_id)
     logout(request)
     messages.add_message(
         request, messages.SUCCESS,
         _('Your new OTP secret has been set. You can login now.'))
     return redirect(reverse_lazy('ha:auth:login'))
Ejemplo n.º 14
0
 def authenticate(self,
                  request: HttpRequest,
                  username=None,
                  password=None,
                  one_time_pw=None) -> Optional[HubUser]:
     # pylint: disable=arguments-differ
     random = SystemRandom()
     for i in range(random.randrange(1, 5)):  # nosec
         HubUser().set_password('against-timing-attack' +
                                str(i))  # Mitigation against timing attack
     if username is None or password is None or one_time_pw is None:
         return None
     if any([
             len(username) < 1,
             len(username) > 150,
             len(password) < 1,
             len(password) > 1000,
             len(one_time_pw) != 6
     ]):
         return None
     c_user = TotpAuthenticationBackend.clean_username(username)
     user_object = super(TotpAuthenticationBackend,
                         self).authenticate(request, c_user,
                                            password)  # type: HubUser
     if user_object is None:
         return None
     if user_object.totp_secret is None:
         return None
     possible_tokens = get_possible_otps(SymmetricCrypt().decrypt(
         user_object.totp_secret))
     if one_time_pw not in possible_tokens:
         return None
     current_time = now()
     starting_at = current_time - datetime.timedelta(hours=1)
     try:
         BurnedOtp.objects.filter(
             user=user_object,
             burned_timestamp__gte=starting_at).get(token=one_time_pw)
         return None
     except BurnedOtp.DoesNotExist:
         BurnedOtp.objects.create(user=user_object, token=one_time_pw)
     return user_object
Ejemplo n.º 15
0
 def post(self, request: HttpRequest, recovery: UUID):
     """
     Set new temporary Secret and display it, ask the user to confirm with an OTP
     """
     form = ForgottenCredentialsStep3BaseForm(request.POST)
     if not form.is_valid():
         return deny_step(request, 'EA04')
     recovery_str = str(recovery)
     try:
         auth = Signer(salt=recovery_str).unsign(form.cleaned_data['auth'])
         user = PendingCredentialRecovery.objects.filter(
             user__is_active=True,
             valid_until__gte=now(),
             recovery_type='otp-secret').get(uuid=recovery, key=auth).user
     except PendingCredentialRecovery.DoesNotExist:  # pragma: no cover  # Database Safeguard
         return deny_step(request, 'EA05')
     except (ValueError,
             BadSignature):  # pragma: no cover  # Manipulation Safeguard
         return deny_step(request, 'EA06')
     new_secret = create_random_totp_secret()
     new_secret_encrypted = b64encode(
         SymmetricCrypt().encrypt(new_secret)).decode('us-ascii')
     request.session[
         'encrypted_temporary_otp_secret'] = new_secret_encrypted
     return render(request,
                   self.template_name, {
                       'new_secret':
                       new_secret,
                       'username':
                       user.username,
                       'recovery':
                       recovery_str,
                       'form':
                       ForgottenCredentialsStep3ConfirmOtpForm(
                           initial={'auth': form.cleaned_data['auth']})
                   },
                   content_type=self.content_type)
Ejemplo n.º 16
0
 def test_round_trip(self):
     """
     Test the complete Round Trip with wrong inputs and finally a good one
     """
     self.client.login(username=self.normal_user.username,
                       password=self.password,
                       one_time_pw=get_otp(self.secret))
     old_secret = self.normal_user.get_totp_secret()
     self._open()
     test_otps = ('', '1', '22', '333', '4444', '55555', '666666', 'aaaaaa',
                  'a1b2c3')
     for test_otp in test_otps:
         with self.subTest(
                 msg='Sending OTP "{}" without creating a secret before'.
                 format(test_otp)):
             response = self.client.post(self.url,
                                         data={'otp_confirm': test_otp},
                                         follow=False)
             self.assertEqual(200, response.status_code)
             self.assertEqual(
                 old_secret,
                 HubUser.objects.get(
                     id=self.normal_user.id).get_totp_secret())
     for expect in (True, False):
         with self.subTest(
                 msg='Creating a secret, expecting "{}"'.format(expect)):
             response = self.client.put(self.url)
             self.assertEqual(200, response.status_code)
             self.assertJSONEqual(response.content,
                                  {'secret_created': expect})
             self.assertEqual(
                 old_secret,
                 HubUser.objects.get(
                     id=self.normal_user.id).get_totp_secret())
     session_data = str(
         b64decode(
             self.client.session.model.objects.first().session_data).decode(
                 'utf-8'))
     session_data = loads(session_data[session_data.index(':') + 1:])
     new_otp_secret = SymmetricCrypt().decrypt(
         b64decode(session_data['encrypted_new_totp_secret']))
     test_otps = [
         '', '1', '22', '333', '4444', '55555', 'aaaaaa', 'a1b2c3'
     ] + [get_otp(new_otp_secret, -10)]
     for test_otp in test_otps:
         with self.subTest(
                 msg='Sending OTP "{}" which is wrong'.format(test_otp)):
             response = self.client.post(self.url,
                                         data={'otp_confirm': test_otp},
                                         follow=False)
             self.assertEqual(200, response.status_code)
             self.assertEqual(
                 old_secret,
                 HubUser.objects.get(
                     id=self.normal_user.id).get_totp_secret())
     with self.subTest(msg='Testing with correct OTP'):
         response = self.client.post(
             self.url,
             data={'otp_confirm': get_otp(new_otp_secret)},
             follow=False)
         self.assertEqual(200, response.status_code)
         self.assertNotEqual(
             old_secret,
             HubUser.objects.get(id=self.normal_user.id).get_totp_secret())
         self.assertEqual(
             new_otp_secret,
             HubUser.objects.get(id=self.normal_user.id).get_totp_secret())