Coverage for sfkit/encryption/mpc/random_number_generator.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-07 15:11 -0400

1# sourcery skip: avoid-single-character-names-variables, require-parameter-annotation 

2# for MPC GWAS 

3 

4import math 

5 

6import nacl.secret 

7 

8# some arbitrary string of length 1000; its content is not important to the efficacy/security of the encryption; in particular, it is okay to be deterministic like this 

9MESSAGE = b"neoiiztdnrzxokrhqnzlufoehvdknkflkypwvgnjzhfivnlecgzijiepozmiqnrqcaefhzusbymkzcrcxboozvtlvcylhpxemteaycpluxbezsiczcezzmdvibqraczxztvlaolphtiwogpinowxffviwkzapoqozozagnnzrnstxpvtidnajdmqxvvlsbzlzdcgnznhodcjxrjqigrcgzppcrfpidfwldtzbqzaaxkjeddmytjgfoekmvqvkixfthipaczpdcmlvucctxkmblpusybzsgyopzeedtqlhgbrbmfxcpdafktznmnrhhuzebmipynozsglrzaqbywexrvnudcxtelwhyarbvrsphefztdivytybagfcrqxbulgzndqgkoodgsxnntofscryscfkvgvlafvreabrymxpwhkbyjwetsehlwvaoiutqrdppydxcspzlkurijvbhjpoqosntdeofmmajydthafqubarwbngxydqpzjgtaotsgdqpelnfycvggoyxomgnqkcvosrirtelcdqhbfmtuvzoxmnrdbfltdohcitiutciyyxrzallhtjcqwqbxinckicdhvupwbnlkkvmmuoxlxhkflxhgqxoymevqfxihruqdqilqkydlrvzyvmrkncjdcrkudtjufzayhifjogywnyxfclqpyhdssrkwytnbdxlvwxwrsliymzlcvsjertgcychbzncgkhopawsufcefjwdveivduwphrkasigxtndyftmswovaxxkprxehscmflhmqkveqxlekpgrhnxpsgpmriibfeivotfbmkcwocsewxhusduzqgxbjfasutjwpdgntljntjgbrrozcfmbxbjkqihzytwdauznoofukgucmibfriisdqrqgxzjewyngwefvstvbibuylkbqcfjhqgvdhqqmatrwnjoxycejcxpqrbvwxqhkgnivjuuzylitpvfbmdwjdqhartpvcjookn" 

10 

11 

12class PseudoRandomNumberGenerator: 

13 """ 

14 This custom Random Number Generator deterministically generates random 

15 numbers in the range of some prime in a cryptographically secure fashion. 

16 """ 

17 

18 def __init__(self, key: bytes, base_p: int): 

19 self.base_p = base_p 

20 self.box = nacl.secret.SecretBox(key) 

21 self.nonce = 0 

22 self.buffer = [] 

23 

24 def generate_buffer(self) -> None: 

25 """ 

26 Generates a buffer of random numbers in the range of base_p 

27 """ 

28 self.nonce += 1 

29 

30 pseudo_random_byte_string = self.box.encrypt(MESSAGE, self.nonce.to_bytes(24, byteorder="big")).ciphertext[16:] 

31 # 16: because this encryption adds 16 bytes at the beginning, relative to the cpp implementation 

32 

33 self.buffer = self.convert_byte_string_to_list_of_ints_in_range(pseudo_random_byte_string) 

34 

35 def next(self) -> int: 

36 if not self.buffer: 

37 self.generate_buffer() 

38 return self.buffer.pop(0) 

39 

40 def convert_byte_string_to_list_of_ints_in_range(self, byte_string) -> list: 

41 res = [] 

42 

43 l = len(bin(self.base_p)) - 2 # bit length; -2 to remove the "0b" prefix 

44 n = math.ceil(l / 8) # number of bytes we need to convert 

45 

46 # pop first n bytes from byte_string 

47 while len(byte_string) >= n: 

48 cur = list(byte_string[:n]) 

49 byte_string = byte_string[n:] 

50 

51 # if there are extra bits, we mask the first ones with 0; this is currently not necessary, but it is good to have it in case we change the base_p 

52 if (n * 8) > l: 

53 diff = n * 8 - l 

54 # mask first diff bits as 0 

55 cur[0] = cur[0] & (1 << diff) - 1 

56 

57 cur = int.from_bytes(cur, byteorder="big") 

58 

59 # check if cur is in range; I can't just use the modulus as that would 

60 # lead to a non-uniform distribution for the numbers 

61 if cur <= self.base_p: 

62 res.append(cur) 

63 

64 return res