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
« 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
4import math
6import nacl.secret
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"
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 """
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 = []
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
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
33 self.buffer = self.convert_byte_string_to_list_of_ints_in_range(pseudo_random_byte_string)
35 def next(self) -> int:
36 if not self.buffer:
37 self.generate_buffer()
38 return self.buffer.pop(0)
40 def convert_byte_string_to_list_of_ints_in_range(self, byte_string) -> list:
41 res = []
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
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:]
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
57 cur = int.from_bytes(cur, byteorder="big")
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)
64 return res