def test_get_num_zero_bits(self): """ Test that reading zero bits from the stream as a number results in getting the number 0. """ bitstream = BitStream() self.assertEquals(bitstream.get_num(0), 0) # Store a 64-bit integer in the stream. num = int('00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) bitstream.seek(0) self.assertEquals(bitstream.get_num(0), 0)
def test_stress(self): """ Stress test BitStream by writings lots of numbers of different bit sizes and reading them back. """ # num_writes = 10000 num_writes = 1000 min_bit_length = 1 max_bit_length = 8192 # Generate the numbers nums = [] total_bit_length = 0 for i in range(0, num_writes): bit_length = random.randint(min_bit_length, max_bit_length) total_bit_length += bit_length val = random.randint(0, 2**bit_length - 1) nums.append({"bit_length": bit_length, "value": val}) # Write them to a BitStream object bitstream = BitStream() for num in nums: bitstream.put_num(num["value"], num["bit_length"]) # Check the BitStream length and current position self.assertEquals(bitstream.get_length(), total_bit_length) self.assertEquals(bitstream.get_current_pos(), total_bit_length) # Read all numbers back bitstream.seek(0) for num in nums: self.assertEquals(bitstream.get_num(num["bit_length"]), num["value"])
def test_stress(self): """ Stress test BitStream by writings lots of numbers of different bit sizes and reading them back. """ # num_writes = 10000 num_writes = 1000 min_bit_length = 1 max_bit_length = 8192 # Generate the numbers nums = [] total_bit_length = 0 for i in range(0, num_writes): bit_length = random.randint(min_bit_length, max_bit_length) total_bit_length += bit_length val = random.randint(0, 2**bit_length - 1) nums.append({"bit_length" : bit_length, "value" : val}) # Write them to a BitStream object bitstream = BitStream() for num in nums: bitstream.put_num(num["value"], num["bit_length"]) # Check the BitStream length and current position self.assertEquals(bitstream.get_length(),total_bit_length) self.assertEquals(bitstream.get_current_pos(),total_bit_length) # Read all numbers back bitstream.seek(0) for num in nums: self.assertEquals(bitstream.get_num(num["bit_length"]),num["value"])
def test_get_num_zero_bits(self): """ Test that reading zero bits from the stream as a number results in getting the number 0. """ bitstream = BitStream() self.assertEquals(bitstream.get_num(0), 0) # Store a 64-bit integer in the stream. num = int( '00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) bitstream.seek(0) self.assertEquals(bitstream.get_num(0), 0)
def test_seek_basic(self): """ Use seek to read data at different points in the bitstream. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int( '00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) # Get the 0th, 3rd, 5th and 6th bytes of the stream # First in forwards order bitstream.seek(0) self.assertEqual(bitstream.get_num(8), int('00000000', 2)) bitstream.seek(3 * 8) self.assertEqual(bitstream.get_num(8), int('01010011', 2)) bitstream.seek(5 * 8) self.assertEqual(bitstream.get_num(8), int('00100010', 2)) self.assertEqual(bitstream.get_num(8), int('10001001', 2)) # Then in backwards order bitstream.seek(6 * 8) self.assertEqual(bitstream.get_num(8), int('10001001', 2)) bitstream.seek(5 * 8) self.assertEqual(bitstream.get_num(8), int('00100010', 2)) bitstream.seek(3 * 8) self.assertEqual(bitstream.get_num(8), int('01010011', 2)) bitstream.seek(0) self.assertEqual(bitstream.get_num(8), int('00000000', 2))
def test_seek_basic(self): """ Use seek to read data at different points in the bitstream. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int('00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) # Get the 0th, 3rd, 5th and 6th bytes of the stream # First in forwards order bitstream.seek(0) self.assertEqual(bitstream.get_num(8),int('00000000',2)) bitstream.seek(3*8) self.assertEqual(bitstream.get_num(8),int('01010011',2)) bitstream.seek(5*8) self.assertEqual(bitstream.get_num(8),int('00100010',2)) self.assertEqual(bitstream.get_num(8),int('10001001',2)) # Then in backwards order bitstream.seek(6*8) self.assertEqual(bitstream.get_num(8),int('10001001',2)) bitstream.seek(5*8) self.assertEqual(bitstream.get_num(8),int('00100010',2)) bitstream.seek(3*8) self.assertEqual(bitstream.get_num(8),int('01010011',2)) bitstream.seek(0) self.assertEqual(bitstream.get_num(8),int('00000000',2))
def test_num_write_part(self): """ Test that we can write a large number piece by piece and read it as whole number. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int('00000000000000110101011001010011' '00101100001000101000100110101111', 2) # ... in 8-bit, 16-bit and 32-bit increments bitstream.put_num(int('00000000',2), 8) bitstream.put_num(int('0000001101010110',2), 16) bitstream.put_num(int('01010011001011000010001010001001',2), 32) bitstream.put_num(int('10101111',2), 8) # Get it as a single 64-bit number bitstream.seek(0) self.assertEqual(bitstream.get_num(64),num)
def test_num_write_part(self): """ Test that we can write a large number piece by piece and read it as whole number. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int( '00000000000000110101011001010011' '00101100001000101000100110101111', 2) # ... in 8-bit, 16-bit and 32-bit increments bitstream.put_num(int('00000000', 2), 8) bitstream.put_num(int('0000001101010110', 2), 16) bitstream.put_num(int('01010011001011000010001010001001', 2), 32) bitstream.put_num(int('10101111', 2), 8) # Get it as a single 64-bit number bitstream.seek(0) self.assertEqual(bitstream.get_num(64), num)
def test_num_read_part(self): """ Test that we can read only some bits of a large number and interpret them as a shorter number with the expected value. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int( '00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) # Get it as 8-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(8), int('00000000', 2)) self.assertEqual(bitstream.get_num(8), int('00000011', 2)) self.assertEqual(bitstream.get_num(8), int('01010110', 2)) self.assertEqual(bitstream.get_num(8), int('01010011', 2)) self.assertEqual(bitstream.get_num(8), int('00101100', 2)) self.assertEqual(bitstream.get_num(8), int('00100010', 2)) self.assertEqual(bitstream.get_num(8), int('10001001', 2)) self.assertEqual(bitstream.get_num(8), int('10101111', 2)) # Get it as 16-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(16), int('0000000000000011', 2)) self.assertEqual(bitstream.get_num(16), int('0101011001010011', 2)) self.assertEqual(bitstream.get_num(16), int('0010110000100010', 2)) self.assertEqual(bitstream.get_num(16), int('1000100110101111', 2)) # Get it as 32-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(32), int('00000000000000110101011001010011', 2)) self.assertEqual(bitstream.get_num(32), int('00101100001000101000100110101111', 2))
def test_num_read_part(self): """ Test that we can read only some bits of a large number and interpret them as a shorter number with the expected value. """ bitstream = BitStream() # Store a 64-bit integer in the stream. num = int('00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) # Get it as 8-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(8),int('00000000',2)) self.assertEqual(bitstream.get_num(8),int('00000011',2)) self.assertEqual(bitstream.get_num(8),int('01010110',2)) self.assertEqual(bitstream.get_num(8),int('01010011',2)) self.assertEqual(bitstream.get_num(8),int('00101100',2)) self.assertEqual(bitstream.get_num(8),int('00100010',2)) self.assertEqual(bitstream.get_num(8),int('10001001',2)) self.assertEqual(bitstream.get_num(8),int('10101111',2)) # Get it as 16-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(16),int('0000000000000011',2)) self.assertEqual(bitstream.get_num(16),int('0101011001010011',2)) self.assertEqual(bitstream.get_num(16),int('0010110000100010',2)) self.assertEqual(bitstream.get_num(16),int('1000100110101111',2)) # Get it as 32-bit numbers bitstream.seek(0) self.assertEqual(bitstream.get_num(32), int('00000000000000110101011001010011',2)) self.assertEqual(bitstream.get_num(32), int('00101100001000101000100110101111',2))
def test_num_basic(self): """ Test basic put_num and get_num behavior """ bitstream = BitStream() # Some parameters num_sizes = [4, 8, 16, 32, 64, 128, 256, 2839] num_writes = 10 nums = [] # Add num_writes integers of each size to nums: for i in range(0, len(num_sizes)): max_val = 2**num_sizes[i] - 1 nums.append([]) for j in range(0, num_writes): # append a random number between 0 and max_val, inclusive nums[i].append(random.randint(0, max_val)) # Write all values in nums to the stream for i in range(0, len(num_sizes)): for j in range(0, num_writes): bitstream.put_num(nums[i][j], num_sizes[i]) # Go back to start of the stream bitstream.seek(0) # Sanity check: expected_length = 0 for num_size in num_sizes: expected_length += num_size * num_writes self.assertEqual(bitstream.get_length(), expected_length) # Read them back and compare for i in range(0, len(num_sizes)): for j in range(0, num_writes): n = bitstream.get_num(num_sizes[i]) self.assertEqual(n, nums[i][j])
def test_num_basic(self): """ Test basic put_num and get_num behavior """ bitstream = BitStream() # Some parameters num_sizes = [4,8,16,32,64,128,256,2839] num_writes = 10 nums = [] # Add num_writes integers of each size to nums: for i in range(0,len(num_sizes)): max_val = 2**num_sizes[i] - 1 nums.append([]) for j in range(0,num_writes): # append a random number between 0 and max_val, inclusive nums[i].append(random.randint(0, max_val)) # Write all values in nums to the stream for i in range(0,len(num_sizes)): for j in range(0,num_writes): bitstream.put_num(nums[i][j], num_sizes[i]) # Go back to start of the stream bitstream.seek(0) # Sanity check: expected_length = 0 for num_size in num_sizes: expected_length += num_size * num_writes self.assertEqual(bitstream.get_length(), expected_length) # Read them back and compare for i in range(0,len(num_sizes)): for j in range(0,num_writes): n = bitstream.get_num(num_sizes[i]) self.assertEqual(n, nums[i][j])
def from_file(cls, filename, SerializerClass=serialize.XMLSerializer): """ Loads an instance of Ciphertext from the given file. Arguments: filename::string -- The name of a file containing the ciphertext in serialized form. SerializerClass::class -- The class that provides the deserialization. XMLSerializer by default. Must inherit from serialize.BaseSerializer and provide an adequate deserialize_from_file method. Note that often the same class used to serialize the data must be used to deserialize it. (see utilities/serialize.py documentation for more information) Throws: InvalidPloneVoteCryptoFileError -- If the file is not a valid PloneVoteCryptoLib stored ciphertext file. """ # Create a new serializer object for the Ciphertext structure definition serializer = SerializerClass(Ciphertext_serialize_structure_definition) # Deserialize the Ciphertext instance from file try: data = serializer.deserialize_from_file(filename) except serialize.InvalidSerializeDataError as e: # Convert the exception to an InvalidPloneVoteCryptoFileError raise InvalidPloneVoteCryptoFileError(filename, "File \"%s\" does not contain a valid ciphertext. The " \ "following error occurred while trying to deserialize the " \ "file contents: %s" % (filename, str(e))) # Get the values from the deserialized data try: nbits = int(data["PloneVoteCiphertext"]["nbits"]) except ValueError: raise InvalidPloneVoteCryptoFileError(filename, "File \"%s\" does not contain a valid ciphertext. The " \ "stored value for nbits is not a valid (decimal) integer." \ % filename) fingerprint_str = data["PloneVoteCiphertext"]["PKFingerprint"] enc_data_str = data["PloneVoteCiphertext"]["EncryptedData"] # Construct a new Ciphertext object with the given nbits and fingerprint ciphertext = cls(nbits, fingerprint_str) # Load the encrypted data bitstream = BitStream() bitstream.put_base64(enc_data_str) bitstream.seek(0) length = bitstream.get_length() # number of gamma and delta blocks in the bitstream: blocks = length // (nbits * 2) for i in range(0, blocks): gamma_val = bitstream.get_num(nbits) delta_val = bitstream.get_num(nbits) ciphertext.append(gamma_val, delta_val) # Return the ciphertext return ciphertext
def new(cls, original_collection, shuffled_collection, mapping): """ Constructs a new proof of equivalence between original_collection and shuffled_collection. This method should not be used outside of plonevotecryptolib.Mixnet. Consider using CiphertextCollection.shuffle_with_proof() instead. The given CiphertextCollectionMapping must be a valid mapping between original_collection and shuffled_collection. Arguments: original_collection::CiphertextCollection -- The original collection to be shuffled. shuffled_collection::CiphertextCollection -- The shuffled collection resulting from applying mapping to original_collection. mapping::CiphertextCollectionMapping -- The mapping between original_collection and shuffled_collection. Returns: proof::ShufflingProof -- The zero-knowledge proof of equivalence between original_collection and shuffled_collection. Throws: InvalidCiphertextCollectionMappingError -- If mapping is not a valid mapping between original_collection and shuffled_collection. ValueError -- If params.SHUFFLING_PROOF_SECURITY_PARAMETER is within an invalid range. """ # Check that we have a valid mapping between the two collections: if(not mapping.verify(original_collection, shuffled_collection)): raise InvalidCiphertextCollectionMappingError( \ "mapping was expected to be a CiphertextCollectionMapping "\ "object representing a valid mapping between "\ "original_collection and shuffled_collection. However, "\ "mapping.verify(original_collection, shuffled_collection) "\ "returns False. A zero-knowledge proof of equivalence "\ "between two shuffled collections cannot be constructed "\ "without first having a valid mapping between them.") # Get the security parameter P security_parameter = params.SHUFFLING_PROOF_SECURITY_PARAMETER # Check that the security parameter is <= 256, since that is the most # bits of challenge we can currently have. Also check that P is at least 1 if(security_parameter < 1 or security_parameter > 256): raise ValueError("Security parameter for shuffling proof " \ "(params.SHUFFLING_PROOF_SECURITY_PARAMETER) is out of " \ "range. The security parameter must be between 1 and 256, "\ "its current value is %d. If you have set " \ "CUSTOM_SHUFFLING_PROOF_SECURITY_PARAMETER in the file " \ "params.py, please correct this value or set it to None, " \ "in order for the security parameter to be decided based " \ "on the global SECURITY_LEVEL of plonevotecryptolib. It " \ "is recommended that " \ "CUSTOM_SHUFFLING_PROOF_SECURITY_PARAMETER be always set " \ "to None for deployment operation of plonevotecryptolib." \ % security_parameter) # Construct a new empty proof proof = ShufflingProof() # Populate proof._collections with P random shuffles of # original_collection. We save each mapping for now in proof._mappings. # (ie. every mapping in proof._mappings[i] will initially be from the # original collection into proof._collections[i]) for i in range(0, security_parameter): ## print "Generating collection #%d" % i # replace with TaskMonitor API calls new_mapping = CiphertextCollectionMapping.new(original_collection) proof._mappings.append(new_mapping) proof._collections.append(new_mapping.apply(original_collection)) # Generate the challenge proof._challenge = \ proof._generate_challenge(original_collection, shuffled_collection) # Get the challenge as a BitStream for easier manipulation challenge_bits = BitStream() challenge_bits.put_hex(proof._challenge) challenge_bits.seek(0) # back to the beginning of the stream # For each of the first P bits in the stream for i in range(0, security_parameter): ## print "Processing challenge bit %d" % i # replace with TaskMonitor API calls bit = challenge_bits.get_num(1) if(bit == 0): # Do nothing. # proof._mappings[i] is already a mapping from # original_collection unto proof._collections[i] pass elif(bit == 1): # Change proof._mappings[i] to be a mapping from # proof._collections[i] unto shuffled_collection, using # CiphertextCollectionMapping.rebase(...) # rebase(O->D, O->C_{i}) => C_{i}->D rebased_mapping = mapping.rebase(proof._mappings[i]) # Replace O->C_{i} with C_{i}->D proof._mappings[i] = rebased_mapping else: assert False, "We took a single bit, its value must be either "\ "0 or 1." # return the proof object return proof
def verify(self, original_collection, shuffled_collection): """ Verifies that original_collection and shuffled_collection are equivalent as proven by this ShufflingProof object. If this method returns true, then we have proof that both collections contain encryptions of the same collection of plaintexts, save for the negligible probability (for a correct security parameter configured in params.py) that the zero-knowledge proof has been faked. Otherwise we gain no information about the two collections, other than they are not shown to be equivalent by this particular proof. ShufflingProof is a zero-knowledge proof: the result of this verification or the information within the ShufflingProof object for which two collections pass this verification, provide no information as to the association of particular ciphertexts in original_collection with particular ciphertexts of shuffled_collection. Arguments: original_collection::CiphertextCollection -- The original collection of ciphertexts. shuffled_collection::CiphertextCollection -- Another collection for which we wish to know if the current ShufflingProof object demonstrates equivalence with original_collection. Returns: result::bool -- True if this proof shows both collections to be equivalent. False otherwise. """ # Get the security parameter P with which the proof was originally # created. This is reflect in the length of self._collections and # self._mappings. assert len(self._collections) == len(self._mappings), \ "The length of the private properties self._collections and " \ "self._mappings of ShufflingProof must always be the same." security_parameter = len(self._collections) # Get the security parameter specified in params minimum_allowed_security_parameter = params.SHUFFLING_PROOF_SECURITY_PARAMETER # Check that the security parameter is <= 256, since that is the most # bits of challenge we can currently have. Also check that P is at least 1 if(minimum_allowed_security_parameter < 1 or minimum_allowed_security_parameter > 256): raise ValueError("Security parameter for shuffling proof " \ "(params.SHUFFLING_PROOF_SECURITY_PARAMETER) is out of " \ "range. The security parameter must be between 1 and 256, "\ "its current value is %d. If you have set " \ "CUSTOM_SHUFFLING_PROOF_SECURITY_PARAMETER in the file " \ "params.py, please correct this value or set it to None, " \ "in order for the security parameter to be decided based " \ "on the global SECURITY_LEVEL of plonevotecryptolib. It " \ "is recommended that " \ "CUSTOM_SHUFFLING_PROOF_SECURITY_PARAMETER be always set " \ "to None for deployment operation of plonevotecryptolib." \ % security_parameter) # Check that the security parameter for which the proof was generated # is correct and meets the security standards declared in params.py if(security_parameter < minimum_allowed_security_parameter or security_parameter < 1 or security_parameter > 256): raise InvalidShuffilingProofError("The security parameter for " \ "(which the current proof was created is %d. This is " \ "(either an incorrect value (outside of the [1,256] " \ "(range) or not secure enough to meet the standards set " \ "(in the configuration of params.py (< %d). The proof " \ "(is thus invalid." \ % (security_parameter, minimum_allowed_security_parameter)) # Generate the challenge challenge = \ self._generate_challenge(original_collection, shuffled_collection) # Verify that the challenge corresponds to the stored one if(challenge != self._challenge): return False # Get the challenge as a BitStream for easier manipulation challenge_bits = BitStream() challenge_bits.put_hex(challenge) challenge_bits.seek(0) # back to the beginning of the stream # For each of the first P bits in the stream for i in range(0, security_parameter): bit = challenge_bits.get_num(1) if(bit == 0): # verify that self._mapping[i] maps original_collection into # self._collections[i] if(not self._mappings[i].verify(original_collection, self._collections[i])): return False elif(bit == 1): # verify that self._mapping[i] self._collections[i] into # self._collections[i] if(not self._mappings[i].verify(self._collections[i], shuffled_collection)): return False else: assert False, "We took a single bit, its value must be either "\ "0 or 1." # If we made it so far, the proof is correct # (each mapping is in accordance to the challenge and valid) return True
def encrypt_bitstream(self, bitstream, pad_to=None, task_monitor=None): """ Encrypts the given bitstream into a ciphertext object. Arguments: bitstream::BitStream-- A stream of bits to encrypt (see BitStream utility class). pad_to::int -- Minimum size (in bytes) of the resulting ciphertext. Data will be padded before encryption to match this size. task_monitor::TaskMonitor -- A task monitor for this task. Returns: ciphertext:Ciphertext -- A ciphertext object encapsulating the encrypted data. """ random = StrongRandom() ## PART 1 # First, format the bitstream as per Ciphertext.py Note 001, # previous to encryption. # [size (64 bits) | message (size bits) | padding (X bits) ] ## formated_bitstream = BitStream() # The first 64 encode the size of the actual data in bits SIZE_BLOCK_LENGTH = 64 size_in_bits = bitstream.get_length() if(size_in_bits >= 2**SIZE_BLOCK_LENGTH): raise ValueError("The size of the bitstream to encrypt is larger " \ "than 16 Exabits. The current format for " \ "PloneVote ciphertext only allows encrypting a " \ "maximum of 16 Exabits of information.") formated_bitstream.put_num(size_in_bits, SIZE_BLOCK_LENGTH) # We then copy the contents of the original bitstream bitstream.seek(0) formated_bitstream.put_bitstream_copy(bitstream) # Finally, we append random data until we reach the desired pad_to # length unpadded_length = formated_bitstream.get_length() if(pad_to != None and (pad_to * 8) > unpadded_length): full_length = pad_to * 8 else: full_length = unpadded_length padding_left = full_length - unpadded_length while(padding_left > 1024): padding_bits = random.randint(1, 2**1024) formated_bitstream.put_num(padding_bits,1024) padding_left -= 1024 if(padding_left > 0): padding_bits = random.randint(1, 2**padding_left) formated_bitstream.put_num(padding_bits, padding_left) padding_left = 0 ## PART 2 # We encrypt the formated bitsteam using ElGamal into a Ciphertext # object. # See "Handbook of Applied Cryptography" Algorithm 8.18 ## # block_size is the size of each block of bits to encrypt # since we can only encrypt messages in [0, p - 1] # we should use (nbits - 1) as the block size, where # 2**(nbits - 1) < p < 2**nbits block_size = self.cryptosystem.get_nbits() - 1 prime = self.cryptosystem.get_prime() generator = self.cryptosystem.get_generator() # We pull data from the bitstream one block at a time and encrypt it formated_bitstream.seek(0) ciphertext = \ Ciphertext(self.cryptosystem.get_nbits(), self.get_fingerprint()) plaintext_bits_left = formated_bitstream.get_length() # Check if we have a task monitor and register with it if(task_monitor != None): # We will do two tick()s per block to encrypt: one for generating # the gamma component of the ciphertext block and another for the # delta component (those are the two time intensive steps, # because of exponentiation). ticks = math.ceil((1.0 * plaintext_bits_left) / block_size) * 2 encrypt_task_mon = \ task_monitor.new_subtask("Encrypt data", expected_ticks = ticks) while(plaintext_bits_left > 0): # get next block (message, m, etc) to encrypt if(plaintext_bits_left >= block_size): block = formated_bitstream.get_num(block_size) plaintext_bits_left -= block_size else: block = formated_bitstream.get_num(plaintext_bits_left) # Encrypt as if the stream was filled with random data past its # end, this avoids introducing a 0's gap during decryption to # bitstream displacement = block_size - plaintext_bits_left block = block << displacement padding = random.randint(0, 2**displacement - 1) assert (padding // 2**displacement == 0), \ "padding should be at most displacement bits long" block = block | padding plaintext_bits_left = 0 # Select a random integer k, 1 <= k <= p − 2 k = random.randint(1, prime - 2) # Compute gamma and delta gamma = pow(generator, k, prime) if(task_monitor != None): encrypt_task_mon.tick() delta = (block * pow(self._key, k, prime)) % prime if(task_monitor != None): encrypt_task_mon.tick() # Add this encrypted data portion to the ciphertext object ciphertext.append(gamma, delta) # return the ciphertext object return ciphertext
def encrypt_bitstream(self, bitstream, pad_to=None, task_monitor=None): """ Encrypts the given bitstream into a ciphertext object. Arguments: bitstream::BitStream-- A stream of bits to encrypt (see BitStream utility class). pad_to::int -- Minimum size (in bytes) of the resulting ciphertext. Data will be padded before encryption to match this size. task_monitor::TaskMonitor -- A task monitor for this task. Returns: ciphertext:Ciphertext -- A ciphertext object encapsulating the encrypted data. """ random = StrongRandom() ## PART 1 # First, format the bitstream as per Ciphertext.py Note 001, # previous to encryption. # [size (64 bits) | message (size bits) | padding (X bits) ] ## formated_bitstream = BitStream() # The first 64 encode the size of the actual data in bits SIZE_BLOCK_LENGTH = 64 size_in_bits = bitstream.get_length() if(size_in_bits >= 2**SIZE_BLOCK_LENGTH): raise ValueError("The size of the bitstream to encrypt is larger " \ "than 16 Exabits. The current format for " \ "PloneVote ciphertext only allows encrypting a " \ "maximum of 16 Exabits of information.") formated_bitstream.put_num(size_in_bits, SIZE_BLOCK_LENGTH) # We then copy the contents of the original bitstream bitstream.seek(0) formated_bitstream.put_bitstream_copy(bitstream) # Finally, we append random data until we reach the desired pad_to # length unpadded_length = formated_bitstream.get_length() if(pad_to != None and (pad_to * 8) > unpadded_length): full_length = pad_to * 8 else: full_length = unpadded_length padding_left = full_length - unpadded_length while(padding_left > 1024): padding_bits = random.randint(1, 2**1024) formated_bitstream.put_num(padding_bits,1024) padding_left -= 1024 if(padding_left > 0): padding_bits = random.randint(1, 2**padding_left) formated_bitstream.put_num(padding_bits, padding_left) padding_left = 0 ## PART 2 # We encrypt the formated bitsteam using ElGamal into a Ciphertext # object. # See "Handbook of Applied Cryptography" Algorithm 8.18 ## # block_size is the size of each block of bits to encrypt # since we can only encrypt messages in [0, p - 1] # we should use (nbits - 1) as the block size, where # 2**(nbits - 1) < p < 2**nbits block_size = self.cryptosystem.get_nbits() - 1 prime = self.cryptosystem.get_prime() generator = self.cryptosystem.get_generator() # We pull data from the bitstream one block at a time and encrypt it formated_bitstream.seek(0) ciphertext = \ Ciphertext(self.cryptosystem.get_nbits(), self.get_fingerprint()) plaintext_bits_left = formated_bitstream.get_length() # Check if we have a task monitor and register with it if(task_monitor != None): # We will do two tick()s per block to encrypt: one for generating # the gamma component of the ciphertext block and another for the # delta component (those are the two time intensive steps, # because of exponentiation). ticks = math.ceil((1.0 * plaintext_bits_left) / block_size) * 2 encrypt_task_mon = \ task_monitor.new_subtask("Encrypt data", expected_ticks = ticks) while(plaintext_bits_left > 0): # get next block (message, m, etc) to encrypt if(plaintext_bits_left >= block_size): block = formated_bitstream.get_num(block_size) plaintext_bits_left -= block_size else: block = formated_bitstream.get_num(plaintext_bits_left) # Encrypt as if the stream was filled with random data past its # end, this avoids introducing a 0's gap during decryption to # bitstream displacement = block_size - plaintext_bits_left block = block << displacement padding = random.randint(0, 2**displacement - 1) assert (padding / 2**displacement == 0), \ "padding should be at most displacement bits long" block = block | padding plaintext_bits_left = 0 # Select a random integer k, 1 <= k <= p − 2 k = random.randint(1, prime - 2) # Compute gamma and delta gamma = pow(generator, k, prime) if(task_monitor != None): encrypt_task_mon.tick() delta = (block * pow(self._key, k, prime)) % prime if(task_monitor != None): encrypt_task_mon.tick() # Add this encrypted data portion to the ciphertext object ciphertext.append(gamma, delta) # return the ciphertext object return ciphertext