Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) 2018-2020
2# MPIB <https://www.mpib-berlin.mpg.de/>,
3# MPI-CBS <https://www.cbs.mpg.de/>,
4# MPIP <http://www.psych.mpg.de/>
5#
6# This file is part of Castellum.
7#
8# Castellum is free software; you can redistribute it and/or modify it
9# under the terms of the GNU Affero General Public License as published
10# by the Free Software Foundation; either version 3 of the License, or
11# (at your option) any later version.
12#
13# Castellum is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16# Affero General Public License for more details.
17#
18# You should have received a copy of the GNU Affero General Public
19# License along with Castellum. If not, see
20# <http://www.gnu.org/licenses/>.
22"""Generate random strings with a checksum.
24- We use an alphabet of length 32 and a checksum of length 2. So the
25 probability of a random string from the alphabet passing the check
26 is 32**-2 ~= 10**-3.
27- The number of possible strings is 32**(length-2). If we have already
28 used 1024 of them and assuming a length of 10, the probability of
29 generating a duplicate is 1024 / 32**(10-2) = 2**-30 ~= 10**-9.
31"""
33import hashlib
34import math
35import random
37from django.forms import ValidationError
38from django.utils.translation import gettext_lazy as _
40CHECK_DIGITS = 2
41ALPHABET = '0123456789ACDEFGHJKLMNPQRTUVWXYZ' # without BIOS for similarity to 8105
42HASH = hashlib.md5
44safe_random = random.SystemRandom()
47def checksum1(msg):
48 # Generate a digit that is guaranteed to change on a single input error.
49 x = sum(ALPHABET.index(c) for c in msg)
50 return ALPHABET[x % len(ALPHABET)]
53def checksum2(msg, length):
54 h = HASH(msg.encode('ascii')).digest()
55 return ''.join(ALPHABET[h[i] % len(ALPHABET)] for i in range(length))
58def checksum(msg):
59 s = ''
60 if CHECK_DIGITS >= 1: 60 ↛ 62line 60 didn't jump to line 62, because the condition on line 60 was never false
61 s += checksum1(msg)
62 if CHECK_DIGITS >= 2: 62 ↛ 64line 62 didn't jump to line 64, because the condition on line 62 was never false
63 s += checksum2(msg, CHECK_DIGITS - 1)
64 return s
67def generate(bits=40):
68 k = math.ceil(bits * math.log(2, len(ALPHABET)))
69 msg = ''.join(safe_random.choices(ALPHABET, k=k))
70 return msg + checksum(msg)
73def normalize(s):
74 return s\
75 .upper()\
76 .replace('B', '8')\
77 .replace('I', '1')\
78 .replace('O', '0')\
79 .replace('S', '5')
82def clean(s):
83 s = normalize(s)
85 if not all(c in ALPHABET for c in s):
86 raise ValidationError(_('Pseudonym contains invalid characters'), code='characters')
88 msg = s[:-CHECK_DIGITS]
89 actual = s[-CHECK_DIGITS:]
90 expected = checksum(msg)
91 if actual != expected:
92 raise ValidationError(_('Invalid pseudonym'), code='invalid')
94 return s