Still building initial conversions
This commit is contained in:
parent
cea56d2868
commit
3cfd9840e8
23 changed files with 2925 additions and 21 deletions
16
CHANGELOG
16
CHANGELOG
|
@ -1,3 +1,19 @@
|
|||
Mon, 02 Sep 2019 02:03:23 -0500
|
||||
Keaton <kii-chan@tutanota.com>
|
||||
Still building initial conversions
|
||||
|
||||
Everything is still completely untested. These conversions were done a few weeks
|
||||
ago, so my memory on any design changes is a little rusty. Aside from what's
|
||||
listed below, all changes are just compatibility fixes.
|
||||
|
||||
- Removed references to six since we're dropping Python 2 support
|
||||
- Removed references to QR code urls since those will be browser generated
|
||||
- Added template for creating a drop-in SQRL login element
|
||||
- Also changed some variable names to be less confusing
|
||||
- Changed login pages to use drop-in element
|
||||
|
||||
--------------------
|
||||
|
||||
Wed, 14 Aug 2019 12:14:52 -0500
|
||||
Keaton <kii-chan@tutanota.com>
|
||||
fix typo
|
||||
|
|
136
sqrl/exceptions.py
Normal file
136
sqrl/exceptions.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def _make_tif_property(val):
|
||||
"""
|
||||
Helper function for generating property methods for :obj:`.TIF`
|
||||
which will boolean whether a particular SQRL ``TIF`` bit is ``True``
|
||||
in the ``TIF`` value.
|
||||
Parameters
|
||||
----------
|
||||
val : int
|
||||
Value with particular ``True`` bit which will be tested
|
||||
within the generated property.
|
||||
Returns
|
||||
-------
|
||||
function
|
||||
Function which can be made into a property
|
||||
"""
|
||||
|
||||
def is_bit_present(self):
|
||||
"""
|
||||
Property which returns boolean whether ``{hex}`` or ``{bits}``
|
||||
bit is present in the TIF value.
|
||||
"""
|
||||
return bool(self & val)
|
||||
|
||||
is_bit_present.__doc__ = is_bit_present.__doc__.format(
|
||||
hex=hex(val),
|
||||
bits=bin(val),
|
||||
)
|
||||
|
||||
return is_bit_present
|
||||
|
||||
|
||||
class TIF(int):
|
||||
"""
|
||||
SQRL ``TIF`` ``int`` subclass which can represent SQRL ``TIF`` flags.
|
||||
Example
|
||||
-------
|
||||
::
|
||||
>>> tif = TIF(TIF.IP_MATCH | TIF.TRANSIENT_FAILURE | TIF.COMMAND_FAILED)
|
||||
>>> tif.is_ip_match
|
||||
True
|
||||
>>> tif.is_id_match
|
||||
False
|
||||
>>> tif.is_transient_failure
|
||||
True
|
||||
>>> tif
|
||||
100
|
||||
>>> tif.as_hex_string()
|
||||
'64'
|
||||
>>> tif.breakdown() == {
|
||||
... 'id_match': False,
|
||||
... 'previous_id_match': False,
|
||||
... 'ip_match': True,
|
||||
... 'sqrl_disabled': False,
|
||||
... 'not_supported': False,
|
||||
... 'transient_failure': True,
|
||||
... 'command_failed': True,
|
||||
... 'client_failure': False,
|
||||
... }
|
||||
True
|
||||
"""
|
||||
ID_MATCH = 0x1
|
||||
"""SQRL ID was found in DB"""
|
||||
PREVIOUS_ID_MATCH = 0x2
|
||||
"""Previous SQRL ID was found in DB"""
|
||||
IP_MATCH = 0x4
|
||||
"""SQRL client is used from same IP as where transaction started"""
|
||||
SQRL_DISABLED = 0x8
|
||||
"""SQRL auth is disabled for the found SQRL identity as per users request"""
|
||||
NOT_SUPPORTED = 0x10
|
||||
"""SQRL client requested SQRl operation which is not supported"""
|
||||
TRANSIENT_FAILURE = 0x20
|
||||
"""SQRL command failed transiently. Most likely restarting SQRL transaction should fix this"""
|
||||
COMMAND_FAILED = 0x40
|
||||
"""SQRL command failed for any reason"""
|
||||
CLIENT_FAILURE = 0x80
|
||||
"""SQRL command failed because SQRL client sent invalid data"""
|
||||
BAD_ID_ASSOCIATION = 0x100
|
||||
"""SQRL Identity is already a ssociated with a different account"""
|
||||
|
||||
is_id_match = property(_make_tif_property(ID_MATCH))
|
||||
is_previous_id_match = property(_make_tif_property(PREVIOUS_ID_MATCH))
|
||||
is_ip_match = property(_make_tif_property(IP_MATCH))
|
||||
is_sqrl_disabled = property(_make_tif_property(SQRL_DISABLED))
|
||||
is_transient_failure = property(_make_tif_property(TRANSIENT_FAILURE))
|
||||
is_command_failed = property(_make_tif_property(COMMAND_FAILED))
|
||||
is_client_failure = property(_make_tif_property(CLIENT_FAILURE))
|
||||
is_not_supported = property(_make_tif_property(NOT_SUPPORTED))
|
||||
is_bad_id_association = property(_make_tif_property(BAD_ID_ASSOCIATION))
|
||||
|
||||
def as_hex_string(self):
|
||||
"""
|
||||
Return TIF value as hex string
|
||||
"""
|
||||
return '{:x}'.format(self)
|
||||
|
||||
def breakdown(self):
|
||||
"""
|
||||
Returns a full breakdown of the TIF value.
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Keys are the SQRL TIF property and values are booleans.
|
||||
"""
|
||||
return {
|
||||
k.lower(): bool(self & v)
|
||||
for k, v in vars(self.__class__).items()
|
||||
if not k.startswith('_') and k.isupper()
|
||||
}
|
||||
|
||||
def update(self, other):
|
||||
"""
|
||||
Return updated TIF which will contain both bits already set
|
||||
in the ``self`` value as well as the ``other value.
|
||||
Parameters
|
||||
----------
|
||||
other : int
|
||||
Other ``TIF`` value which be merged with ``self`` bits
|
||||
Returns
|
||||
-------
|
||||
TIF
|
||||
New :obj:`.TIF` value which has merged bits.
|
||||
"""
|
||||
return type(self)(self | other)
|
||||
|
||||
|
||||
class TIFException(Exception):
|
||||
"""
|
||||
Custom Exception which can be used in the views to raise
|
||||
specific :obj:`.TIF` bits and immediately return appropriate response
|
||||
to the user.
|
||||
"""
|
||||
|
||||
def __init__(self, tif):
|
||||
self.tif = TIF(tif)
|
212
sqrl/fields.py
Normal file
212
sqrl/fields.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.urls import Resolver404, resolve
|
||||
from django.core.validators import URLValidator
|
||||
from django.http import QueryDict
|
||||
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from .utils import Base64
|
||||
|
||||
|
||||
class NextUrlField(forms.CharField):
|
||||
"""
|
||||
Custom ``CharField`` which validates that a value is a valid next URL.
|
||||
|
||||
It validates that by checking that the value can be resolved to a view
|
||||
hence guaranteeing that when redirected URL will not fail.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'invalid_url': 'Invalid next url.'
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Validate that value is a valid URL for this project.
|
||||
"""
|
||||
value = super(NextUrlField, self).to_python(value)
|
||||
|
||||
if value in self.empty_values:
|
||||
return value
|
||||
|
||||
path = urlparse(value).path
|
||||
|
||||
try:
|
||||
resolve(path)
|
||||
except Resolver404:
|
||||
raise forms.ValidationError(self.error_messages['invalid_url'])
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
class ExtractedNextUrlField(NextUrlField):
|
||||
"""
|
||||
Similar to :obj:`.NextUrlField` however this extracts next url from full encoded URL.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'missing_next': 'Missing next query parameter.'
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Extract next url from full URL string and then use :obj:`.NextUrlField`
|
||||
to validate that value is valid URL.
|
||||
"""
|
||||
value = forms.CharField.to_python(self, value)
|
||||
|
||||
if value in self.empty_values:
|
||||
return value
|
||||
|
||||
decoded = urlparse(unquote(value))
|
||||
data = QueryDict(decoded.query)
|
||||
|
||||
if 'next' not in data:
|
||||
raise forms.ValidationError(self.error_messages['missing_next'])
|
||||
|
||||
return super(ExtractedNextUrlField, self).to_python(data['next'])
|
||||
|
||||
|
||||
class SQRLURLValidator(URLValidator):
|
||||
"""
|
||||
Custom URL validator which validates that a URL is a valid SQRL url.
|
||||
|
||||
These are the differences with regular HTTP URLs:
|
||||
|
||||
* scheme is either sqrl (secure) and qrl (non-secure)
|
||||
* ``:`` is a valid path separator which can be used to indicate
|
||||
which section of the SQRL should be used to generate
|
||||
public/provate keypair for the domain.
|
||||
|
||||
Okay, but is there support for qrl schema???
|
||||
|
||||
"""
|
||||
schemes = ['sqrl', 'qrl']
|
||||
# schemes = ['sqrl']
|
||||
|
||||
|
||||
class SQRLURLField(forms.URLField):
|
||||
"""
|
||||
SQRL URL field which uses :obj:`.SQRLURLValidator` for validation.
|
||||
"""
|
||||
default_validators = [SQRLURLValidator()]
|
||||
|
||||
|
||||
class Base64Field(forms.CharField):
|
||||
"""
|
||||
Field which decodes base64 values using :meth:`.utils.Base64.decode`.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'base64': 'Invalid value. Must be base64url encoded string.',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Decodes base64 value and returns binary data.
|
||||
"""
|
||||
value = super(Base64Field, self).to_python(value)
|
||||
if not value:
|
||||
return b''
|
||||
try:
|
||||
return Base64.decode(value)
|
||||
except (ValueError, TypeError):
|
||||
raise forms.ValidationError(self.error_messages['base64'])
|
||||
|
||||
|
||||
class Base64CharField(Base64Field):
|
||||
"""
|
||||
Similar to :obj:`.Base64Field` however this field normalizes to ``str`` (``unicode``) data.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'base64_ascii': 'Invalid value. Must be ascii base64url encoded string.',
|
||||
}
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Returns base64 decoded data as string.
|
||||
|
||||
Uses :meth:`.Base64Field.to_python` to decode base64 value
|
||||
which returns binary data and then this method further
|
||||
decodes ascii data to return ``str`` (``unicode``) data.
|
||||
"""
|
||||
value = super(Base64CharField, self).to_python(value)
|
||||
if not value:
|
||||
return ''
|
||||
try:
|
||||
return value.decode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
raise forms.ValidationError(self.error_messages['base64_ascii'])
|
||||
|
||||
|
||||
class Base64PairsField(Base64CharField):
|
||||
"""
|
||||
Field which normalizes base64 encoded multistring key-value pairs to ``OrderedDict``.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
always_pairs : bool
|
||||
Boolean which enforces that the value must always be keypairs.
|
||||
When ``False`` and the value is not a keypair, the value itself
|
||||
is returned.
|
||||
"""
|
||||
default_error_messages = {
|
||||
'crlf': 'Invalid value. Must be multi-line string separated by CRLF.',
|
||||
'pairs': 'Invalid value. Must be multi-line string of pair of values.',
|
||||
}
|
||||
always_pairs = True
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Normalizes multiline base64 keypairs string to ``OrderedDict``.
|
||||
"""
|
||||
value = super(Base64PairsField, self).to_python(value)
|
||||
if not value:
|
||||
return OrderedDict()
|
||||
|
||||
if not value.endswith('\r\n'):
|
||||
if self.always_pairs:
|
||||
raise forms.ValidationError(self.error_messages['crlf'])
|
||||
else:
|
||||
return value
|
||||
|
||||
try:
|
||||
return OrderedDict(
|
||||
line.split('=', 1) for line in filter(None, value.splitlines())
|
||||
)
|
||||
except ValueError:
|
||||
raise forms.ValidationError(self.error_messages['pairs'])
|
||||
|
||||
|
||||
class Base64ConditionalPairsField(Base64PairsField):
|
||||
"""
|
||||
Similar to :obj:`.Base64PairsField` but this field does not force
|
||||
the value to be keypairs.
|
||||
"""
|
||||
always_pairs = False
|
||||
|
||||
|
||||
class TildeMultipleValuesField(forms.CharField):
|
||||
"""
|
||||
Field which returns tilde-separated list.
|
||||
"""
|
||||
|
||||
def to_python(self, value):
|
||||
"""
|
||||
Normalizes to a Python list by splitting string by tilde (~) delimiter.
|
||||
"""
|
||||
value = super(TildeMultipleValuesField, self).to_python(value)
|
||||
if not value:
|
||||
return []
|
||||
return value.split('~')
|
||||
|
||||
|
||||
class TildeMultipleValuesFieldChoiceField(TildeMultipleValuesField, forms.ChoiceField):
|
||||
"""
|
||||
Similar to :obj:`.TildeMultipleValuesField` however this field also validates
|
||||
each value to be a valid choice.
|
||||
"""
|
||||
|
||||
def validate(self, value):
|
||||
for i in value:
|
||||
super(TildeMultipleValuesFieldChoiceField, self).validate(i)
|
442
sqrl/forms.py
Normal file
442
sqrl/forms.py
Normal file
|
@ -0,0 +1,442 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django import forms
|
||||
from django.contrib.auth import SESSION_KEY, get_user_model
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
|
||||
from .crypto import HMAC, Ed25519
|
||||
from .exceptions import TIF
|
||||
from .fields import (
|
||||
Base64ConditionalPairsField,
|
||||
Base64Field,
|
||||
Base64PairsField,
|
||||
ExtractedNextUrlField,
|
||||
NextUrlField,
|
||||
SQRLURLField,
|
||||
TildeMultipleValuesField,
|
||||
)
|
||||
from .models import SQRLIdentity
|
||||
from .utils import Base64
|
||||
|
||||
|
||||
class NextUrlForm(forms.Form):
|
||||
"""
|
||||
Form which validates that next URL is a valid URL.
|
||||
Currently is used by :obj:`.views.SQRLCompleteRegistrationView` to validate
|
||||
next URL to which user will be redirected when registration is completed.
|
||||
"""
|
||||
next = NextUrlField()
|
||||
|
||||
|
||||
class ExtractedNextUrlForm(forms.Form):
|
||||
"""
|
||||
Form which extracts and validates next URL from a full URL.
|
||||
Currently is used by :obj:`.views.SQRLStatusView` to return an appropriate
|
||||
URL to the JS to which JS will redirect to when SQRL transaction is complete.
|
||||
The reason we want to extract the next url here vs js is for simplicity
|
||||
of the js code. Now js can simply find ``<input name=next/>`` or if not present
|
||||
pass current full URL which might contain ``?next=<url>`` to the server.
|
||||
Python then can extract next url when present and if valid, return appropriate
|
||||
URL for the user to redirect to.
|
||||
"""
|
||||
url = ExtractedNextUrlField()
|
||||
|
||||
|
||||
class AuthQueryDictForm(forms.Form):
|
||||
"""
|
||||
Form to validate the ``request.GET`` in :obj:`.views.SQRLAuthView`.
|
||||
This allows to validate that the nut is of expected length and matches
|
||||
particular regex pattern before we attempt to look it up in the db.
|
||||
Normally this would be validated by a url pattern however in SQRL,
|
||||
nut is sent as a querystring parameter which is outside of the scope
|
||||
of URL pattern matching in Django.
|
||||
"""
|
||||
nut = forms.SlugField(max_length=43)
|
||||
|
||||
|
||||
class ClientForm(forms.Form):
|
||||
"""
|
||||
Form used to validate client portion of the SQRL request payload.
|
||||
Since this form is used as nested form by :obj:`.RequestForm`
|
||||
and therefore it does not have access to the signatures, this form
|
||||
only validates the values themselves. :obj:`.RequestForm` takes care
|
||||
of all conditional validations such as validating signatures
|
||||
when some keys are present in this form.
|
||||
"""
|
||||
ver = forms.IntegerField(label='Version', min_value=1, max_value=1)
|
||||
cmd = TildeMultipleValuesField(label='Command')
|
||||
opt = TildeMultipleValuesField(label='Options', required=False)
|
||||
idk = Base64Field(label='Identity Key')
|
||||
pidk = Base64Field(label='Previous Identity Key', required=False)
|
||||
suk = Base64Field(label='Server Unlock Key', required=False)
|
||||
vuk = Base64Field(label='Verify Unlock Key', required=False)
|
||||
|
||||
|
||||
class RequestForm(forms.Form):
|
||||
"""
|
||||
This is a main form for validating SQRL request payload.
|
||||
This form uses uses :obj:`.ClientForm` for validating client portion of the request.
|
||||
Therefore some of the validation portions by the time they are executed,
|
||||
they expect client form to be validated so that they can lookup client values.
|
||||
Currently this does not require any *magic* solutions since ``client``
|
||||
is first defined field which means it will be validated first.
|
||||
Parameters
|
||||
----------
|
||||
nut : SQRLNut
|
||||
Nut for which this form will be validated
|
||||
"""
|
||||
client = Base64PairsField()
|
||||
server = Base64ConditionalPairsField()
|
||||
ids = Base64Field()
|
||||
pids = Base64Field(required=False)
|
||||
urs = Base64Field(required=False)
|
||||
|
||||
def __init__(self, nut, *args, **kwargs):
|
||||
self.nut = nut
|
||||
self.session = None
|
||||
self.identity = None
|
||||
self.previous_identity = None
|
||||
self.tif = TIF(0)
|
||||
super(RequestForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_client(self):
|
||||
"""
|
||||
Since Django forms dont support nested forms, this method is used
|
||||
to do that in ad-hoc fashion.
|
||||
By the time this method is called, we know that ``client`` data will be
|
||||
``OrderedDict``, as guaranteed by :obj:`.fields.Base64PairsField`.
|
||||
We use that information and simply pass the client data to the
|
||||
:obj:`.ClientForm`. If validated, :obj:`.ClientForm` cleaned data will be
|
||||
a dictionary which we then simply return in this method. If not validated,
|
||||
we simply raise validation exception here which stop the rest of the
|
||||
validation steps.
|
||||
"""
|
||||
client_form = ClientForm(data=self.cleaned_data['client'])
|
||||
|
||||
if not client_form.is_valid():
|
||||
raise forms.ValidationError(client_form.errors)
|
||||
|
||||
return client_form.cleaned_data
|
||||
|
||||
def clean_server(self):
|
||||
"""
|
||||
This method conditionally validates ``server`` field.
|
||||
The reason why we validate it conditionally is because on initial SQRL
|
||||
request, client only sends initial SQRL URL since does not have access
|
||||
to the server response yet which would be a dict. In that case we do not
|
||||
validate server field at all.
|
||||
.. note::
|
||||
Even though on initial SQRL request, we dont validate server field,
|
||||
it is still indirectly validated against tampering via signatures.
|
||||
When we do validate it, we check the following:
|
||||
* nut value matches the looked up nut nonce
|
||||
(this should ever happen but just in case)
|
||||
* validate that ``mac`` is present in the server dict
|
||||
* validate the ``mac`` value that is correctly signs the server response
|
||||
excluding ``mac`` value itself.
|
||||
"""
|
||||
server = self.cleaned_data['server']
|
||||
|
||||
if not isinstance(server, dict):
|
||||
return server
|
||||
|
||||
if server.get('nut') != self.nut.nonce:
|
||||
raise forms.ValidationError('Nut mismatch between server value and looked up nut.')
|
||||
|
||||
if 'mac' not in server:
|
||||
raise forms.ValidationError('Missing server signature.')
|
||||
|
||||
try:
|
||||
is_valid_signature = HMAC(self.nut, server).is_signature_valid(
|
||||
Base64.decode(server['mac'])
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
is_valid_signature = False
|
||||
|
||||
if not is_valid_signature:
|
||||
raise forms.ValidationError('Invalid server signature.')
|
||||
|
||||
return server
|
||||
|
||||
def _validate_signature(self, name, key, signature):
|
||||
client = self.data['client']
|
||||
server = self.data['server']
|
||||
msg = (client + server).encode('ascii')
|
||||
|
||||
if not Ed25519(key, None, msg).is_signature_valid(signature):
|
||||
raise forms.ValidationError('Invalid {} signature.'.format(name))
|
||||
|
||||
def clean_ids(self):
|
||||
"""
|
||||
Validate SQRL ID signature which always must be present.
|
||||
This validates that the ID signature correctly signed raw client + raw server
|
||||
concatenated base64 encoded strings.
|
||||
"""
|
||||
idk = self.cleaned_data['client']['idk']
|
||||
ids = self.cleaned_data['ids']
|
||||
|
||||
self._validate_signature('ids', idk, ids)
|
||||
|
||||
return ids
|
||||
|
||||
def clean_pids(self):
|
||||
"""
|
||||
Validate Previous SQRL ID signature when present.
|
||||
This validates:
|
||||
* if either previous ID or previous signature are present, both are required
|
||||
* previous ID signature correctly signed raw client + raw server
|
||||
concatenated base64 encoded strings.
|
||||
"""
|
||||
pidk = self.cleaned_data['client'].get('pidk')
|
||||
pids = self.cleaned_data.get('pids')
|
||||
|
||||
if not all((pids, pidk)):
|
||||
raise forms.ValidationError(
|
||||
'Cannot validate previous ID signature without server knowing pid public and secret keys.'
|
||||
)
|
||||
|
||||
if pidk and pids:
|
||||
self._validate_signature('pids', pidk, pids)
|
||||
|
||||
return pids
|
||||
|
||||
def _clean_urs(self):
|
||||
vuk = getattr(self.identity, 'verify_unlock_key', None)
|
||||
suk = getattr(self.identity, 'server_unlock_key', None)
|
||||
|
||||
urs = self.cleaned_data.get('urs')
|
||||
|
||||
if urs:
|
||||
if not all((vuk, suk)):
|
||||
raise forms.ValidationError(
|
||||
'Cannot validate urs signature without server knowing vuk and suk keys.'
|
||||
)
|
||||
|
||||
self._validate_signature('urs', Base64.decode(vuk), urs)
|
||||
|
||||
return urs
|
||||
|
||||
def _clean_client_cmd(self):
|
||||
client = self.cleaned_data['client']
|
||||
cmds = client['cmd']
|
||||
|
||||
for cmd in cmds:
|
||||
method_name = '_clean_client_cmd_{}'.format(cmd)
|
||||
if hasattr(self, method_name):
|
||||
getattr(self, method_name)(client)
|
||||
|
||||
def _clean_client_cmd_ident(self, client):
|
||||
suk = client.get('suk')
|
||||
vuk = client.get('vuk')
|
||||
|
||||
if not self.identity and not all([suk, vuk]):
|
||||
raise forms.ValidationError(
|
||||
'Missing suk or vuk which are required when creating new identity.'
|
||||
)
|
||||
|
||||
if self.identity and any([suk, vuk]):
|
||||
raise forms.ValidationError(
|
||||
'Cannot send suk or vuk when SQRL identity is already associated.'
|
||||
)
|
||||
|
||||
if 'disable' in client['cmd']:
|
||||
raise forms.ValidationError(
|
||||
'Cannot use disable command at the same time as ident command.'
|
||||
)
|
||||
|
||||
# since we only store a single identity at the time
|
||||
# its impossible for when identity is being changed
|
||||
# self.identity will exist since by definition server
|
||||
# should only be aware of the previous identity
|
||||
# since the client is sending a new identity for storage.
|
||||
if all((not self.identity,
|
||||
self.previous_identity,
|
||||
not self.cleaned_data.get('urs'))):
|
||||
raise forms.ValidationError(
|
||||
'Must supply urs (unlock request signature) when switching identities '
|
||||
'from previously stored identity (pidk) to new current identity (idk).'
|
||||
)
|
||||
|
||||
def _clean_client_cmd_disable(self, client):
|
||||
if not self.identity:
|
||||
raise forms.ValidationError(
|
||||
'Must have identity associated in order to disable SQRL.'
|
||||
)
|
||||
|
||||
if 'enable' in client['cmd']:
|
||||
raise forms.ValidationError(
|
||||
'Cannot use enable command at the same time as disable command.'
|
||||
)
|
||||
|
||||
def _clean_client_cmd_enable(self, client):
|
||||
if not self.identity:
|
||||
raise forms.ValidationError(
|
||||
'Must have identity associated in order to enable SQRL.'
|
||||
)
|
||||
|
||||
if not self.cleaned_data.get('urs'):
|
||||
raise forms.ValidationError(
|
||||
'Must supply urs (unlock request signature) to enable SQRL access.'
|
||||
)
|
||||
|
||||
if 'disable' in client['cmd']:
|
||||
raise forms.ValidationError(
|
||||
'Cannot use disable command at the same time as enable command.'
|
||||
)
|
||||
|
||||
def _clean_client_cmd_remove(self, client):
|
||||
if not self.identity:
|
||||
raise forms.ValidationError(
|
||||
'Must have identity associated in order to remove SQRL.'
|
||||
)
|
||||
|
||||
if not self.cleaned_data.get('urs'):
|
||||
raise forms.ValidationError(
|
||||
'Must supply urs (unlock request signature) to enable SQRL access.'
|
||||
)
|
||||
|
||||
if client['cmd'] != ['remove']:
|
||||
raise forms.ValidationError(
|
||||
'Cannot use any other commands at the same time as remove command.'
|
||||
)
|
||||
|
||||
def _clean_session(self):
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
user_id = self.session.get(SESSION_KEY)
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = int(user_id)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if self.identity and self.identity.user_id != user_id:
|
||||
self.tif = self.tif.update(TIF.BAD_ID_ASSOCIATION)
|
||||
raise forms.ValidationError(
|
||||
'Cannot use SQRL within existing authenticated session'
|
||||
'when SQRL identity is already associated with a different account'
|
||||
)
|
||||
|
||||
user = get_user_model().objects.filter(pk=user_id).first()
|
||||
|
||||
try:
|
||||
if not user or not user.sqrl_identity:
|
||||
return
|
||||
except SQRLIdentity.DoesNotExist:
|
||||
return
|
||||
|
||||
# We want to make sure that if the user is logged in
|
||||
# and already has an identity associated with the account,
|
||||
# that either current or previous identity supplied in the
|
||||
# client request matches the identity already associated
|
||||
# with the account.
|
||||
# That will force the user to go through the SQRL identity
|
||||
# [un]lock processes to change the sqrl identity
|
||||
# (e.g. force the user to load the identity unlock key
|
||||
# and use that to change identities).
|
||||
# If this condition is not checked, it will be possible
|
||||
# for any SQRL identity to overwrite existing identity,
|
||||
# without any additional checks, which is not desirable.
|
||||
# For example if the existing auth scheme (e.g. username+password)
|
||||
# gets compromised, it will be possible for malicious party to
|
||||
# associate their SQRL identity with the account hence
|
||||
# locking out legitimate account owner.
|
||||
idk = Base64.encode(self.cleaned_data['client']['idk'])
|
||||
pidk = Base64.encode(self.cleaned_data['client'].get('pidk', b''))
|
||||
|
||||
if user.sqrl_identity.public_key not in [idk, pidk]:
|
||||
self.tif = self.tif.update(TIF.BAD_ID_ASSOCIATION)
|
||||
raise forms.ValidationError(
|
||||
'Both current and previous identities do not match user\'s already '
|
||||
'associated SQRL identity. If the identity needs to be changed, '
|
||||
'SQRL identity unlock processes must be followed.'
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Assuming all fields successfully validated, this method validates form as a whole.
|
||||
Also this field does any db lookups such as retrieving SQRL identities.
|
||||
That is necessary for validation however since lookup already happens here,
|
||||
the :obj:`.views.SQRLAuthView` reuses the looked up values so that it
|
||||
does not have to lookup same objects twice.
|
||||
This method does the following (sometimes using private methods):
|
||||
#. lookups identities - both current and previous
|
||||
#. validates client commands (``client.cmd``) such as ``ident``.
|
||||
They can only be validated when identities are looked up.
|
||||
For example ``disable`` cannot be requested when no SQRL identity is found.
|
||||
#. validates ``urs`` (unlock request signature).
|
||||
That can only be done here as this requires to use data stored
|
||||
in the stored identity.
|
||||
#. lookups client session (where user will be logged in)
|
||||
#. validates session. For example if the user is already logged with existing
|
||||
matching SQRL identity, this validates that only stored SQRL identity
|
||||
public key can be used in SQRL transaction.
|
||||
"""
|
||||
cleaned_data = super(RequestForm, self).clean()
|
||||
|
||||
self.find_identities()
|
||||
self._clean_client_cmd()
|
||||
self._clean_urs()
|
||||
|
||||
self.find_session()
|
||||
self._clean_session()
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def find_session(self):
|
||||
"""
|
||||
This method finds the session where SQRL transaction was initiated.
|
||||
This is the session where user potentially will be signed in.
|
||||
"""
|
||||
self.session = SessionMiddleware().SessionStore(self.nut.session_key)
|
||||
|
||||
def find_identities(self):
|
||||
"""
|
||||
This method finds both current and previous SQRL identities.
|
||||
At most only one of them should be found.
|
||||
"""
|
||||
self.identity = self._get_identity(self.cleaned_data['client']['idk'])
|
||||
self.previous_identity = self._get_identity(self.cleaned_data['client'].get('pidk'))
|
||||
|
||||
def _get_identity(self, key):
|
||||
if not key:
|
||||
return None
|
||||
|
||||
return SQRLIdentity.objects.filter(
|
||||
public_key=Base64.encode(key)
|
||||
).first()
|
||||
|
||||
|
||||
class PasswordLessUserCreationForm(UserCreationForm):
|
||||
"""
|
||||
Form for creating user account without password.
|
||||
This form is used when user successfully completes SQRL transaction
|
||||
however does not yet have a user account. Since they already successfully
|
||||
used SQRL, this implies that they they prefer to use SQRL over
|
||||
username/password. Therefore we simply create a user with unusable
|
||||
password using Django's ``set_unusable_password`` capability.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PasswordLessUserCreationForm, self).__init__(*args, **kwargs)
|
||||
# loop over all the fields and remove all password fields
|
||||
# by default this removes both password and verify_password fields
|
||||
for field in list(self.fields):
|
||||
if 'password' in field:
|
||||
self.fields.pop(field)
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
Custom user save implementation which saves user with unusable password.
|
||||
The implementation is very similar to how ``UserCreationForm`` saves
|
||||
a user model, except this method uses :meth:`AbstractBaseUser.set_unusable_password`
|
||||
to set a users password field.
|
||||
"""
|
||||
user = super(UserCreationForm, self).save(commit=False)
|
||||
user.set_unusable_password()
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
2
sqrl/management/__init__.py
Normal file
2
sqrl/management/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function, unicode_literals
|
2
sqrl/management/commands/__init__.py
Normal file
2
sqrl/management/commands/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function, unicode_literals
|
21
sqrl/management/commands/clearsqrlnuts.py
Normal file
21
sqrl/management/commands/clearsqrlnuts.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.timezone import now
|
||||
|
||||
from sqrl.models import SQRLNut
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ('Clears expired SQRL nuts. '
|
||||
'This command should be used as a cron job. '
|
||||
'The recommended execution frequency is 5 minutes '
|
||||
'which will result in longest nut lifespan of 10 minutes.')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
ttl = getattr(settings, 'SQRL', {}).get('TTL', 60 * 5) # 5 minutes
|
||||
delete_before = now() + timedelta(seconds=-ttl)
|
||||
|
||||
SQRLNut.objects.filter(timestamp__lt=delete_before).delete()
|
66
sqrl/response.py
Normal file
66
sqrl/response.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from pprint import pformat
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .crypto import HMAC
|
||||
from .exceptions import TIF
|
||||
from .utils import Encoder
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SQRLHttpResponse(HttpResponse):
|
||||
"""
|
||||
Custom ``HTTPResponse`` class used to return SQRL-formatted response.
|
||||
|
||||
The response is automatically signed, normalized and encoded
|
||||
as per SQRL specification.
|
||||
|
||||
This view also adds a couple of DEBUG logs for easier SQRL debugging
|
||||
and also returns all SQRL data back as ``X-SQRL-*`` headers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nut : SQRLNut
|
||||
Nut which will be used to sign the response data.
|
||||
data : OrderedDict
|
||||
Data to be returned back to the user.
|
||||
"""
|
||||
|
||||
def __init__(self, nut, data, *args, **kwargs):
|
||||
normalized_data = Encoder.normalize(self.sign_response(nut, data))
|
||||
content = Encoder.base64_dumps(normalized_data)
|
||||
|
||||
kwargs.setdefault('content_type', 'application/sqrl')
|
||||
|
||||
super(SQRLHttpResponse, self).__init__(content, *args, **kwargs)
|
||||
|
||||
self['Content-Length'] = len(self.content)
|
||||
|
||||
if settings.DEBUG:
|
||||
for k, v in normalized_data.items():
|
||||
self['X-SQRL-{}'.format(k)] = v
|
||||
|
||||
log.debug('Response encoded data:\n{}'
|
||||
''.format(content))
|
||||
log.debug('Response data:\n{}'
|
||||
''.format(pformat(normalized_data)))
|
||||
log.debug('Response TIF breakdown:\n{}'
|
||||
''.format(pformat(TIF(int(data['tif'], 16)).breakdown())))
|
||||
|
||||
def sign_response(self, nut, data):
|
||||
"""
|
||||
When nut is present, this method signs the data by adding ``mac`` key.
|
||||
|
||||
For signing :meth:`.crypto.HMAC.sign_data` is used.
|
||||
"""
|
||||
if not nut:
|
||||
return data
|
||||
|
||||
data['mac'] = HMAC(nut, data).sign_data()
|
||||
|
||||
return data
|
|
@ -136,8 +136,6 @@ class SQRLInitialization(object):
|
|||
def sqrl_url(self):
|
||||
"""
|
||||
Property for getting full SQRL auth view URL including SQRL scheme and full domain with port.
|
||||
|
||||
Is qrl:// a recognized schema? I can't find documentation of it anywhere...
|
||||
"""
|
||||
return (
|
||||
'{scheme}://{host}{url}'
|
||||
|
|
82
sqrl/static/admin/sqrl.css
Normal file
82
sqrl/static/admin/sqrl.css
Normal file
|
@ -0,0 +1,82 @@
|
|||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
text-align: center;
|
||||
clear: both;
|
||||
padding: 1em 0 0 0;
|
||||
}
|
||||
|
||||
.line-center {
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
background: #ffffff;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sqrl {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sqrl .or {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sqrl .or:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-top: solid 1px #dddddd;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.sqrl .or {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.sqrl h3 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.sqrl .img {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sqrl .img img {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.sqrl .submit-row {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.sqrl-wrap {
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
border-radius: 5%;
|
||||
}
|
||||
#sqrl-id img {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
}
|
||||
.sqrl-wrap a {
|
||||
font-size: 50%;
|
||||
}
|
850
sqrl/static/sqrl/sqrl.js
Normal file
850
sqrl/static/sqrl/sqrl.js
Normal file
|
@ -0,0 +1,850 @@
|
|||
'use strict';
|
||||
|
||||
(function() {
|
||||
var get_next_url = function() {
|
||||
var input = document.querySelectorAll('input[name="next"]');
|
||||
return input.length > 0 ? input[0].value : null;
|
||||
},
|
||||
current_url = window.location.href,
|
||||
sqrl_frequency = 1500,
|
||||
sqrl_call = function() {
|
||||
setTimeout(sqrl_handler, sqrl_frequency);
|
||||
},
|
||||
sqrl_handler = function() {
|
||||
var request = new XMLHttpRequest(),
|
||||
url = SQRL_CHECK_URL + '?url=',
|
||||
next_url = get_next_url();
|
||||
|
||||
if (next_url !== null) {
|
||||
url = url + encodeURIComponent('?next=' + next_url);
|
||||
} else {
|
||||
url = url + encodeURIComponent(current_url);
|
||||
}
|
||||
|
||||
request.open('POST', url, false);
|
||||
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
request.onreadystatechange = handleStateChange;
|
||||
|
||||
function handleStateChange() {
|
||||
if (request.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
var data = JSON.parse(this.responseText);
|
||||
if (data.transaction_complete === true) {
|
||||
if (data.redirect_to !== undefined) {
|
||||
window.location.href = data.redirect_to;
|
||||
} else {
|
||||
console.error('Server indicated that SQRL transaction is complete ' +
|
||||
'but has not indicated where to redirect');
|
||||
}
|
||||
} else {
|
||||
sqrl_call();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
request.send(null);
|
||||
} catch (exception) {
|
||||
// do not send anymore requests if error occurred
|
||||
}
|
||||
};
|
||||
|
||||
window.onload = sqrl_handler;
|
||||
|
||||
})();
|
||||
|
||||
// This javascript is a mostly unchanged version of David Shim's qrcode.js, originally released under the MIT liscense with no conditionals specified
|
||||
// All credit goes to them
|
||||
// The original can be found here:
|
||||
// https://davidshimjs.github.io/qrcodejs/
|
||||
|
||||
var QRCode;
|
||||
! function() {
|
||||
function a(a) {
|
||||
this.mode = c.MODE_8BIT_BYTE, this.data = a, this.parsedData = [];
|
||||
for (var b = [], d = 0, e = this.data.length; e > d; d++) {
|
||||
var f = this.data.charCodeAt(d);
|
||||
f > 65536 ? (b[0] = 240 | (1835008 & f) >>> 18, b[1] = 128 | (258048 & f) >>> 12, b[2] = 128 | (4032 & f) >>> 6, b[3] = 128 | 63 & f) : f > 2048 ? (b[0] = 224 | (61440 & f) >>> 12, b[1] = 128 | (4032 & f) >>> 6, b[2] = 128 | 63 & f) : f > 128 ? (b[0] = 192 | (1984 & f) >>> 6, b[1] = 128 | 63 & f) : b[0] = f, this.parsedData = this.parsedData.concat(b)
|
||||
}
|
||||
this.parsedData.length != this.data.length && (this.parsedData.unshift(191), this.parsedData.unshift(187), this.parsedData.unshift(239))
|
||||
}
|
||||
|
||||
function b(a, b) {
|
||||
this.typeNumber = a, this.errorCorrectLevel = b, this.modules = null, this.moduleCount = 0, this.dataCache = null, this.dataList = []
|
||||
}
|
||||
|
||||
function i(a, b) {
|
||||
if (void 0 == a.length) throw new Error(a.length + "/" + b);
|
||||
for (var c = 0; c < a.length && 0 == a[c];) c++;
|
||||
this.num = new Array(a.length - c + b);
|
||||
for (var d = 0; d < a.length - c; d++) this.num[d] = a[d + c]
|
||||
}
|
||||
|
||||
function j(a, b) {
|
||||
this.totalCount = a, this.dataCount = b
|
||||
}
|
||||
|
||||
function k() {
|
||||
this.buffer = [], this.length = 0
|
||||
}
|
||||
|
||||
function m() {
|
||||
return "undefined" != typeof CanvasRenderingContext2D
|
||||
}
|
||||
|
||||
function n() {
|
||||
var a = !1,
|
||||
b = navigator.userAgent;
|
||||
return /android/i.test(b) && (a = !0, aMat = b.toString().match(/android ([0-9]\.[0-9])/i), aMat && aMat[1] && (a = parseFloat(aMat[1]))), a
|
||||
}
|
||||
|
||||
function r(a, b) {
|
||||
for (var c = 1, e = s(a), f = 0, g = l.length; g >= f; f++) {
|
||||
var h = 0;
|
||||
switch (b) {
|
||||
case d.L:
|
||||
h = l[f][0];
|
||||
break;
|
||||
case d.M:
|
||||
h = l[f][1];
|
||||
break;
|
||||
case d.Q:
|
||||
h = l[f][2];
|
||||
break;
|
||||
case d.H:
|
||||
h = l[f][3]
|
||||
}
|
||||
if (h >= e) break;
|
||||
c++
|
||||
}
|
||||
if (c > l.length) throw new Error("Too long data");
|
||||
return c
|
||||
}
|
||||
|
||||
function s(a) {
|
||||
var b = encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g, "a");
|
||||
return b.length + (b.length != a ? 3 : 0)
|
||||
}
|
||||
a.prototype = {
|
||||
getLength: function() {
|
||||
return this.parsedData.length
|
||||
},
|
||||
write: function(a) {
|
||||
for (var b = 0, c = this.parsedData.length; c > b; b++) a.put(this.parsedData[b], 8)
|
||||
}
|
||||
}, b.prototype = {
|
||||
addData: function(b) {
|
||||
var c = new a(b);
|
||||
this.dataList.push(c), this.dataCache = null
|
||||
},
|
||||
isDark: function(a, b) {
|
||||
if (0 > a || this.moduleCount <= a || 0 > b || this.moduleCount <= b) throw new Error(a + "," + b);
|
||||
return this.modules[a][b]
|
||||
},
|
||||
getModuleCount: function() {
|
||||
return this.moduleCount
|
||||
},
|
||||
make: function() {
|
||||
this.makeImpl(!1, this.getBestMaskPattern())
|
||||
},
|
||||
makeImpl: function(a, c) {
|
||||
this.moduleCount = 4 * this.typeNumber + 17, this.modules = new Array(this.moduleCount);
|
||||
for (var d = 0; d < this.moduleCount; d++) {
|
||||
this.modules[d] = new Array(this.moduleCount);
|
||||
for (var e = 0; e < this.moduleCount; e++) this.modules[d][e] = null
|
||||
}
|
||||
this.setupPositionProbePattern(0, 0), this.setupPositionProbePattern(this.moduleCount - 7, 0), this.setupPositionProbePattern(0, this.moduleCount - 7), this.setupPositionAdjustPattern(), this.setupTimingPattern(), this.setupTypeInfo(a, c), this.typeNumber >= 7 && this.setupTypeNumber(a), null == this.dataCache && (this.dataCache = b.createData(this.typeNumber, this.errorCorrectLevel, this.dataList)), this.mapData(this.dataCache, c)
|
||||
},
|
||||
setupPositionProbePattern: function(a, b) {
|
||||
for (var c = -1; 7 >= c; c++)
|
||||
if (!(-1 >= a + c || this.moduleCount <= a + c))
|
||||
for (var d = -1; 7 >= d; d++) - 1 >= b + d || this.moduleCount <= b + d || (this.modules[a + c][b + d] = c >= 0 && 6 >= c && (0 == d || 6 == d) || d >= 0 && 6 >= d && (0 == c || 6 == c) || c >= 2 && 4 >= c && d >= 2 && 4 >= d ? !0 : !1)
|
||||
},
|
||||
getBestMaskPattern: function() {
|
||||
for (var a = 0, b = 0, c = 0; 8 > c; c++) {
|
||||
this.makeImpl(!0, c);
|
||||
var d = f.getLostPoint(this);
|
||||
(0 == c || a > d) && (a = d, b = c)
|
||||
}
|
||||
return b
|
||||
},
|
||||
createMovieClip: function(a, b, c) {
|
||||
var d = a.createEmptyMovieClip(b, c),
|
||||
e = 1;
|
||||
this.make();
|
||||
for (var f = 0; f < this.modules.length; f++)
|
||||
for (var g = f * e, h = 0; h < this.modules[f].length; h++) {
|
||||
var i = h * e,
|
||||
j = this.modules[f][h];
|
||||
j && (d.beginFill(0, 100), d.moveTo(i, g), d.lineTo(i + e, g), d.lineTo(i + e, g + e), d.lineTo(i, g + e), d.endFill())
|
||||
}
|
||||
return d
|
||||
},
|
||||
setupTimingPattern: function() {
|
||||
for (var a = 8; a < this.moduleCount - 8; a++) null == this.modules[a][6] && (this.modules[a][6] = 0 == a % 2);
|
||||
for (var b = 8; b < this.moduleCount - 8; b++) null == this.modules[6][b] && (this.modules[6][b] = 0 == b % 2)
|
||||
},
|
||||
setupPositionAdjustPattern: function() {
|
||||
for (var a = f.getPatternPosition(this.typeNumber), b = 0; b < a.length; b++)
|
||||
for (var c = 0; c < a.length; c++) {
|
||||
var d = a[b],
|
||||
e = a[c];
|
||||
if (null == this.modules[d][e])
|
||||
for (var g = -2; 2 >= g; g++)
|
||||
for (var h = -2; 2 >= h; h++) this.modules[d + g][e + h] = -2 == g || 2 == g || -2 == h || 2 == h || 0 == g && 0 == h ? !0 : !1
|
||||
}
|
||||
},
|
||||
setupTypeNumber: function(a) {
|
||||
for (var b = f.getBCHTypeNumber(this.typeNumber), c = 0; 18 > c; c++) {
|
||||
var d = !a && 1 == (1 & b >> c);
|
||||
this.modules[Math.floor(c / 3)][c % 3 + this.moduleCount - 8 - 3] = d
|
||||
}
|
||||
for (var c = 0; 18 > c; c++) {
|
||||
var d = !a && 1 == (1 & b >> c);
|
||||
this.modules[c % 3 + this.moduleCount - 8 - 3][Math.floor(c / 3)] = d
|
||||
}
|
||||
},
|
||||
setupTypeInfo: function(a, b) {
|
||||
for (var c = this.errorCorrectLevel << 3 | b, d = f.getBCHTypeInfo(c), e = 0; 15 > e; e++) {
|
||||
var g = !a && 1 == (1 & d >> e);
|
||||
6 > e ? this.modules[e][8] = g : 8 > e ? this.modules[e + 1][8] = g : this.modules[this.moduleCount - 15 + e][8] = g
|
||||
}
|
||||
for (var e = 0; 15 > e; e++) {
|
||||
var g = !a && 1 == (1 & d >> e);
|
||||
8 > e ? this.modules[8][this.moduleCount - e - 1] = g : 9 > e ? this.modules[8][15 - e - 1 + 1] = g : this.modules[8][15 - e - 1] = g
|
||||
}
|
||||
this.modules[this.moduleCount - 8][8] = !a
|
||||
},
|
||||
mapData: function(a, b) {
|
||||
for (var c = -1, d = this.moduleCount - 1, e = 7, g = 0, h = this.moduleCount - 1; h > 0; h -= 2)
|
||||
for (6 == h && h--;;) {
|
||||
for (var i = 0; 2 > i; i++)
|
||||
if (null == this.modules[d][h - i]) {
|
||||
var j = !1;
|
||||
g < a.length && (j = 1 == (1 & a[g] >>> e));
|
||||
var k = f.getMask(b, d, h - i);
|
||||
k && (j = !j), this.modules[d][h - i] = j, e--, -1 == e && (g++, e = 7)
|
||||
} if (d += c, 0 > d || this.moduleCount <= d) {
|
||||
d -= c, c = -c;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, b.PAD0 = 236, b.PAD1 = 17, b.createData = function(a, c, d) {
|
||||
for (var e = j.getRSBlocks(a, c), g = new k, h = 0; h < d.length; h++) {
|
||||
var i = d[h];
|
||||
g.put(i.mode, 4), g.put(i.getLength(), f.getLengthInBits(i.mode, a)), i.write(g)
|
||||
}
|
||||
for (var l = 0, h = 0; h < e.length; h++) l += e[h].dataCount;
|
||||
if (g.getLengthInBits() > 8 * l) throw new Error("code length overflow. (" + g.getLengthInBits() + ">" + 8 * l + ")");
|
||||
for (g.getLengthInBits() + 4 <= 8 * l && g.put(0, 4); 0 != g.getLengthInBits() % 8;) g.putBit(!1);
|
||||
for (;;) {
|
||||
if (g.getLengthInBits() >= 8 * l) break;
|
||||
if (g.put(b.PAD0, 8), g.getLengthInBits() >= 8 * l) break;
|
||||
g.put(b.PAD1, 8)
|
||||
}
|
||||
return b.createBytes(g, e)
|
||||
}, b.createBytes = function(a, b) {
|
||||
for (var c = 0, d = 0, e = 0, g = new Array(b.length), h = new Array(b.length), j = 0; j < b.length; j++) {
|
||||
var k = b[j].dataCount,
|
||||
l = b[j].totalCount - k;
|
||||
d = Math.max(d, k), e = Math.max(e, l), g[j] = new Array(k);
|
||||
for (var m = 0; m < g[j].length; m++) g[j][m] = 255 & a.buffer[m + c];
|
||||
c += k;
|
||||
var n = f.getErrorCorrectPolynomial(l),
|
||||
o = new i(g[j], n.getLength() - 1),
|
||||
p = o.mod(n);
|
||||
h[j] = new Array(n.getLength() - 1);
|
||||
for (var m = 0; m < h[j].length; m++) {
|
||||
var q = m + p.getLength() - h[j].length;
|
||||
h[j][m] = q >= 0 ? p.get(q) : 0
|
||||
}
|
||||
}
|
||||
for (var r = 0, m = 0; m < b.length; m++) r += b[m].totalCount;
|
||||
for (var s = new Array(r), t = 0, m = 0; d > m; m++)
|
||||
for (var j = 0; j < b.length; j++) m < g[j].length && (s[t++] = g[j][m]);
|
||||
for (var m = 0; e > m; m++)
|
||||
for (var j = 0; j < b.length; j++) m < h[j].length && (s[t++] = h[j][m]);
|
||||
return s
|
||||
};
|
||||
for (var c = {
|
||||
MODE_NUMBER: 1,
|
||||
MODE_ALPHA_NUM: 2,
|
||||
MODE_8BIT_BYTE: 4,
|
||||
MODE_KANJI: 8
|
||||
}, d = {
|
||||
L: 1,
|
||||
M: 0,
|
||||
Q: 3,
|
||||
H: 2
|
||||
}, e = {
|
||||
PATTERN000: 0,
|
||||
PATTERN001: 1,
|
||||
PATTERN010: 2,
|
||||
PATTERN011: 3,
|
||||
PATTERN100: 4,
|
||||
PATTERN101: 5,
|
||||
PATTERN110: 6,
|
||||
PATTERN111: 7
|
||||
}, f = {
|
||||
PATTERN_POSITION_TABLE: [
|
||||
[],
|
||||
[6, 18],
|
||||
[6, 22],
|
||||
[6, 26],
|
||||
[6, 30],
|
||||
[6, 34],
|
||||
[6, 22, 38],
|
||||
[6, 24, 42],
|
||||
[6, 26, 46],
|
||||
[6, 28, 50],
|
||||
[6, 30, 54],
|
||||
[6, 32, 58],
|
||||
[6, 34, 62],
|
||||
[6, 26, 46, 66],
|
||||
[6, 26, 48, 70],
|
||||
[6, 26, 50, 74],
|
||||
[6, 30, 54, 78],
|
||||
[6, 30, 56, 82],
|
||||
[6, 30, 58, 86],
|
||||
[6, 34, 62, 90],
|
||||
[6, 28, 50, 72, 94],
|
||||
[6, 26, 50, 74, 98],
|
||||
[6, 30, 54, 78, 102],
|
||||
[6, 28, 54, 80, 106],
|
||||
[6, 32, 58, 84, 110],
|
||||
[6, 30, 58, 86, 114],
|
||||
[6, 34, 62, 90, 118],
|
||||
[6, 26, 50, 74, 98, 122],
|
||||
[6, 30, 54, 78, 102, 126],
|
||||
[6, 26, 52, 78, 104, 130],
|
||||
[6, 30, 56, 82, 108, 134],
|
||||
[6, 34, 60, 86, 112, 138],
|
||||
[6, 30, 58, 86, 114, 142],
|
||||
[6, 34, 62, 90, 118, 146],
|
||||
[6, 30, 54, 78, 102, 126, 150],
|
||||
[6, 24, 50, 76, 102, 128, 154],
|
||||
[6, 28, 54, 80, 106, 132, 158],
|
||||
[6, 32, 58, 84, 110, 136, 162],
|
||||
[6, 26, 54, 82, 110, 138, 166],
|
||||
[6, 30, 58, 86, 114, 142, 170]
|
||||
],
|
||||
G15: 1335,
|
||||
G18: 7973,
|
||||
G15_MASK: 21522,
|
||||
getBCHTypeInfo: function(a) {
|
||||
for (var b = a << 10; f.getBCHDigit(b) - f.getBCHDigit(f.G15) >= 0;) b ^= f.G15 << f.getBCHDigit(b) - f.getBCHDigit(f.G15);
|
||||
return (a << 10 | b) ^ f.G15_MASK
|
||||
},
|
||||
getBCHTypeNumber: function(a) {
|
||||
for (var b = a << 12; f.getBCHDigit(b) - f.getBCHDigit(f.G18) >= 0;) b ^= f.G18 << f.getBCHDigit(b) - f.getBCHDigit(f.G18);
|
||||
return a << 12 | b
|
||||
},
|
||||
getBCHDigit: function(a) {
|
||||
for (var b = 0; 0 != a;) b++, a >>>= 1;
|
||||
return b
|
||||
},
|
||||
getPatternPosition: function(a) {
|
||||
return f.PATTERN_POSITION_TABLE[a - 1]
|
||||
},
|
||||
getMask: function(a, b, c) {
|
||||
switch (a) {
|
||||
case e.PATTERN000:
|
||||
return 0 == (b + c) % 2;
|
||||
case e.PATTERN001:
|
||||
return 0 == b % 2;
|
||||
case e.PATTERN010:
|
||||
return 0 == c % 3;
|
||||
case e.PATTERN011:
|
||||
return 0 == (b + c) % 3;
|
||||
case e.PATTERN100:
|
||||
return 0 == (Math.floor(b / 2) + Math.floor(c / 3)) % 2;
|
||||
case e.PATTERN101:
|
||||
return 0 == b * c % 2 + b * c % 3;
|
||||
case e.PATTERN110:
|
||||
return 0 == (b * c % 2 + b * c % 3) % 2;
|
||||
case e.PATTERN111:
|
||||
return 0 == (b * c % 3 + (b + c) % 2) % 2;
|
||||
default:
|
||||
throw new Error("bad maskPattern:" + a)
|
||||
}
|
||||
},
|
||||
getErrorCorrectPolynomial: function(a) {
|
||||
for (var b = new i([1], 0), c = 0; a > c; c++) b = b.multiply(new i([1, g.gexp(c)], 0));
|
||||
return b
|
||||
},
|
||||
getLengthInBits: function(a, b) {
|
||||
if (b >= 1 && 10 > b) switch (a) {
|
||||
case c.MODE_NUMBER:
|
||||
return 10;
|
||||
case c.MODE_ALPHA_NUM:
|
||||
return 9;
|
||||
case c.MODE_8BIT_BYTE:
|
||||
return 8;
|
||||
case c.MODE_KANJI:
|
||||
return 8;
|
||||
default:
|
||||
throw new Error("mode:" + a)
|
||||
} else if (27 > b) switch (a) {
|
||||
case c.MODE_NUMBER:
|
||||
return 12;
|
||||
case c.MODE_ALPHA_NUM:
|
||||
return 11;
|
||||
case c.MODE_8BIT_BYTE:
|
||||
return 16;
|
||||
case c.MODE_KANJI:
|
||||
return 10;
|
||||
default:
|
||||
throw new Error("mode:" + a)
|
||||
} else {
|
||||
if (!(41 > b)) throw new Error("type:" + b);
|
||||
switch (a) {
|
||||
case c.MODE_NUMBER:
|
||||
return 14;
|
||||
case c.MODE_ALPHA_NUM:
|
||||
return 13;
|
||||
case c.MODE_8BIT_BYTE:
|
||||
return 16;
|
||||
case c.MODE_KANJI:
|
||||
return 12;
|
||||
default:
|
||||
throw new Error("mode:" + a)
|
||||
}
|
||||
}
|
||||
},
|
||||
getLostPoint: function(a) {
|
||||
for (var b = a.getModuleCount(), c = 0, d = 0; b > d; d++)
|
||||
for (var e = 0; b > e; e++) {
|
||||
for (var f = 0, g = a.isDark(d, e), h = -1; 1 >= h; h++)
|
||||
if (!(0 > d + h || d + h >= b))
|
||||
for (var i = -1; 1 >= i; i++) 0 > e + i || e + i >= b || (0 != h || 0 != i) && g == a.isDark(d + h, e + i) && f++;
|
||||
f > 5 && (c += 3 + f - 5)
|
||||
}
|
||||
for (var d = 0; b - 1 > d; d++)
|
||||
for (var e = 0; b - 1 > e; e++) {
|
||||
var j = 0;
|
||||
a.isDark(d, e) && j++, a.isDark(d + 1, e) && j++, a.isDark(d, e + 1) && j++, a.isDark(d + 1, e + 1) && j++, (0 == j || 4 == j) && (c += 3)
|
||||
}
|
||||
for (var d = 0; b > d; d++)
|
||||
for (var e = 0; b - 6 > e; e++) a.isDark(d, e) && !a.isDark(d, e + 1) && a.isDark(d, e + 2) && a.isDark(d, e + 3) && a.isDark(d, e + 4) && !a.isDark(d, e + 5) && a.isDark(d, e + 6) && (c += 40);
|
||||
for (var e = 0; b > e; e++)
|
||||
for (var d = 0; b - 6 > d; d++) a.isDark(d, e) && !a.isDark(d + 1, e) && a.isDark(d + 2, e) && a.isDark(d + 3, e) && a.isDark(d + 4, e) && !a.isDark(d + 5, e) && a.isDark(d + 6, e) && (c += 40);
|
||||
for (var k = 0, e = 0; b > e; e++)
|
||||
for (var d = 0; b > d; d++) a.isDark(d, e) && k++;
|
||||
var l = Math.abs(100 * k / b / b - 50) / 5;
|
||||
return c += 10 * l
|
||||
}
|
||||
}, g = {
|
||||
glog: function(a) {
|
||||
if (1 > a) throw new Error("glog(" + a + ")");
|
||||
return g.LOG_TABLE[a]
|
||||
},
|
||||
gexp: function(a) {
|
||||
for (; 0 > a;) a += 255;
|
||||
for (; a >= 256;) a -= 255;
|
||||
return g.EXP_TABLE[a]
|
||||
},
|
||||
EXP_TABLE: new Array(256),
|
||||
LOG_TABLE: new Array(256)
|
||||
}, h = 0; 8 > h; h++) g.EXP_TABLE[h] = 1 << h;
|
||||
for (var h = 8; 256 > h; h++) g.EXP_TABLE[h] = g.EXP_TABLE[h - 4] ^ g.EXP_TABLE[h - 5] ^ g.EXP_TABLE[h - 6] ^ g.EXP_TABLE[h - 8];
|
||||
for (var h = 0; 255 > h; h++) g.LOG_TABLE[g.EXP_TABLE[h]] = h;
|
||||
i.prototype = {
|
||||
get: function(a) {
|
||||
return this.num[a]
|
||||
},
|
||||
getLength: function() {
|
||||
return this.num.length
|
||||
},
|
||||
multiply: function(a) {
|
||||
for (var b = new Array(this.getLength() + a.getLength() - 1), c = 0; c < this.getLength(); c++)
|
||||
for (var d = 0; d < a.getLength(); d++) b[c + d] ^= g.gexp(g.glog(this.get(c)) + g.glog(a.get(d)));
|
||||
return new i(b, 0)
|
||||
},
|
||||
mod: function(a) {
|
||||
if (this.getLength() - a.getLength() < 0) return this;
|
||||
for (var b = g.glog(this.get(0)) - g.glog(a.get(0)), c = new Array(this.getLength()), d = 0; d < this.getLength(); d++) c[d] = this.get(d);
|
||||
for (var d = 0; d < a.getLength(); d++) c[d] ^= g.gexp(g.glog(a.get(d)) + b);
|
||||
return new i(c, 0).mod(a)
|
||||
}
|
||||
}, j.RS_BLOCK_TABLE = [
|
||||
[1, 26, 19],
|
||||
[1, 26, 16],
|
||||
[1, 26, 13],
|
||||
[1, 26, 9],
|
||||
[1, 44, 34],
|
||||
[1, 44, 28],
|
||||
[1, 44, 22],
|
||||
[1, 44, 16],
|
||||
[1, 70, 55],
|
||||
[1, 70, 44],
|
||||
[2, 35, 17],
|
||||
[2, 35, 13],
|
||||
[1, 100, 80],
|
||||
[2, 50, 32],
|
||||
[2, 50, 24],
|
||||
[4, 25, 9],
|
||||
[1, 134, 108],
|
||||
[2, 67, 43],
|
||||
[2, 33, 15, 2, 34, 16],
|
||||
[2, 33, 11, 2, 34, 12],
|
||||
[2, 86, 68],
|
||||
[4, 43, 27],
|
||||
[4, 43, 19],
|
||||
[4, 43, 15],
|
||||
[2, 98, 78],
|
||||
[4, 49, 31],
|
||||
[2, 32, 14, 4, 33, 15],
|
||||
[4, 39, 13, 1, 40, 14],
|
||||
[2, 121, 97],
|
||||
[2, 60, 38, 2, 61, 39],
|
||||
[4, 40, 18, 2, 41, 19],
|
||||
[4, 40, 14, 2, 41, 15],
|
||||
[2, 146, 116],
|
||||
[3, 58, 36, 2, 59, 37],
|
||||
[4, 36, 16, 4, 37, 17],
|
||||
[4, 36, 12, 4, 37, 13],
|
||||
[2, 86, 68, 2, 87, 69],
|
||||
[4, 69, 43, 1, 70, 44],
|
||||
[6, 43, 19, 2, 44, 20],
|
||||
[6, 43, 15, 2, 44, 16],
|
||||
[4, 101, 81],
|
||||
[1, 80, 50, 4, 81, 51],
|
||||
[4, 50, 22, 4, 51, 23],
|
||||
[3, 36, 12, 8, 37, 13],
|
||||
[2, 116, 92, 2, 117, 93],
|
||||
[6, 58, 36, 2, 59, 37],
|
||||
[4, 46, 20, 6, 47, 21],
|
||||
[7, 42, 14, 4, 43, 15],
|
||||
[4, 133, 107],
|
||||
[8, 59, 37, 1, 60, 38],
|
||||
[8, 44, 20, 4, 45, 21],
|
||||
[12, 33, 11, 4, 34, 12],
|
||||
[3, 145, 115, 1, 146, 116],
|
||||
[4, 64, 40, 5, 65, 41],
|
||||
[11, 36, 16, 5, 37, 17],
|
||||
[11, 36, 12, 5, 37, 13],
|
||||
[5, 109, 87, 1, 110, 88],
|
||||
[5, 65, 41, 5, 66, 42],
|
||||
[5, 54, 24, 7, 55, 25],
|
||||
[11, 36, 12],
|
||||
[5, 122, 98, 1, 123, 99],
|
||||
[7, 73, 45, 3, 74, 46],
|
||||
[15, 43, 19, 2, 44, 20],
|
||||
[3, 45, 15, 13, 46, 16],
|
||||
[1, 135, 107, 5, 136, 108],
|
||||
[10, 74, 46, 1, 75, 47],
|
||||
[1, 50, 22, 15, 51, 23],
|
||||
[2, 42, 14, 17, 43, 15],
|
||||
[5, 150, 120, 1, 151, 121],
|
||||
[9, 69, 43, 4, 70, 44],
|
||||
[17, 50, 22, 1, 51, 23],
|
||||
[2, 42, 14, 19, 43, 15],
|
||||
[3, 141, 113, 4, 142, 114],
|
||||
[3, 70, 44, 11, 71, 45],
|
||||
[17, 47, 21, 4, 48, 22],
|
||||
[9, 39, 13, 16, 40, 14],
|
||||
[3, 135, 107, 5, 136, 108],
|
||||
[3, 67, 41, 13, 68, 42],
|
||||
[15, 54, 24, 5, 55, 25],
|
||||
[15, 43, 15, 10, 44, 16],
|
||||
[4, 144, 116, 4, 145, 117],
|
||||
[17, 68, 42],
|
||||
[17, 50, 22, 6, 51, 23],
|
||||
[19, 46, 16, 6, 47, 17],
|
||||
[2, 139, 111, 7, 140, 112],
|
||||
[17, 74, 46],
|
||||
[7, 54, 24, 16, 55, 25],
|
||||
[34, 37, 13],
|
||||
[4, 151, 121, 5, 152, 122],
|
||||
[4, 75, 47, 14, 76, 48],
|
||||
[11, 54, 24, 14, 55, 25],
|
||||
[16, 45, 15, 14, 46, 16],
|
||||
[6, 147, 117, 4, 148, 118],
|
||||
[6, 73, 45, 14, 74, 46],
|
||||
[11, 54, 24, 16, 55, 25],
|
||||
[30, 46, 16, 2, 47, 17],
|
||||
[8, 132, 106, 4, 133, 107],
|
||||
[8, 75, 47, 13, 76, 48],
|
||||
[7, 54, 24, 22, 55, 25],
|
||||
[22, 45, 15, 13, 46, 16],
|
||||
[10, 142, 114, 2, 143, 115],
|
||||
[19, 74, 46, 4, 75, 47],
|
||||
[28, 50, 22, 6, 51, 23],
|
||||
[33, 46, 16, 4, 47, 17],
|
||||
[8, 152, 122, 4, 153, 123],
|
||||
[22, 73, 45, 3, 74, 46],
|
||||
[8, 53, 23, 26, 54, 24],
|
||||
[12, 45, 15, 28, 46, 16],
|
||||
[3, 147, 117, 10, 148, 118],
|
||||
[3, 73, 45, 23, 74, 46],
|
||||
[4, 54, 24, 31, 55, 25],
|
||||
[11, 45, 15, 31, 46, 16],
|
||||
[7, 146, 116, 7, 147, 117],
|
||||
[21, 73, 45, 7, 74, 46],
|
||||
[1, 53, 23, 37, 54, 24],
|
||||
[19, 45, 15, 26, 46, 16],
|
||||
[5, 145, 115, 10, 146, 116],
|
||||
[19, 75, 47, 10, 76, 48],
|
||||
[15, 54, 24, 25, 55, 25],
|
||||
[23, 45, 15, 25, 46, 16],
|
||||
[13, 145, 115, 3, 146, 116],
|
||||
[2, 74, 46, 29, 75, 47],
|
||||
[42, 54, 24, 1, 55, 25],
|
||||
[23, 45, 15, 28, 46, 16],
|
||||
[17, 145, 115],
|
||||
[10, 74, 46, 23, 75, 47],
|
||||
[10, 54, 24, 35, 55, 25],
|
||||
[19, 45, 15, 35, 46, 16],
|
||||
[17, 145, 115, 1, 146, 116],
|
||||
[14, 74, 46, 21, 75, 47],
|
||||
[29, 54, 24, 19, 55, 25],
|
||||
[11, 45, 15, 46, 46, 16],
|
||||
[13, 145, 115, 6, 146, 116],
|
||||
[14, 74, 46, 23, 75, 47],
|
||||
[44, 54, 24, 7, 55, 25],
|
||||
[59, 46, 16, 1, 47, 17],
|
||||
[12, 151, 121, 7, 152, 122],
|
||||
[12, 75, 47, 26, 76, 48],
|
||||
[39, 54, 24, 14, 55, 25],
|
||||
[22, 45, 15, 41, 46, 16],
|
||||
[6, 151, 121, 14, 152, 122],
|
||||
[6, 75, 47, 34, 76, 48],
|
||||
[46, 54, 24, 10, 55, 25],
|
||||
[2, 45, 15, 64, 46, 16],
|
||||
[17, 152, 122, 4, 153, 123],
|
||||
[29, 74, 46, 14, 75, 47],
|
||||
[49, 54, 24, 10, 55, 25],
|
||||
[24, 45, 15, 46, 46, 16],
|
||||
[4, 152, 122, 18, 153, 123],
|
||||
[13, 74, 46, 32, 75, 47],
|
||||
[48, 54, 24, 14, 55, 25],
|
||||
[42, 45, 15, 32, 46, 16],
|
||||
[20, 147, 117, 4, 148, 118],
|
||||
[40, 75, 47, 7, 76, 48],
|
||||
[43, 54, 24, 22, 55, 25],
|
||||
[10, 45, 15, 67, 46, 16],
|
||||
[19, 148, 118, 6, 149, 119],
|
||||
[18, 75, 47, 31, 76, 48],
|
||||
[34, 54, 24, 34, 55, 25],
|
||||
[20, 45, 15, 61, 46, 16]
|
||||
], j.getRSBlocks = function(a, b) {
|
||||
var c = j.getRsBlockTable(a, b);
|
||||
if (void 0 == c) throw new Error("bad rs block @ typeNumber:" + a + "/errorCorrectLevel:" + b);
|
||||
for (var d = c.length / 3, e = [], f = 0; d > f; f++)
|
||||
for (var g = c[3 * f + 0], h = c[3 * f + 1], i = c[3 * f + 2], k = 0; g > k; k++) e.push(new j(h, i));
|
||||
return e
|
||||
}, j.getRsBlockTable = function(a, b) {
|
||||
switch (b) {
|
||||
case d.L:
|
||||
return j.RS_BLOCK_TABLE[4 * (a - 1) + 0];
|
||||
case d.M:
|
||||
return j.RS_BLOCK_TABLE[4 * (a - 1) + 1];
|
||||
case d.Q:
|
||||
return j.RS_BLOCK_TABLE[4 * (a - 1) + 2];
|
||||
case d.H:
|
||||
return j.RS_BLOCK_TABLE[4 * (a - 1) + 3];
|
||||
default:
|
||||
return void 0
|
||||
}
|
||||
}, k.prototype = {
|
||||
get: function(a) {
|
||||
var b = Math.floor(a / 8);
|
||||
return 1 == (1 & this.buffer[b] >>> 7 - a % 8)
|
||||
},
|
||||
put: function(a, b) {
|
||||
for (var c = 0; b > c; c++) this.putBit(1 == (1 & a >>> b - c - 1))
|
||||
},
|
||||
getLengthInBits: function() {
|
||||
return this.length
|
||||
},
|
||||
putBit: function(a) {
|
||||
var b = Math.floor(this.length / 8);
|
||||
this.buffer.length <= b && this.buffer.push(0), a && (this.buffer[b] |= 128 >>> this.length % 8), this.length++
|
||||
}
|
||||
};
|
||||
var l = [
|
||||
[17, 14, 11, 7],
|
||||
[32, 26, 20, 14],
|
||||
[53, 42, 32, 24],
|
||||
[78, 62, 46, 34],
|
||||
[106, 84, 60, 44],
|
||||
[134, 106, 74, 58],
|
||||
[154, 122, 86, 64],
|
||||
[192, 152, 108, 84],
|
||||
[230, 180, 130, 98],
|
||||
[271, 213, 151, 119],
|
||||
[321, 251, 177, 137],
|
||||
[367, 287, 203, 155],
|
||||
[425, 331, 241, 177],
|
||||
[458, 362, 258, 194],
|
||||
[520, 412, 292, 220],
|
||||
[586, 450, 322, 250],
|
||||
[644, 504, 364, 280],
|
||||
[718, 560, 394, 310],
|
||||
[792, 624, 442, 338],
|
||||
[858, 666, 482, 382],
|
||||
[929, 711, 509, 403],
|
||||
[1003, 779, 565, 439],
|
||||
[1091, 857, 611, 461],
|
||||
[1171, 911, 661, 511],
|
||||
[1273, 997, 715, 535],
|
||||
[1367, 1059, 751, 593],
|
||||
[1465, 1125, 805, 625],
|
||||
[1528, 1190, 868, 658],
|
||||
[1628, 1264, 908, 698],
|
||||
[1732, 1370, 982, 742],
|
||||
[1840, 1452, 1030, 790],
|
||||
[1952, 1538, 1112, 842],
|
||||
[2068, 1628, 1168, 898],
|
||||
[2188, 1722, 1228, 958],
|
||||
[2303, 1809, 1283, 983],
|
||||
[2431, 1911, 1351, 1051],
|
||||
[2563, 1989, 1423, 1093],
|
||||
[2699, 2099, 1499, 1139],
|
||||
[2809, 2213, 1579, 1219],
|
||||
[2953, 2331, 1663, 1273]
|
||||
],
|
||||
o = function() {
|
||||
var a = function(a, b) {
|
||||
this._el = a, this._htOption = b
|
||||
};
|
||||
return a.prototype.draw = function(a) {
|
||||
function g(a, b) {
|
||||
var c = document.createElementNS("http://www.w3.org/2000/svg", a);
|
||||
for (var d in b) b.hasOwnProperty(d) && c.setAttribute(d, b[d]);
|
||||
return c
|
||||
}
|
||||
var b = this._htOption,
|
||||
c = this._el,
|
||||
d = a.getModuleCount();
|
||||
Math.floor(b.width / d), Math.floor(b.height / d), this.clear();
|
||||
var h = g("svg", {
|
||||
viewBox: "0 0 " + String(d) + " " + String(d),
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
fill: b.colorLight
|
||||
});
|
||||
h.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"), c.appendChild(h), h.appendChild(g("rect", {
|
||||
fill: b.colorDark,
|
||||
width: "1",
|
||||
height: "1",
|
||||
id: "template"
|
||||
}));
|
||||
for (var i = 0; d > i; i++)
|
||||
for (var j = 0; d > j; j++)
|
||||
if (a.isDark(i, j)) {
|
||||
var k = g("use", {
|
||||
x: String(i),
|
||||
y: String(j)
|
||||
});
|
||||
k.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template"), h.appendChild(k)
|
||||
}
|
||||
}, a.prototype.clear = function() {
|
||||
for (; this._el.hasChildNodes();) this._el.removeChild(this._el.lastChild)
|
||||
}, a
|
||||
}(),
|
||||
p = "svg" === document.documentElement.tagName.toLowerCase(),
|
||||
q = p ? o : m() ? function() {
|
||||
function a() {
|
||||
this._elImage.src = this._elCanvas.toDataURL("image/png"), this._elImage.style.display = "block", this._elCanvas.style.display = "none"
|
||||
}
|
||||
|
||||
function d(a, b) {
|
||||
var c = this;
|
||||
if (c._fFail = b, c._fSuccess = a, null === c._bSupportDataURI) {
|
||||
var d = document.createElement("img"),
|
||||
e = function() {
|
||||
c._bSupportDataURI = !1, c._fFail && _fFail.call(c)
|
||||
},
|
||||
f = function() {
|
||||
c._bSupportDataURI = !0, c._fSuccess && c._fSuccess.call(c)
|
||||
};
|
||||
return d.onabort = e, d.onerror = e, d.onload = f, d.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", void 0
|
||||
}
|
||||
c._bSupportDataURI === !0 && c._fSuccess ? c._fSuccess.call(c) : c._bSupportDataURI === !1 && c._fFail && c._fFail.call(c)
|
||||
}
|
||||
if (this._android && this._android <= 2.1) {
|
||||
var b = 1 / window.devicePixelRatio,
|
||||
c = CanvasRenderingContext2D.prototype.drawImage;
|
||||
CanvasRenderingContext2D.prototype.drawImage = function(a, d, e, f, g, h, i, j) {
|
||||
if ("nodeName" in a && /img/i.test(a.nodeName))
|
||||
for (var l = arguments.length - 1; l >= 1; l--) arguments[l] = arguments[l] * b;
|
||||
else "undefined" == typeof j && (arguments[1] *= b, arguments[2] *= b, arguments[3] *= b, arguments[4] *= b);
|
||||
c.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
var e = function(a, b) {
|
||||
this._bIsPainted = !1, this._android = n(), this._htOption = b, this._elCanvas = document.createElement("canvas"), this._elCanvas.width = b.width, this._elCanvas.height = b.height, a.appendChild(this._elCanvas), this._el = a, this._oContext = this._elCanvas.getContext("2d"), this._bIsPainted = !1, this._elImage = document.createElement("img"), this._elImage.style.display = "none", this._el.appendChild(this._elImage), this._bSupportDataURI = null
|
||||
};
|
||||
return e.prototype.draw = function(a) {
|
||||
var b = this._elImage,
|
||||
c = this._oContext,
|
||||
d = this._htOption,
|
||||
e = a.getModuleCount(),
|
||||
f = d.width / e,
|
||||
g = d.height / e,
|
||||
h = Math.round(f),
|
||||
i = Math.round(g);
|
||||
b.style.display = "none", this.clear();
|
||||
for (var j = 0; e > j; j++)
|
||||
for (var k = 0; e > k; k++) {
|
||||
var l = a.isDark(j, k),
|
||||
m = k * f,
|
||||
n = j * g;
|
||||
c.strokeStyle = l ? d.colorDark : d.colorLight, c.lineWidth = 1, c.fillStyle = l ? d.colorDark : d.colorLight, c.fillRect(m, n, f, g), c.strokeRect(Math.floor(m) + .5, Math.floor(n) + .5, h, i), c.strokeRect(Math.ceil(m) - .5, Math.ceil(n) - .5, h, i)
|
||||
}
|
||||
this._bIsPainted = !0
|
||||
}, e.prototype.makeImage = function() {
|
||||
this._bIsPainted && d.call(this, a)
|
||||
}, e.prototype.isPainted = function() {
|
||||
return this._bIsPainted
|
||||
}, e.prototype.clear = function() {
|
||||
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height), this._bIsPainted = !1
|
||||
}, e.prototype.round = function(a) {
|
||||
return a ? Math.floor(1e3 * a) / 1e3 : a
|
||||
}, e
|
||||
}() : function() {
|
||||
var a = function(a, b) {
|
||||
this._el = a, this._htOption = b
|
||||
};
|
||||
return a.prototype.draw = function(a) {
|
||||
for (var b = this._htOption, c = this._el, d = a.getModuleCount(), e = Math.floor(b.width / d), f = Math.floor(b.height / d), g = ['<table style="border:0;border-collapse:collapse;">'], h = 0; d > h; h++) {
|
||||
g.push("<tr>");
|
||||
for (var i = 0; d > i; i++) g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + e + "px;height:" + f + "px;background-color:" + (a.isDark(h, i) ? b.colorDark : b.colorLight) + ';"></td>');
|
||||
g.push("</tr>")
|
||||
}
|
||||
g.push("</table>"), c.innerHTML = g.join("");
|
||||
var j = c.childNodes[0],
|
||||
k = (b.width - j.offsetWidth) / 2,
|
||||
l = (b.height - j.offsetHeight) / 2;
|
||||
k > 0 && l > 0 && (j.style.margin = l + "px " + k + "px")
|
||||
}, a.prototype.clear = function() {
|
||||
this._el.innerHTML = ""
|
||||
}, a
|
||||
}();
|
||||
QRCode = function(a, b) {
|
||||
if (this._htOption = {
|
||||
width: 256,
|
||||
height: 256,
|
||||
typeNumber: 4,
|
||||
colorDark: "#000000",
|
||||
colorLight: "#ffffff",
|
||||
correctLevel: d.H
|
||||
}, "string" == typeof b && (b = {
|
||||
text: b
|
||||
}), b)
|
||||
for (var c in b) this._htOption[c] = b[c];
|
||||
"string" == typeof a && (a = document.getElementById(a)), this._android = n(), this._el = a, this._oQRCode = null, this._oDrawing = new q(this._el, this._htOption), this._htOption.text && this.makeCode(this._htOption.text)
|
||||
}, QRCode.prototype.makeCode = function(a) {
|
||||
this._oQRCode = new b(r(a, this._htOption.correctLevel), this._htOption.correctLevel), this._oQRCode.addData(a), this._oQRCode.make(), this._oDrawing.draw(this._oQRCode), this.makeImage()
|
||||
}, QRCode.prototype.makeImage = function() {
|
||||
"function" == typeof this._oDrawing.makeImage && (!this._android || this._android >= 3) && this._oDrawing.makeImage()
|
||||
}, QRCode.prototype.clear = function() {
|
||||
this._oDrawing.clear()
|
||||
}, QRCode.CorrectLevel = d
|
||||
}();
|
||||
|
||||
var qr = document.getElementById("sqrl-qr");
|
||||
new QRCode(qr, qr.dataset.sqrl);
|
85
sqrl/templates/admin/auth/user/sqrl_manage.html
Normal file
85
sqrl/templates/admin/auth/user/sqrl_manage.html
Normal file
|
@ -0,0 +1,85 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_static %}
|
||||
{% load sqrl %}
|
||||
|
||||
{% block title %}Manage SQRL{% endblock %}
|
||||
{% block content_title %}<h1>Manage SQRL</h1>{% endblock %}
|
||||
|
||||
{% block extrahead %}{{ block.super }}
|
||||
<script src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/sqrl.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block userlinks %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
{% trans 'Manage SQRL' %} /
|
||||
<a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› {% trans 'Manage SQRL' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
|
||||
{% if not user.sqrl_identity %}
|
||||
<p>
|
||||
You dont have SQRL identity associated with your account yet.
|
||||
Please use SQRL link/QR code below to associate SQRL identity with your account.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Congratulations! You already have SQRL identity associated with your account.
|
||||
If you would like to either change or delete existing SQRL identity
|
||||
associated with your account, you can do that by selecting appropriate
|
||||
option in your SQRL client and then using the SQRL link/QR code below.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Note:</strong> For both changing or deleting your SQRL identity,
|
||||
you will need to load your SQRL rescue code.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Caution:</strong> Normally it is not advised to change or delete
|
||||
your SQRL identity. Usually these operations are only required when
|
||||
SQRL identity is compromised.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% sqrl as session_sqrl %}
|
||||
|
||||
<fieldset class="module aligned">
|
||||
<div class="sqrl-wrap">
|
||||
SQRL Login
|
||||
<a href="{{ session_sqrl.sqrl_url }}">
|
||||
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
|
||||
</a>
|
||||
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
|
||||
{# redirect to manage page after successful SQRL transaction #}
|
||||
<input type="hidden" name="next" value="{% url 'admin-sqrl_manage' %}">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<form method="get" action="{{ session_sqrl.sqrl_url }}" class="sqrl">
|
||||
<div class="submit-row">
|
||||
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
|
||||
<input type="submit" value="Manage SQRL" class="default" style="float: left;">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
19
sqrl/templates/admin/base.html
Normal file
19
sqrl/templates/admin/base.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'contrib/admin/templates/admin/base.html' %}
|
||||
{% load i18n admin_static %}
|
||||
|
||||
{% block userlinks %}
|
||||
{% if site_url %}
|
||||
<a href="{{ site_url }}">{% trans 'View site' %}</a> /
|
||||
{% endif %}
|
||||
{% if user.is_active and user.is_staff %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.has_usable_password %}
|
||||
<a href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'admin-sqrl_manage' %}">{% trans 'Manage SQRL' %}</a> /
|
||||
<a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
|
||||
{% endblock %}
|
91
sqrl/templates/admin/login.html
Normal file
91
sqrl/templates/admin/login.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n admin_static %}
|
||||
{% load sqrl %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/login.css' %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/sqrl.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyclass %}{{ block.super }} login{% endblock %}
|
||||
|
||||
{% block nav-global %}{% endblock %}
|
||||
|
||||
{% block content_title %}{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if form.errors and not form.non_field_errors %}
|
||||
<p class="errornote">
|
||||
{% if form.errors.items|length == 1 %}
|
||||
{% trans "Please correct the error below." %}{% else %}
|
||||
{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="errornote">
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div id="content-main">
|
||||
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
|
||||
<div class="form-row">
|
||||
{{ form.username.errors }}
|
||||
<label for="id_username"
|
||||
class="required">{{ form.username.label }}:</label> {{ form.username }}
|
||||
</div>
|
||||
<div class="form-row">
|
||||
{{ form.password.errors }}
|
||||
<label for="id_password"
|
||||
class="required">{% trans 'Password:' %}</label> {{ form.password }}
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
</div>
|
||||
{% url 'admin_password_reset' as password_reset_url %}
|
||||
{% if password_reset_url %}
|
||||
<div class="password-reset-link">
|
||||
<a href="{{ password_reset_url }}">{% trans 'Forgotten your password or username?' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="submit-row">
|
||||
<label> </label><input type="submit" value="{% trans 'Log in' %}"/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% sqrl as session_sqrl %}
|
||||
|
||||
<form method="get" action="{{ session_sqrl.sqrl_url }}" class="sqrl">
|
||||
<p class="align-center or">
|
||||
<span class="line-center">or</span>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h3>Login using SQRL</h3>
|
||||
|
||||
<div class="sqrl-wrap">
|
||||
SQRL Login
|
||||
<a href="{{ session_sqrl.sqrl_url }}">
|
||||
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
|
||||
</a>
|
||||
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
|
||||
</div>
|
||||
|
||||
<div class="submit-row">
|
||||
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
|
||||
<input type="submit" value="{% trans 'Log in using SQRL' %}"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% sqrl_status_url_script_tag session_sqrl %}
|
||||
<script type="text/javascript">
|
||||
document.getElementById('id_username').focus()
|
||||
</script>
|
||||
<script src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
</div>
|
||||
{% endblock %}
|
27
sqrl/templates/sqrl/login.html
Normal file
27
sqrl/templates/sqrl/login.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% extends "sqrl/sqrl_base.html" %}
|
||||
{% load sqrl %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Log in via SQRL{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Log in</h1>
|
||||
|
||||
<p>
|
||||
Please use via SQRL using the information below.
|
||||
</p>
|
||||
|
||||
{% sqrl as session_sqrl %}
|
||||
{% sqrl_login_dropin session_sqrl %}
|
||||
{% endblock %}
|
||||
|
||||
{% comment %}
|
||||
**sqrl/login.html**
|
||||
This is SQRL-only login page.
|
||||
No special variables are passed in the context for the login to work.
|
||||
All necessary data should already be in the context.
|
||||
|
||||
Please note again that this template is for SQRL-exclusive logins.
|
||||
If you would like to add SQRL login to an existing login page,
|
||||
you should rather adjust that template as it is probably way more involved.
|
||||
{% endcomment %}
|
54
sqrl/templates/sqrl/manage.html
Normal file
54
sqrl/templates/sqrl/manage.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
{% extends "sqrl/sqrl_base.html" %}
|
||||
{% load sqrl %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Manage SQRL Identity{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Manage SQRL Identity with your account</h1>
|
||||
|
||||
{% if not user.sqrl_identity %}
|
||||
<p>
|
||||
You dont have SQRL identity associated with your account yet.
|
||||
Please use SQRL link/QR code below to associate SQRL identity with your account.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Congratulations! You already have SQRL identity associated with your account.
|
||||
If you would like to either change or delete existing SQRL identity
|
||||
associated with your account, you can do that by selecting appropriate
|
||||
option in your SQRL client and then using the SQRL link/QR code below.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Note:</strong> For both changing or deleting your SQRL identity,
|
||||
you will need to load your SQRL rescue code.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Caution:</strong> Normally it is not advised to change or delete
|
||||
your SQRL identity. Usually these operations are only required when
|
||||
SQRL identity is compromised.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% sqrl as session_sqrl %}
|
||||
{% sqrl_login_dropin session_sqrl method="manage" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'sqrl/sqrl.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% comment %}
|
||||
**sqrl/manage.html**
|
||||
Used to manage SQRL identity in relation to existing logged in account.
|
||||
When no SQRL identity is already associated with the account,
|
||||
it can be used to associate a new identity.
|
||||
When existing SQRL is already associated, this page can be used
|
||||
to either change or delete existing identity.
|
||||
|
||||
No special variables are passed in the context for the association
|
||||
to work. All necessary data should already be in the context.
|
||||
{% endcomment %}
|
30
sqrl/templates/sqrl/register.html
Normal file
30
sqrl/templates/sqrl/register.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends "sqrl/sqrl_base.html" %}
|
||||
|
||||
{% block title %}Complete User Tegistration{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Complete User Registration</h1>
|
||||
|
||||
<p>
|
||||
Already have account?
|
||||
Login <a href="{% url 'login' %}">here</a> to associate
|
||||
SQRL identity with existing account.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
{{ form.as_p }}
|
||||
<input type="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% comment %}
|
||||
**sqrl/register.html**
|
||||
Used to complete user registration. This template is used when user is trying to login
|
||||
with SQRL however does not already have that SQRL identity associated with any
|
||||
existing account. Since SQRL does not provide any additional information other then
|
||||
SQRL identity itslef, we need to collect some additinal information from the user
|
||||
such as username. All the relevant information is collected via form.
|
||||
|
||||
No special variables are passed in the context.
|
||||
All necessary data should already be in the context.
|
||||
{% endcomment %}
|
41
sqrl/templates/sqrl/sqrl-dropin.html
Normal file
41
sqrl/templates/sqrl/sqrl-dropin.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{% load static %}
|
||||
{% load sqrl %}
|
||||
<style>
|
||||
.sqrl-wrap {
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
border-radius: 5%;
|
||||
}
|
||||
#sqrl-id img {
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
padding-top: 3px;
|
||||
}
|
||||
.sqrl-wrap a {
|
||||
font-size: 50%;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<form method="get" action="{{ session_sqrl.sqrl_url }}">
|
||||
<div class="sqrl-wrap">
|
||||
SQRL Login
|
||||
<a href="{{ session_sqrl.sqrl_url }}">
|
||||
<div id="sqrl-qr" data-sqrl="{{ session_sqrl.sqrl_url }}"></div>
|
||||
</a>
|
||||
<a href="https://www.grc.com/sqrl/sqrl.htm">What is SQRL?</a>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="nut" value="{{ session_sqrl.nut.nonce }}">
|
||||
{% if session_sqrl.method == "manage" %}
|
||||
{# redirect to manage page after successful SQRL transaction #}
|
||||
<input type="hidden" name="next" value="{% url 'sqrl:manage' %}">
|
||||
<input type="submit" value="Manage SQRL">
|
||||
{% else %}
|
||||
<input type="submit" value="Log in using SQRL">
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% sqrl_status_url_script_tag session_sqrl %}
|
||||
<script src="{% static sqrl/sqrl.js %}"></script>
|
1
sqrl/templates/sqrl/sqrl_base.html
Normal file
1
sqrl/templates/sqrl/sqrl_base.html
Normal file
|
@ -0,0 +1 @@
|
|||
{% extends "base.html" %}
|
44
sqrl/templatetags/sqrl.py
Normal file
44
sqrl/templatetags/sqrl.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.template.defaultfilters import urlencode
|
||||
|
||||
from ..sqrl import SQRLInitialization
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.assignment_tag(takes_context=True)
|
||||
def sqrl(context):
|
||||
return SQRLInitialization(context['request'])
|
||||
|
||||
|
||||
@register.inclusion_tag('sqrl/sqrl-dropin.html')
|
||||
def sqrl_login_dropin(session_sqrl, method="login"):
|
||||
"""
|
||||
Creates a drop-in SQRL element in your template pages.
|
||||
Add it to your login template to make it SQRL-aware.
|
||||
|
||||
Usage:
|
||||
{% load sqrl %}
|
||||
{% sqrl as session_sqrl %}
|
||||
{% sqrl_login_dropin session_sqrl [method=METHOD] %}
|
||||
|
||||
METHOD is an optional argument that changes the way the form
|
||||
behaves. Possible arguments are:
|
||||
- login: The default method. No special redirections occur
|
||||
- manage: Will redirect the user to sqrl/manage
|
||||
|
||||
Notes:
|
||||
The drop-in is defaulted to a max-width of 300px. Set the width
|
||||
property of the parent if you want or need it smaller. You will
|
||||
likely want to change the font-size as well in this case.
|
||||
"""
|
||||
return {'session_sqrl':session_sqrl, 'method': method}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def sqrl_status_url_script_tag(sqrl):
|
||||
url = reverse('sqrl:status', kwargs={'transaction': sqrl.nut.transaction_nonce})
|
||||
return '<script>SQRL_CHECK_URL="{url}"</script>'.format(url=url)
|
18
sqrl/urls.py
18
sqrl/urls.py
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.urls import path, re_path
|
||||
|
||||
from .views import (
|
||||
SQRLAuthView,
|
||||
SQRLCompleteRegistrationView,
|
||||
SQRLIdentityManagementView,
|
||||
SQRLLoginView,
|
||||
SQRLStatusView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("auth/", SQRLAuthView.as_view(), name="auth"),
|
||||
path("login/", SQRLLoginView.as_view(), name="login"),
|
||||
path("manage/", SQRLIdentityManagementView.as_view(), name='manage')
|
||||
path("register/",SQRLCompleteRegistrationView.as_view(), name='complete-registration'),
|
||||
re_path(r"^status/(?P<transaction>[A-Za-z0-9_-]{43})/$", SQRLStatusView.as_view(), name='status'),
|
||||
]
|
|
@ -2,8 +2,7 @@
|
|||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
class Base64(object):
|
||||
"""
|
||||
|
@ -14,26 +13,24 @@ class Base64(object):
|
|||
def encode(cls, s):
|
||||
"""
|
||||
Encode binary string as base64. Remaining "=" characters are removed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s: bytes
|
||||
Bytes string to be encoded as base64
|
||||
"""
|
||||
assert isinstance(s, (six.binary_type, bytearray))
|
||||
assert isinstance(s, (bites, bytearray))
|
||||
return urlsafe_b64encode(s).decode('ascii').rstrip('=')
|
||||
|
||||
@classmethod
|
||||
def decode(cls, s):
|
||||
"""
|
||||
Decode unicode string from base64 where remaining "=" characters were stripped.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s: str
|
||||
Unicode string to be decoded from base64
|
||||
"""
|
||||
assert isinstance(s, six.text_type)
|
||||
assert isinstance(s, str)
|
||||
return urlsafe_b64decode((s + '=' * (4 - len(s) % 4)).encode('ascii'))
|
||||
|
||||
|
||||
|
@ -46,11 +43,9 @@ class Encoder(object):
|
|||
def base64_dumps(cls, data):
|
||||
"""
|
||||
Dumps given data into a single Base64 string.
|
||||
|
||||
Practically this is the same as :meth:`dumps` except :meth:`dumps`
|
||||
can return multiline string for ``dict``. This method normalizes that
|
||||
further by converting that multiline string to a single base64 encoded value.
|
||||
|
||||
Returns
|
||||
-------
|
||||
binary
|
||||
|
@ -64,11 +59,8 @@ class Encoder(object):
|
|||
def dumps(cls, data):
|
||||
"""
|
||||
Recursively dumps given data to SQRL response format.
|
||||
|
||||
Before data is dumped out, it is normalized by using :meth:`.normalize`.
|
||||
|
||||
This dumps each data type as follows:
|
||||
|
||||
:``dict``: returns an ``\\r\\n`` multiline string. Each line is for a single key-pair
|
||||
of format ``<key>=<dumped value>``.
|
||||
:``list``: tilde (``~``) joined dumped list of values
|
||||
|
@ -90,9 +82,7 @@ class Encoder(object):
|
|||
def normalize(cls, data):
|
||||
"""
|
||||
Recursively normalize data for encoding.
|
||||
|
||||
This encodes each data type as follows:
|
||||
|
||||
:``dict``: returns an ``OrderedDict`` where all values are recursively normalized.
|
||||
Empty dict is normalized to empty string
|
||||
:``list``: each value is recursively normalized
|
||||
|
@ -110,12 +100,12 @@ class Encoder(object):
|
|||
return ''
|
||||
elif isinstance(data, (list, tuple)):
|
||||
return [cls.dumps(i) for i in data]
|
||||
elif isinstance(data, six.binary_type):
|
||||
elif isinstance(data, bytes):
|
||||
return Base64.encode(data)
|
||||
elif isinstance(data, six.text_type):
|
||||
elif isinstance(data, str):
|
||||
return data
|
||||
else:
|
||||
return six.text_type(data)
|
||||
return str(data)
|
||||
|
||||
def get_user_ip(request):
|
||||
"""
|
||||
|
|
681
sqrl/views.py
681
sqrl/views.py
|
@ -1,3 +1,680 @@
|
|||
from django.shortcuts import render
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from pprint import pformat
|
||||
|
||||
# Create your views here.
|
||||
from braces.views._access import LoginRequiredMixin
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY,
|
||||
HASH_SESSION_KEY,
|
||||
SESSION_KEY,
|
||||
login,
|
||||
)
|
||||
from django.core import serializers
|
||||
from django.url import reverse
|
||||
from django.http import Http404, HttpResponse, JsonResponse, QueryDict
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import FormView, TemplateView, View
|
||||
|
||||
from .backends import SQRL_MODEL_BACKEND
|
||||
from .exceptions import TIF, TIFException
|
||||
from .forms import (
|
||||
AuthQueryDictForm,
|
||||
ExtractedNextUrlForm,
|
||||
GenerateQRForm,
|
||||
NextUrlForm,
|
||||
PasswordLessUserCreationForm,
|
||||
RequestForm,
|
||||
)
|
||||
from .models import SQRLIdentity, SQRLNut
|
||||
from .response import SQRLHttpResponse
|
||||
from .sqrl import SQRLInitialization
|
||||
from .utils import Base64, QRGenerator, get_user_ip
|
||||
|
||||
|
||||
SQRL_IDENTITY_SESSION_KEY = '_sqrl_identity'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SQRLLoginView(TemplateView):
|
||||
"""
|
||||
Simple ``TemplateView`` which renders ``sqrl/login.html`` template.
|
||||
|
||||
The template can (and probably should) be customized in each Django Project.
|
||||
|
||||
.. note::
|
||||
This templates only provides SQRL auth method. If other methods are required
|
||||
on the same login page, it is probably better to add SQRL auth method to
|
||||
existing login page vs customizing this template/view.
|
||||
"""
|
||||
template_name = 'sqrl/login.html'
|
||||
|
||||
|
||||
class SQRLStatusView(View):
|
||||
"""
|
||||
Ajax view which returns the status of the SQRL transaction back to the user.
|
||||
|
||||
The state of the transaction is looked up by finding the appropriate
|
||||
:obj:`.models.SQRLNut` by the transaction ID which is a kwarg in the url pattern.
|
||||
When the nut is found, :attr:`.models.SQRLNut.is_transaction_complete` is
|
||||
used to determine the state of the transaction.
|
||||
|
||||
This view is useful because when it returns a redirect upon successful completing
|
||||
of SQRL transaction, js can dynamically redirect the user to that page.
|
||||
Without this behaviour, user will have to manually refresh the page
|
||||
which is inconvenient.
|
||||
|
||||
.. note::
|
||||
Currently this view is being used via polling on js side
|
||||
however this view's concept can easily be adopted to any other
|
||||
real-time technology such as websockets.
|
||||
"""
|
||||
success_url = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
Get the url to which the user will be redirect to after
|
||||
successful SQRL transaction.
|
||||
|
||||
The url is computed using :obj:`.forms.ExtractedNextUrlForm`.
|
||||
Following URLs are used depending if the form is valid:
|
||||
|
||||
:``True``: Next url within the ``?url=`` querystring parameter
|
||||
:``False``: ``settings.LOGIN_REDIRECT_URL``
|
||||
|
||||
When however the user is not logged in, even after successful
|
||||
SQRL transaction and has pending registration, user will be
|
||||
redirected to ``sqrl-complete-registration`` view with the
|
||||
``?next=`` querystring parameter set to the url computed above.
|
||||
"""
|
||||
next_form = ExtractedNextUrlForm(self.request.GET)
|
||||
|
||||
if next_form.is_valid():
|
||||
url = next_form.cleaned_data['url']
|
||||
else:
|
||||
url = self.success_url
|
||||
|
||||
if all([not self.request.user.is_authenticated(),
|
||||
SQRL_IDENTITY_SESSION_KEY in self.request.session]):
|
||||
return reverse('sqrl:complete-registration') + '?next={}'.format(url)
|
||||
else:
|
||||
return url
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
Get the :obj:`.models.SQRLNut` by transaction is or raise ``404``.
|
||||
"""
|
||||
return get_object_or_404(SQRLNut, transaction_nonce=self.kwargs['transaction'])
|
||||
|
||||
def post(self, request, transaction, *args, **kwargs):
|
||||
"""
|
||||
Handle the request and return appropriate data back to the user.
|
||||
|
||||
Following keys can be returned:
|
||||
|
||||
:``transaction_complete``: boolean which is always returned
|
||||
:``redirect_to``: also present when ``transaction_complete == True``
|
||||
and this is where the js should redirect the user to.
|
||||
|
||||
.. note::
|
||||
This view is restricted to ajax calls as to restrict its
|
||||
use from regular forms.
|
||||
"""
|
||||
if not request.is_ajax():
|
||||
return HttpResponse(status=405) # method not allowed
|
||||
|
||||
transaction = self.get_object()
|
||||
|
||||
data = {
|
||||
'transaction_complete': False,
|
||||
}
|
||||
|
||||
if transaction.is_transaction_complete:
|
||||
data.update({
|
||||
'transaction_complete': True,
|
||||
'redirect_to': self.get_success_url(),
|
||||
})
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class SQRLAuthView(View):
|
||||
"""
|
||||
This is the main view responsible for all interactions with SQRL client.
|
||||
|
||||
The responsibilities of this view are:
|
||||
|
||||
* validate that URL is correct since nut value is part of querystring
|
||||
which cannot be matched in url patterns.
|
||||
When invalid, 404 should be returned.
|
||||
* find the nut via nut nonce or return transient error TIF
|
||||
* validate client payload by using :obj:`.forms.RequestForm` which
|
||||
includes validating validity of signatures and looking up stored
|
||||
SQRL identities.
|
||||
* executing all SQRL commands such as ``query``, ``ident``, etc
|
||||
as instructed in the SQRL payload.
|
||||
If any of the commands are not supported not supported TIF
|
||||
is returned.
|
||||
* finalize the any remaining state such as saving identity objects
|
||||
if all commands successfully completed
|
||||
* returning response back to the user
|
||||
"""
|
||||
http_method_names = ['post']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SQRLAuthView, self).__init__(*args, **kwargs)
|
||||
|
||||
self.tif = TIF(0)
|
||||
|
||||
self.nut_value = None
|
||||
self.nut = None
|
||||
self.client = None
|
||||
self.identity = None
|
||||
self.previous_identity = None
|
||||
self.session = None
|
||||
self.is_disabled = False
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Standard ``dispatch`` with custom exception handling
|
||||
for :obj:`.exceptions.TIFException` in which error response is returned
|
||||
with TIF code as specified in the exception.
|
||||
"""
|
||||
try:
|
||||
return super(SQRLAuthView, self).dispatch(request, *args, **kwargs)
|
||||
except TIFException as e:
|
||||
self.tif = self.tif.update(e.tif)
|
||||
return self.render_to_response()
|
||||
|
||||
def get_server_data(self):
|
||||
"""
|
||||
Get data to be returned back to SQRL client.
|
||||
|
||||
This method does not encode for the response. It simply returns
|
||||
a dictionary of information which later on can be used by
|
||||
:obj:`.response.SQRLHttpResponse` to actually construct
|
||||
data to be sent back to the client.
|
||||
|
||||
Returns
|
||||
-------
|
||||
OrderedDict
|
||||
Dict of data to be sent back to SQRL client.
|
||||
The ``Ordered`` part is important as SQRL requires
|
||||
to send some data first such as SQRL version number.
|
||||
"""
|
||||
if self.nut:
|
||||
self.nut.renew()
|
||||
nut = self.nut.nonce
|
||||
qry = SQRLInitialization(self.request, self.nut).url
|
||||
else:
|
||||
nut = self.nut_value
|
||||
qry = self.request.get_full_path()
|
||||
|
||||
data = OrderedDict((
|
||||
('ver', 1),
|
||||
('nut', nut),
|
||||
('tif', self.tif.as_hex_string()),
|
||||
('qry', qry),
|
||||
('sfn', getattr(settings, 'SQRL_SERVER_FRIENDLY_NAME',
|
||||
self.request.get_host().split(':')[0])[:64]),
|
||||
))
|
||||
|
||||
if self.identity:
|
||||
data['suk'] = self.identity.server_unlock_key
|
||||
|
||||
return data
|
||||
|
||||
def render_to_response(self):
|
||||
"""
|
||||
Render a response which will be send to SQRL client.
|
||||
|
||||
Internally this method uses :meth:`.get_server_data` to construct the response
|
||||
data and :obj:`.response.SQRLHttpResponse` to render that data into
|
||||
SQRL-compatible format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
SQRLHttpResponse
|
||||
Completely rendered response ready to the sent to the SQRL client
|
||||
"""
|
||||
return SQRLHttpResponse(self.nut, self.get_server_data())
|
||||
|
||||
def do_ips_match(self):
|
||||
"""
|
||||
This method updates internal TIF state with :attr:`.exceptions.TIF.IP_MATCH` bit.
|
||||
|
||||
The bit is only set when the IP address of the SQRL client making request
|
||||
to this view matches IP address of device used to initiate SQRL transation
|
||||
(where SQRL link/QR code were generated).
|
||||
"""
|
||||
if get_user_ip(self.request) == self.nut.ip_address:
|
||||
self.tif = self.tif.update(TIF.IP_MATCH)
|
||||
|
||||
def do_ids_match(self):
|
||||
"""
|
||||
This method updates internal TIF state with :attr:`.exceptions.TIF.ID_MATCH`
|
||||
and :attr:`.exceptions.TIF.PREVIOUS_ID_MATCH` bits.
|
||||
|
||||
The appropriate bits are only set when the the corresponding identity is found
|
||||
on the server.
|
||||
"""
|
||||
if self.identity:
|
||||
self.tif = self.tif.update(TIF.ID_MATCH)
|
||||
|
||||
if self.previous_identity:
|
||||
self.tif = self.tif.update(TIF.PREVIOUS_ID_MATCH)
|
||||
|
||||
def is_sqrl_disabled(self):
|
||||
"""
|
||||
This method updates internal TIF state with :attr:`.exceptions.TIF.SQRL_DISABLED` bit.
|
||||
|
||||
The bit is only set when either current or previous identity are found
|
||||
and they have :attr:`.models.SQRLIdentity.is_enabled` set as ``False``
|
||||
which means previously SQRL client requested server to disable SQRL
|
||||
auth method for that user.
|
||||
|
||||
Also this method sets ``self.is_disabled`` attribute which later on can be
|
||||
used by other methods to customize their behaviour.
|
||||
"""
|
||||
self.is_disabled = False
|
||||
|
||||
if self.identity and not self.identity.is_enabled:
|
||||
self.tif = self.tif.update(TIF.SQRL_DISABLED)
|
||||
self.is_disabled = True
|
||||
|
||||
if self.previous_identity and not self.previous_identity.is_enabled:
|
||||
self.tif = self.tif.update(TIF.SQRL_DISABLED)
|
||||
self.is_disabled = True
|
||||
|
||||
def get_nut_or_error(self):
|
||||
"""
|
||||
This method finds the :obj:`.models.SQRLNut` by nut nonce in the
|
||||
querystring or if not not raises appropriate :obj:`.exceptions.TIFException`.
|
||||
|
||||
When nut is found, it is saved as ``self.nut``. In addition, this method
|
||||
triggers :meth:`.do_ips_match` to update internal TIF state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
SQRLNut
|
||||
Found :obj:`.models.SQRLNut` via nut nonce from querystring
|
||||
|
||||
Raises
|
||||
------
|
||||
TIFException
|
||||
:obj:`.exceptions.TIFException` with :attr:`.exceptions.TIF.TRANSIENT_FAILURE`
|
||||
and :attr:`.exceptions.TIF.COMMAND_FAILED` bits sets as ``True``
|
||||
when nut is not found.
|
||||
"""
|
||||
self.nut = (SQRLNut.objects
|
||||
.filter(nonce=self.nut_value,
|
||||
is_transaction_complete=False)
|
||||
.first())
|
||||
|
||||
if not self.nut:
|
||||
log.debug('Nut not found')
|
||||
raise TIFException(TIF.TRANSIENT_FAILURE | TIF.COMMAND_FAILED)
|
||||
|
||||
self.do_ips_match()
|
||||
|
||||
return self.nut
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Main view handler since all SQRL requests are ``POST`` requests.
|
||||
|
||||
This method does not implement a lot of logic. It mostly relies on other
|
||||
methods which it then orchestrates. For information on what responsibilities
|
||||
which method has, you can take a look at :obj:`.SQRLAuthView` description.
|
||||
|
||||
Some implementation details worth mentioning:
|
||||
|
||||
* This method uses multiple forms to validate different sections of the payload.
|
||||
Specifically it uses :obj:`.forms.AuthQueryDictForm` to validate the presence
|
||||
of ``?nut=`` within querystring; and :obj:`.forms.RequestForm` to validate
|
||||
the SQRL payload itself.
|
||||
* This method extensively uses :obj:`.exceptions.TIFException` to immediately
|
||||
return some sort of error to the user which is handled by :meth:`.dispatch`.
|
||||
The only exception to that is that it still raises ``Http404`` when nut pattern
|
||||
is not validated. Normally in Django that would of been validated in url patterns
|
||||
however since SQRL forces to use ``?nut=`` querystring, we mimic same behaviour
|
||||
404 Not Found in the view.
|
||||
* To atomically process all SQRL commands (all or nothing), this view
|
||||
implements all SQRL commands as dedicated methods (e.g. :meth:`.query`).
|
||||
That allows this method to find all appropriate handlers for all the commands
|
||||
and if not all are found, :obj:`.exceptions.TIFException` can be raised
|
||||
with :attr:`.exceptions.TIF.NOT_SUPPORTED` bit set as ``True``.
|
||||
If all are found, then it simply processes them in the order they were requested.
|
||||
* Since any SQRL command can potentially fail, none of the SQRL command handlers
|
||||
save any state in either the session or models because other commands can fail
|
||||
after them. If all succeed, this method then explicitly finalizes/saves all the
|
||||
state which includes session and models.
|
||||
"""
|
||||
log.debug('-' * 50)
|
||||
log.debug('Raw request body:\n{}'.format(request.body))
|
||||
|
||||
# in case content-type is not given in which case
|
||||
# request.POST will be empty in which case manually parse
|
||||
# raw request body
|
||||
if not request.POST:
|
||||
request.POST = QueryDict(request.body)
|
||||
|
||||
# nut is not part of URL regex so validate it here
|
||||
# using a form and if not valid, return 404,
|
||||
# same as if nut would of been validated in URL regex
|
||||
self.query_form = AuthQueryDictForm(request.GET)
|
||||
if not self.query_form.is_valid():
|
||||
log.debug('Query form failed with {}'
|
||||
''.format(repr(self.query_form.errors)))
|
||||
raise Http404
|
||||
|
||||
log.debug('Request payload:\n{}'
|
||||
''.format(pformat(request.POST)))
|
||||
|
||||
self.nut_value = self.query_form.cleaned_data['nut']
|
||||
self.nut = self.get_nut_or_error()
|
||||
|
||||
# validate the client data
|
||||
# this also validates submitted signatures and verifies
|
||||
# that echoed back server response was not altered
|
||||
self.payload_form = RequestForm(self.nut, request.POST)
|
||||
if not self.payload_form.is_valid():
|
||||
log.debug('Request payload validation failed with {}'
|
||||
''.format(repr(self.payload_form.errors)))
|
||||
if self.payload_form.tif:
|
||||
raise TIFException(TIF.COMMAND_FAILED | self.payload_form.tif)
|
||||
raise TIFException(TIF.COMMAND_FAILED | TIF.CLIENT_FAILURE)
|
||||
|
||||
log.debug('Request payload successfully parsed and validated:\n{}'
|
||||
''.format(pformat(self.payload_form.cleaned_data)))
|
||||
|
||||
self.client = self.payload_form.cleaned_data['client']
|
||||
|
||||
self.identity = self.payload_form.identity
|
||||
self.previous_identity = self.payload_form.previous_identity
|
||||
self.session = self.payload_form.session
|
||||
self.do_ids_match()
|
||||
self.is_sqrl_disabled()
|
||||
|
||||
cmds = [getattr(self, i, None) for i in self.client['cmd']]
|
||||
|
||||
if not all(cmds):
|
||||
raise TIFException(TIF.COMMAND_FAILED | TIF.NOT_SUPPORTED)
|
||||
|
||||
for cmd in cmds:
|
||||
cmd()
|
||||
|
||||
self.finalize()
|
||||
|
||||
return self.render_to_response()
|
||||
|
||||
def query(self):
|
||||
"""
|
||||
Handler for SQRL ``query`` command.
|
||||
|
||||
Since all necessary information by default is already returned to the user,
|
||||
this method does not have to do anything.
|
||||
"""
|
||||
|
||||
def ident(self):
|
||||
"""
|
||||
Handler for SQRL ``ident`` command.
|
||||
"""
|
||||
if self.is_disabled:
|
||||
return
|
||||
|
||||
self.create_or_update_identity()
|
||||
|
||||
# user is already logged in
|
||||
# so simply associate identity with the user
|
||||
if all((self.session.get(i) for i in
|
||||
[SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY])):
|
||||
self.identity.user_id = self.session.get(SESSION_KEY)
|
||||
|
||||
# user is already associated with identity
|
||||
# so we can login the user
|
||||
elif self.identity.user_id:
|
||||
user = self.identity.user
|
||||
user.backend = SQRL_MODEL_BACKEND
|
||||
|
||||
session_auth_hash = user.get_session_auth_hash()
|
||||
|
||||
self.session[SESSION_KEY] = user.pk
|
||||
self.session[BACKEND_SESSION_KEY] = user.backend
|
||||
self.session[HASH_SESSION_KEY] = session_auth_hash
|
||||
|
||||
log.info('Successfully authenticated user "{}" via SQRL'.format(user.username))
|
||||
|
||||
# user was not found so lets save identity information in session
|
||||
# so that we can complete user registration
|
||||
else:
|
||||
serialized = serializers.serialize('json', [self.identity])
|
||||
self.session[SQRL_IDENTITY_SESSION_KEY] = serialized
|
||||
log.debug('Storing sqrl identity in session "{}" to complete registration:\n{}'
|
||||
''.format(self.session.session_key,
|
||||
pformat(json.loads(serialized)[0]['fields'])))
|
||||
|
||||
self.nut.is_transaction_complete = True
|
||||
|
||||
def disable(self):
|
||||
"""
|
||||
Handler for SQRL ``disable`` command.
|
||||
|
||||
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
|
||||
which guarantees that in order to use ``disable``, user must already have associated
|
||||
:obj:`.models.SQRLIdentity` so this method simply sets :attr:`.models.SQRLIdentity.is_enabled`
|
||||
to ``False``. Then if the rest of the SQRL commands succeed, :meth:`.finalize` will
|
||||
save that change.
|
||||
"""
|
||||
self.create_or_update_identity()
|
||||
self.identity.is_enabled = False
|
||||
|
||||
self.nut.is_transaction_complete = True
|
||||
|
||||
def enable(self):
|
||||
"""
|
||||
Handler for SQRL ``enable`` command.
|
||||
|
||||
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
|
||||
which guarantees that in order to use ``enable``, user must already have associated
|
||||
:obj:`.models.SQRLIdentity` and that the user correctly supplied ``urs`` signature.
|
||||
Therefore this method simply sets :attr:`.models.SQRLIdentity.is_enabled` to ``True``.
|
||||
Then if the rest of the SQRL commands succeed, :meth:`.finalize` will save that change.
|
||||
"""
|
||||
self.create_or_update_identity()
|
||||
self.identity.is_enabled = True
|
||||
|
||||
self.nut.is_transaction_complete = True
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Handler for SQRL ``remove`` command.
|
||||
|
||||
By the time this handler is called, :obj:`.forms.RequestForm` is already validated
|
||||
which guarantees that in order to use ``remove``, user must already have associated
|
||||
:obj:`.models.SQRLIdentity`, that the user correctly supplied ``urs`` signature
|
||||
and that that ``remove`` is the only command.
|
||||
Since all finalizing of the state should be handled by :meth:`.finalize`, this method
|
||||
does not actually delete the identity model but marks it for deletion.
|
||||
"""
|
||||
self.identity.to_remove = True
|
||||
|
||||
self.nut.is_transaction_complete = True
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
State finalization method.
|
||||
|
||||
This is necessary since SQRL can request multiple commands at the same time
|
||||
and any of them can fail. Therefore no state should be saved in any of the
|
||||
command handlers. They should adjust the state but not actually save it.
|
||||
Instead this method saves all the state. This allows the SQRL request
|
||||
processing to be atomic. Current it saves:
|
||||
|
||||
* :obj:`.models.SQRLIdentity`
|
||||
* session data
|
||||
"""
|
||||
if self.identity:
|
||||
if getattr(self.identity, 'to_remove', False):
|
||||
self.identity.delete()
|
||||
elif self.identity.user_id:
|
||||
self.identity.save()
|
||||
if self.session:
|
||||
self.session.save()
|
||||
|
||||
def create_or_update_identity(self):
|
||||
"""
|
||||
This method updates existing :obj:`.models.SQRLIdentity` or creates it
|
||||
when not already present.
|
||||
|
||||
This is used to handle:
|
||||
|
||||
* new users creating their :obj:`.models.SQRLIdentity` in which case
|
||||
all of the data will be set from scratch such as
|
||||
:attr:`.models.SQRLIdentity.public_key`, etc
|
||||
* existing users since they could be sending specific SQRL options
|
||||
(e.g. ``sqrlonly``) which should always update :obj:`.models.SQRLIdentity`
|
||||
depending on the presence of the options in the request.
|
||||
"""
|
||||
if not self.identity:
|
||||
self.identity = SQRLIdentity()
|
||||
|
||||
# by this point form has validated that if the identity is being switched
|
||||
# all necessary signatures were provided and validated
|
||||
# so we can safely set the public key which will either
|
||||
# 1) set new public key for new identity associations
|
||||
# 2) overwrite the existing public key with the same public key
|
||||
# 3) overwrite the existing public key which by this point
|
||||
# is previous identity with new current identity
|
||||
self.identity.public_key = Base64.encode(self.client['idk'])
|
||||
self.identity.is_only_sqrl = 'sqrlonly' in self.client['opt']
|
||||
|
||||
# form validation will make sure that these are supplied when
|
||||
# necessary (e.g. creating new identity)
|
||||
# the reason we don't simply want to always overwrite these is
|
||||
# because for already associated identities, client will not supply
|
||||
# them so we dont want to overwrite the model with empty values
|
||||
if self.client.get('vuk'):
|
||||
self.identity.verify_unlock_key = Base64.encode(self.client['vuk'])
|
||||
if self.client.get('suk'):
|
||||
self.identity.server_unlock_key = Base64.encode(self.client['suk'])
|
||||
|
||||
return self.identity
|
||||
|
||||
|
||||
class SQRLCompleteRegistrationView(FormView):
|
||||
"""
|
||||
This view is used to complete user registration.
|
||||
|
||||
That happens when SQRL transaction is successfully completed
|
||||
however does not have account yet setup. In that case :obj:`.SQRLAuthView`
|
||||
stores SQRL identity information in the session which this view can use.
|
||||
To complete registration, a form is displayed to the user.
|
||||
When form is successfully filled out, this view creates a new user and
|
||||
automatically assigns the stored SQRL identity from the session to the
|
||||
new user.
|
||||
"""
|
||||
form_class = PasswordLessUserCreationForm
|
||||
template_name = 'sqrl/register.html'
|
||||
success_url = settings.LOGIN_REDIRECT_URL
|
||||
|
||||
def check_session_for_sqrl_identity_or_404(self):
|
||||
"""
|
||||
Check if the SQRL identity is stored within the session
|
||||
and if not, raise ``Http404``.
|
||||
|
||||
Raises
|
||||
------
|
||||
Http404
|
||||
When SQRL identity is not stored in session
|
||||
"""
|
||||
if SQRL_IDENTITY_SESSION_KEY not in self.request.session:
|
||||
raise Http404
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Same as regular ``FormView`` except this also checks for identity within session
|
||||
by using :meth:`.check_session_for_sqrl_identity_or_404`.
|
||||
"""
|
||||
self.check_session_for_sqrl_identity_or_404()
|
||||
return super(SQRLCompleteRegistrationView, self).get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Same as regular ``FormView`` except this also checks for identity within session
|
||||
by using :meth:`.check_session_for_sqrl_identity_or_404`.
|
||||
"""
|
||||
self.check_session_for_sqrl_identity_or_404()
|
||||
return super(SQRLCompleteRegistrationView, self).post(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
Get success url to which user will be redirected to when registration is complete.
|
||||
|
||||
The url from the ``?next=`` is used if :obj:`.forms.NextUrlForm` is valid.
|
||||
Otherwise :attr:`.success_url` is used.
|
||||
"""
|
||||
next_form = NextUrlForm(self.request.GET)
|
||||
if next_form.is_valid():
|
||||
return next_form.cleaned_data['next']
|
||||
return self.success_url
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
When registration form is valid, this method finishes up
|
||||
creating new user with new SQRL identity.
|
||||
|
||||
It does the following:
|
||||
|
||||
#. decodes the stored SQRL identity in the session.
|
||||
If this step fails, this method returns ``500`` response.
|
||||
#. saves the new user and assigned the decoded identity to it
|
||||
#. logs in the new user
|
||||
#. redirects to url returned by :meth:`.get_success_url`
|
||||
"""
|
||||
try:
|
||||
identity = next(iter(serializers.deserialize(
|
||||
'json', self.request.session.pop(SQRL_IDENTITY_SESSION_KEY)
|
||||
))).object
|
||||
except Exception:
|
||||
return HttpResponse(status=500)
|
||||
|
||||
user = form.save()
|
||||
user.backend = SQRL_MODEL_BACKEND
|
||||
|
||||
identity.user = user
|
||||
identity.save()
|
||||
|
||||
login(self.request, user)
|
||||
|
||||
log.info('Successfully registered and authenticated user '
|
||||
'"{}" via SQRL'.format(user.username))
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class SQRLIdentityManagementView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Simple ``TemplateView`` which renders ``sqrl/manage.html`` template.
|
||||
|
||||
The template can (and probably should) be customized in each Django Project.
|
||||
|
||||
.. warning::
|
||||
Since this view is to exclusively manage SQRL identity,
|
||||
no other auth methods should be added to this template/view.
|
||||
"""
|
||||
template_name = 'sqrl/manage.html'
|
||||
|
||||
|
||||
class AdminSiteSQRLIdentityManagementView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'admin/auth/user/sqrl_manage.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AdminSiteSQRLIdentityManagementView, self).get_context_data(**kwargs)
|
||||
context.update({
|
||||
'has_permission': True,
|
||||
})
|
||||
return context
|
||||
|
|
Loading…
Reference in a new issue