# -*- coding: utf-8 -*- from django.conf import settings from django.db import models from .crypto import generate_randomness from .managers import SQRLNutManager class SQRLIdentity(models.Model): """ SQRL identity associated with a user. This model stores all necessary for SQRL to complete SQRL transactions and return any data to the client when necessary. The reason this is a standalone model vs lets say extending Django User model is because if added to the user model, each row for the user table is forced to allocate space to store SQRL information which might not be desired. By using a dedicated column, it allows to use SQRL only for users who use it. Also this makes Django-SQRL more modular so that it is easier integrated with existing projects. Attributes ---------- public_key : str Base64 encoded public key of per-site user's SQRL public-private key pair. This key is used to verify users signature signing SQRL transaction including server generated random nut using users private key. server_unlock_key : str Base64 encoded server unlock key which is a public unlock key sent to client which client can use to generate urs (unlock request signature) signature which server can validate using vuk. More information can be found at https://www.grc.com/sqrl/idlock.htm. verify_unlock_key : str Base64 encoded verify unlock key which is a key stored by server which is used to validate urs (unlock request signature) signatures. This key is not sent to user. More information can be found at https://www.grc.com/sqrl/idlock.htm. is_enabled : bool Boolean indicating whether user can authenticate using SQRL. in_only_sqrl : bool Boolean indicating that only SQRL should be allowed to authenticate user. When enabled via flag in SQRL client requests, this should disable all other methods of authentication such as username/password. """ user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='sqrl_identity', on_delete=models.CASCADE) """Foreign key to Django's auth ``User`` object for whom this SQRL identity belongs to.""" public_key = models.CharField( max_length=43, db_index=True, unique=True, help_text='Public key of per-site users public-private key pair. ' 'This key is used to verify users signature signing SQRL transaction ' 'including server generated random nut using users private key.' ) server_unlock_key = models.CharField( max_length=43, blank=True, help_text='This is public unlock key sent to client which client can use to ' 'generate urs (unlock request signature) signature which server can validate ' 'using vuk.' ) verify_unlock_key = models.CharField( max_length=43, blank=True, help_text='This is a key stored by server which is used to validate ' 'urs (unlock request signature) signatures. This key is not sent to user.' ) is_enabled = models.BooleanField( default=True, help_text='Boolean indicating whether user can authenticate using SQRL.' ) is_only_sqrl = models.BooleanField( default=False, help_text='Boolean indicating that only SQRL should be allowed to authenticate user. ' 'When enabled via flag in SQRL client requests, this should disable all other ' 'methods of authentication such as username/password.' ) class Meta(object): # Redefine db_table so that table name is not funny # like sqrl_sqrlidentity. # One way to solve that is to simply name the model # Identity however that is generic enough which might cause # name conflicts so its easier to rename the model # and manually overwrite the table name db_table = 'sqrl_identity' verbose_name = 'SQRL Identity' verbose_name_plural = 'SQRL Identities' def __str__(self): if self.user_id: return '{} for {}'.format(self.__class__.__name__, self.user) else: return '{} with idk={}'.format(self.__class__.__name__, self.public_key) class SQRLNut(models.Model): """ Model for storing temporary state for SQRL transactions. This model by SQRL protocol is not strictly required. Here is the reasoning for it though: SQRL protocol requires couple of things: 1. Each SQRL interaction must use random ``nonce`` or in SQRL terminology SQRL nut 2. Strict enforcement that each nut can only be used at most once 3. Each non-initiating SQRL request should return to the client some information about the initiating request such as whether IP address matches that of the IP where SQRL transaction was initiated. ``#3`` can easily be solved by encoding all necessary information in the nut value itself. On subsequent requests, server can simply decode all the information from the nut. That however will require nut value to grow pretty big. In addition this might also require encryption so that nut value by itself is not revealing. That in turn requires some rotating key management structure so that not all nuts are encrypted with same encryption key. All of the above is possible however is not very elegant, but again, is still possible. In addition, encrypting state in nut value does not hinder ``#1`` since some random bit can also be part of the nut so ``#1`` is not an issue. So far we dont need any state on the server. Each request can have its own nut value. The problem however is with ``#2`` requirement. The only way for the server to guarantee that nuts are never reused is to either keep a state of which nuts were used or keep state of all currently available nuts. Since keeping state of all used nuts will infinitely grow, probably the latter option is better. The default way of adding state in Django project would of been to add a model. But at that point, if you are creating a model anyway, might as well store everything in that model. As a benefit, it makes whole system simpler by not requiring any fancy approaches to encrypting nuts and maintaining rotated key encryption schedule of some sort. Attributes ---------- nonce : str Base64 encoded value of a random nonce. This nonce is used to identify SQRL transaction. It is regenerated for each SQRL communication within a single SQRL transaction. Since this nonce is a one-time token, it allows for the server to prevent replay attacks. This columns is a primary key of the table. transaction_nonce : str Base64 encoded random nonce used to identify a full SQRL transaction from start to finish. Session key cannot be used since it is persistent across complete user visit which can include multiple tabs/windows. This transaction id is regenerated for each new tab which allows the client to identify when a particular SQRL transaction has completed hence redirect user to more appropriate page. session_key : str Session key of the session where SQRL transaction initiated which is used to associate client session to SQRL transaction. This is important because SQRL transaction can be completed on a different device which does not have access to original user session. is_transaction_complete : bool Boolean indicating whether SQRL transaction has completed. It is used by the :obj:`.views.SQRLStatusView` to return redirect URL to the user when transaction has completed. ip_address : str IP address of the user who initiated SQRL transaction. This is where initially SQRL link/qr code were generated. timestamp : datetime Last timestamp when nut was either created or modified. Used for puging purses to remove expired nuts. """ nonce = models.CharField( max_length=43, unique=True, db_index=True, primary_key=True, help_text='Single-use random nonce used to identify SQRL transaction. ' 'This nonce is regenerated for each SQRL communication within ' 'a single SQRL transaction. Since this nonce is a one-time token, ' 'it allows for the sviewerver to prevent replay attacks.' ) transaction_nonce = models.CharField( max_length=43, unique=True, db_index=True, help_text='A random nonce used to identify a full SQRL transaction. ' 'Session key cannot be used since it is persistent across ' 'complete user visit which can include multiple tabs/windows. ' 'This transaction id is regenerated for each new tab which ' 'allows the client to identify when a particular SQRL transaction ' 'has completed hence redirect user to more appropriate page.' ) session_key = models.CharField( max_length=32, unique=True, db_index=True, help_text='User regular session key. This is used to associate client session ' 'to a SQRL transaction since transaction can be completed on a different ' 'device which does not have access to original user session.' ) is_transaction_complete = models.BooleanField( default=False, help_text='Indicator whether transaction is complete. ' 'Can we used by UI to automatically redirect to appropriate page ' 'once SQRL transaction is complete.', ) ip_address = models.GenericIPAddressField( help_text='Originating IP address of client who initiated SQRL transaction. ' 'Used to set appropriate TIF response code.', ) timestamp = models.DateTimeField( auto_now=True, help_text='Last timestamp when nut was either created or modified. ' 'Used for purging purposes.', ) objects = SQRLNutManager() class Meta(object): # Explicitly define db_table for clearer table name db_table = 'sqrl_nut' verbose_name = 'SQRL Nut' verbose_name_plural = 'SQRL Nuts' def __str__(self): return self.nonce def renew(self): """ Renew instance of the nut. This is done by deleting the nut from the db since nonce is a primary key. Then nonce is regenerated and re-saved. """ self.delete() self.nonce = generate_randomness() self.save()