326 lines
11 KiB
Python
326 lines
11 KiB
Python
import binascii
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from io import BytesIO
|
|
|
|
from django.core.validators import RegexValidator
|
|
from django.db import models
|
|
from django.http import HttpResponse
|
|
from django.shortcuts import get_object_or_404
|
|
from django.template.response import TemplateResponse
|
|
from django.urls import reverse
|
|
from django.utils.dateparse import parse_datetime
|
|
from django.utils.translation import gettext_lazy as _
|
|
from nacl.signing import SigningKey
|
|
from qrcode import ERROR_CORRECT_Q, QRCode
|
|
from qrcode.image.pil import PilImage
|
|
|
|
from passerelle.base.models import BaseResource
|
|
from passerelle.utils.api import endpoint
|
|
|
|
CERTIFICATE_SCHEMA = {
|
|
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
'type': 'object',
|
|
'unflatten': True,
|
|
'additionalProperties': False,
|
|
'properties': {
|
|
'data': {
|
|
'type': 'object',
|
|
'title': _('Data to encode in the certificate'),
|
|
'additionalProperties': {'type': 'string'},
|
|
},
|
|
'validity_start': {'type': 'string', 'format': 'date-time'},
|
|
'validity_end': {'type': 'string', 'format': 'date-time'},
|
|
},
|
|
}
|
|
|
|
READER_SCHEMA = {
|
|
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
'type': 'object',
|
|
'additionalProperties': False,
|
|
'properties': {
|
|
'validity_start': {'type': 'string', 'format': 'date-time'},
|
|
'validity_end': {'type': 'string', 'format': 'date-time'},
|
|
},
|
|
}
|
|
|
|
|
|
def generate_key():
|
|
key = os.urandom(32)
|
|
return ''.join(format(x, '02x') for x in key)
|
|
|
|
|
|
UUID_PATTERN = '(?P<uuid>[0-9|a-f]{8}-[0-9|a-f]{4}-[0-9|a-f]{4}-[0-9|a-f]{4}-[0-9a-f]{12})'
|
|
|
|
|
|
class QRCodeConnector(BaseResource):
|
|
category = _('Misc')
|
|
|
|
key = models.CharField(
|
|
_('Private Key'),
|
|
max_length=64,
|
|
default=generate_key,
|
|
validators=[RegexValidator(r'[a-z|0-9]{64}', 'Key should be a 32 bytes hexadecimal string')],
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = _('QR Code')
|
|
|
|
@property
|
|
def signing_key(self):
|
|
binary_key = binascii.unhexlify(self.key)
|
|
return SigningKey(seed=binary_key)
|
|
|
|
@property
|
|
def hex_verify_key(self):
|
|
verify_key = self.signing_key.verify_key.encode()
|
|
return binascii.hexlify(verify_key).decode('utf-8')
|
|
|
|
@endpoint(
|
|
name='save-certificate',
|
|
pattern=f'^{UUID_PATTERN}?$',
|
|
example_pattern='{uuid}',
|
|
description=_('Create or update a certificate'),
|
|
post={'request_body': {'schema': {'application/json': CERTIFICATE_SCHEMA}}},
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('Certificate identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def save_certificate(self, request, uuid=None, post_data=None):
|
|
validity_start = parse_datetime(post_data['validity_start'])
|
|
validity_end = parse_datetime(post_data['validity_end'])
|
|
data = post_data['data']
|
|
|
|
if not uuid:
|
|
certificate = self.certificates.create(
|
|
data=data,
|
|
validity_start=validity_start,
|
|
validity_end=validity_end,
|
|
)
|
|
else:
|
|
certificate = get_object_or_404(self.certificates, uuid=uuid)
|
|
certificate.validity_start = validity_start
|
|
certificate.validity_end = validity_end
|
|
certificate.data = data
|
|
certificate.save()
|
|
|
|
return {
|
|
'data': {
|
|
'uuid': certificate.uuid,
|
|
'qrcode_url': certificate.get_qrcode_url(request),
|
|
}
|
|
}
|
|
|
|
@endpoint(
|
|
name='get-certificate',
|
|
description=_('Retrieve an existing certificate'),
|
|
pattern=f'^{UUID_PATTERN}$',
|
|
example_pattern='{uuid}',
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('Certificate identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def get_certificate(self, request, uuid):
|
|
certificate = get_object_or_404(self.certificates, uuid=uuid)
|
|
return {
|
|
'err': 0,
|
|
'data': {
|
|
'uuid': certificate.uuid,
|
|
'data': certificate.data,
|
|
'validity_start': certificate.validity_start.isoformat(),
|
|
'validity_end': certificate.validity_end.isoformat(),
|
|
'qrcode_url': certificate.get_qrcode_url(request),
|
|
},
|
|
}
|
|
|
|
@endpoint(
|
|
name='get-qrcode',
|
|
description=_('Get QR Code'),
|
|
pattern=f'^{UUID_PATTERN}$',
|
|
example_pattern='{uuid}',
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('QRCode\'s certificate identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def get_qrcode(self, request, uuid):
|
|
certificate = self.certificates.get(uuid=uuid)
|
|
qr_code = certificate.generate_qr_code()
|
|
return HttpResponse(qr_code, content_type='image/png')
|
|
|
|
@endpoint(
|
|
name='save-reader',
|
|
pattern=f'^{UUID_PATTERN}?$',
|
|
example_pattern='{uuid}',
|
|
description=_('Create or update a qrcode reader'),
|
|
post={'request_body': {'schema': {'application/json': READER_SCHEMA}}},
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('QRCode reader identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def save_reader(self, request, uuid=None, post_data=None):
|
|
validity_start = parse_datetime(post_data['validity_start'])
|
|
validity_end = parse_datetime(post_data['validity_end'])
|
|
|
|
if not uuid:
|
|
reader = self.readers.create(
|
|
validity_start=validity_start,
|
|
validity_end=validity_end,
|
|
)
|
|
else:
|
|
reader = get_object_or_404(self.readers, uuid=uuid)
|
|
reader.validity_start = validity_start
|
|
reader.validity_end = validity_end
|
|
reader.save()
|
|
|
|
return {
|
|
'data': {
|
|
'uuid': reader.uuid,
|
|
'url': reader.get_url(request),
|
|
}
|
|
}
|
|
|
|
@endpoint(
|
|
name='get-reader',
|
|
description=_('Get informations about a QRCode reader'),
|
|
pattern=f'^{UUID_PATTERN}$',
|
|
example_pattern='{uuid}',
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('QRCode reader identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def get_reader(self, request, uuid):
|
|
reader = get_object_or_404(self.readers, uuid=uuid)
|
|
return {
|
|
'err': 0,
|
|
'data': {
|
|
'uuid': reader.uuid,
|
|
'validity_start': reader.validity_start.isoformat(),
|
|
'validity_end': reader.validity_end.isoformat(),
|
|
'url': reader.get_url(request),
|
|
},
|
|
}
|
|
|
|
@endpoint(
|
|
name='open-reader',
|
|
perm='OPEN',
|
|
description=_('Open a QRCode reader page.'),
|
|
pattern=f'^{UUID_PATTERN}$',
|
|
example_pattern='{uuid}',
|
|
parameters={
|
|
'uuid': {
|
|
'description': _('QRCode reader identifier'),
|
|
'example_value': '12345678-1234-1234-1234-123456789012',
|
|
}
|
|
},
|
|
)
|
|
def open_reader(self, request, uuid):
|
|
reader = get_object_or_404(self.readers, uuid=uuid)
|
|
now = datetime.now(timezone.utc)
|
|
return TemplateResponse(
|
|
request,
|
|
'qrcode/qrcode-reader.html',
|
|
context={
|
|
'started': now >= reader.validity_start,
|
|
'expired': now >= reader.validity_end,
|
|
'verify_key': self.hex_verify_key,
|
|
'reader': reader,
|
|
},
|
|
)
|
|
|
|
|
|
def encode_mime_like(data):
|
|
msg = ''
|
|
for key, value in data.items():
|
|
msg += '%s: %s\n' % (key, value.replace('\n', '\n '))
|
|
return msg.encode()
|
|
|
|
|
|
BASE45_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
|
BASE45_DICT = {v: i for i, v in enumerate(BASE45_CHARSET)}
|
|
|
|
|
|
def b45encode(buf: bytes) -> bytes:
|
|
"""Convert bytes to base45-encoded string"""
|
|
res = ''
|
|
buflen = len(buf)
|
|
for i in range(0, buflen & ~1, 2):
|
|
x = (buf[i] << 8) + buf[i + 1]
|
|
e, x = divmod(x, 45 * 45)
|
|
d, c = divmod(x, 45)
|
|
res += BASE45_CHARSET[c] + BASE45_CHARSET[d] + BASE45_CHARSET[e]
|
|
if buflen & 1:
|
|
d, c = divmod(buf[-1], 45)
|
|
res += BASE45_CHARSET[c] + BASE45_CHARSET[d]
|
|
return res.encode()
|
|
|
|
|
|
class Certificate(models.Model):
|
|
uuid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
|
|
validity_start = models.DateTimeField(verbose_name=_('Validity Start Date'))
|
|
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'))
|
|
data = models.JSONField(null=True, verbose_name='Certificate Data')
|
|
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='certificates')
|
|
|
|
def generate_qr_code(self):
|
|
data = {
|
|
'uuid': str(self.uuid),
|
|
'validity_start': str(self.validity_start.timestamp()),
|
|
'validity_end': str(self.validity_end.timestamp()),
|
|
} | self.data
|
|
msg = encode_mime_like(data)
|
|
signed = self.resource.signing_key.sign(msg)
|
|
qr_code = QRCode(image_factory=PilImage, error_correction=ERROR_CORRECT_Q)
|
|
qr_code.add_data(b45encode(signed).decode())
|
|
qr_code.make(fit=True)
|
|
image = qr_code.make_image(fill_color='black', back_color='white')
|
|
fd = BytesIO()
|
|
image.save(fd)
|
|
return fd.getvalue()
|
|
|
|
def get_qrcode_url(self, request):
|
|
qrcode_relative_url = reverse(
|
|
'generic-endpoint',
|
|
kwargs={
|
|
'slug': self.resource.slug,
|
|
'connector': self.resource.get_connector_slug(),
|
|
'endpoint': 'get-qrcode',
|
|
'rest': str(self.uuid),
|
|
},
|
|
)
|
|
return request.build_absolute_uri(qrcode_relative_url)
|
|
|
|
|
|
class Reader(models.Model):
|
|
uuid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
|
|
validity_start = models.DateTimeField(verbose_name=_('Validity Start Date'))
|
|
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'))
|
|
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='readers')
|
|
|
|
def get_url(self, request):
|
|
relative_url = reverse(
|
|
'generic-endpoint',
|
|
kwargs={
|
|
'slug': self.resource.slug,
|
|
'connector': self.resource.get_connector_slug(),
|
|
'endpoint': 'open-reader',
|
|
'rest': str(self.uuid),
|
|
},
|
|
)
|
|
return request.build_absolute_uri(relative_url)
|