Hide keyboard shortcuts

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/>. 

21 

22"""Generate random strings with a checksum. 

23 

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. 

30 

31""" 

32 

33import hashlib 

34import math 

35import random 

36 

37from django.forms import ValidationError 

38from django.utils.translation import gettext_lazy as _ 

39 

40CHECK_DIGITS = 2 

41ALPHABET = '0123456789ACDEFGHJKLMNPQRTUVWXYZ' # without BIOS for similarity to 8105 

42HASH = hashlib.md5 

43 

44safe_random = random.SystemRandom() 

45 

46 

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)] 

51 

52 

53def checksum2(msg, length): 

54 h = HASH(msg.encode('ascii')).digest() 

55 return ''.join(ALPHABET[h[i] % len(ALPHABET)] for i in range(length)) 

56 

57 

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 

65 

66 

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) 

71 

72 

73def normalize(s): 

74 return s\ 

75 .upper()\ 

76 .replace('B', '8')\ 

77 .replace('I', '1')\ 

78 .replace('O', '0')\ 

79 .replace('S', '5') 

80 

81 

82def clean(s): 

83 s = normalize(s) 

84 

85 if not all(c in ALPHABET for c in s): 

86 raise ValidationError(_('Pseudonym contains invalid characters'), code='characters') 

87 

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') 

93 

94 return s