def test_seek_negative_position(self): """ Test that seeking to a negative position raises an error. """ bitstream = BitStream() self.assertRaises(SeekOutOfRangeError, bitstream.seek, -1) # An invalid seek(...) call should not move the position indicator. self.assertEqual(bitstream.get_current_pos(), 0)
def test_bitstream_constructor(self): """ Test that creating an empty bitstream succeeds. """ bitstream = BitStream() # An empty bitstream has length 0 ... self.assertEqual(bitstream.get_length(), 0) # ...and has its current position marker at position 0. self.assertEqual(bitstream.get_current_pos(), 0)
def test_get_hex_invalid_length(self): """ Test that trying to read a number of bits that is not a multiple of 4 as hex data raises an exception. """ bitstream = BitStream() bitstream.put_hex("DfF7CE69fF5478A") # 15 digits, 60 bits bitstream.seek(0) self.assertRaises(ValueError, bitstream.get_hex, 47) # read 11.75 hex digits?
def test_get_base64_invalid_length(self): """ Test that trying to read a number of bits that is not a multiple of 8 as base64 data raises an exception. """ bitstream = BitStream() bitstream.put_base64("Zm9vYmE=") # 40 bits bitstream.seek(0) self.assertRaises(ValueError, bitstream.get_base64, 26) # read 3.25 bytes?
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_string_character_zero(self): """ Test that we are handling character zero ('\\0') correctly. """ bitstream = BitStream() bitstream.put_string("Evil \0String.") # 13 chars/bytes self.assertEquals(bitstream.get_length(), 13 * 8) bitstream.seek(0) self.assertEquals(bitstream.get_string(bitstream.get_length()), "Evil \0String.")
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_put_num_negative_length(self): """ Test that put_num raises an exception when passed negative integer values as its bit_length argument. """ bitstream = BitStream() val = random.randint(-128, -1) self.assertRaises(ValueError, bitstream.put_num, 23, val)
def test_put_byte_negative(self): """ Test that put_byte raises an exception when passed negative integer values as its argument. """ bitstream = BitStream() val = random.randint(-2**8 + 1, -1) self.assertRaises(ValueError, bitstream.put_byte, val)
def test_string_character_zero(self): """ Test that we are handling character zero ('\\0') correctly. """ bitstream = BitStream() bitstream.put_string("Evil \0String.") # 13 chars/bytes self.assertEquals(bitstream.get_length(),13*8) bitstream.seek(0) self.assertEquals(bitstream.get_string(bitstream.get_length()), "Evil \0String.")
def test_hex_basic(self): """ This method tests put_hex's and get_hex's basic functionality. """ bitstream = BitStream() # Generate a random string of hex digits, ie: ('0'-'9'|'a'-'f'|'A'-'F')* valid_hex_digits = "0123456789abcdefABCDEF" num_digits = 50 digits = "" for i in range(0, num_digits): digits += random.choice(valid_hex_digits) # Put those digits in the BitStream... bitstream.put_hex(digits) # ...and get them back bitstream.seek(0) read_digits = bitstream.get_hex(len(digits) * 4) # 1 hex digit == 4 bits # Check that the hexadecimal digits were recovered correctly # Note that case information may be lost. Comparison must be case # insensitive (ie. 'a9Bc' and 'A9bC' are equal) self.assertEqual(read_digits.lower(), digits.lower())
def encrypt_text(self, text, pad_to=None, task_monitor=None): """ Encrypts the given string into a ciphertext object. Arguments: text::string -- A string to encrypt. 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. """ bitstream = BitStream() bitstream.put_string(text) return self.encrypt_bitstream(bitstream, pad_to, task_monitor)
def test_put_base64_wrong_type(self): """ Test that put_base64 raises an exception when passed non string values as its argument. """ bitstream = BitStream() self.assertRaises(TypeError, bitstream.put_base64, 23234) self.assertRaises(TypeError, bitstream.put_base64, 0.1) self.assertRaises(TypeError, bitstream.put_base64, object())
def test_put_num_wrong_bit_length(self): """ Test that put_num raises an exception when given incompatible num and bit_length arguments (ie. num >= 2**bit_length) """ bitstream = BitStream() self.assertRaises(ValueError, bitstream.put_num, 2**16, 16) val = random.randint(2**64 + 1, 2**128) self.assertRaises(ValueError, bitstream.put_num, val, 64)
def test_put_byte_to_big(self): """ Test that put_byte raises an exception when passed any integer value > 255 as its argument. """ bitstream = BitStream() self.assertRaises(ValueError, bitstream.put_byte, 256) val = random.randint(2**8 + 1, 2**32) self.assertRaises(ValueError, bitstream.put_byte, val)
def test_put_num_non_integer(self): """ Test that put_num raises an exception when passed non integer values as its num argument. """ bitstream = BitStream() self.assertRaises(TypeError, bitstream.put_num, "gato", 16) self.assertRaises(TypeError, bitstream.put_num, 0.1, 16) self.assertRaises(TypeError, bitstream.put_num, [1, 'a', 3], 16) self.assertRaises(TypeError, bitstream.put_num, object(), 16)
def test_put_num_non_integer_length(self): """ Test that put_num raises an exception when passed non integer values as its bit_length argument. """ bitstream = BitStream() self.assertRaises(TypeError, bitstream.put_num, 23, "gato") self.assertRaises(TypeError, bitstream.put_num, 23, 12.1) self.assertRaises(TypeError, bitstream.put_num, 23, [1, 'a', 3]) self.assertRaises(TypeError, bitstream.put_num, 23, object())
def test_put_byte_non_integer(self): """ Test that put_byte raises an exception when passed non integer values as its argument. """ bitstream = BitStream() self.assertRaises(TypeError, bitstream.put_byte, "gato") self.assertRaises(TypeError, bitstream.put_byte, 0.1) self.assertRaises(TypeError, bitstream.put_byte, [1, 'a', 3]) self.assertRaises(TypeError, bitstream.put_byte, object())
def test_string_unicode(self): """ Test basic put_string and get_string support for python unicode objects. """ bitstream = BitStream() unicode_string = u"ÄäÜüßЯБГДЖЙŁĄŻĘĆŃŚŹてすとアイウエオカキクケコサシスセソタチツテ" bitstream.put_string(unicode_string) bitstream.seek(0) # Note: the string is read back as a "normal" UTF-8 string, unicode # type information is not stored in the bitstream. self.assertEquals(bitstream.get_string(bitstream.get_length()), "ÄäÜüßЯБГДЖЙŁĄŻĘĆŃŚŹてすとアイウエオカキクケコサシスセソタチツテ")
def test_get_base64_zero_bits(self): """ Test that reading zero bits from the stream as base64 data results in getting the empty string: \"\". """ bitstream = BitStream() self.assertEquals(bitstream.get_base64(0), "") # Store some base64 data in the stream. bitstream.put_base64("Zm9vYmE=") # 40 bits bitstream.seek(0) self.assertEquals(bitstream.get_base64(0), "")
def _encrypted_data_as_bitstream(self): """ Returns the contents of this ciphertext as a BitStream object. This includes only the encrypted data (gamma and delta components), not the nbits and public key fingerprint metadata. The components are encoded alternating as follows: [gamma[0], delta[0], gamma[1], delta[1], ...] with each component represented as a nbits long number. Returns: bitstream::BitStream -- The gamma and delta components of this ciphertext as a bitstream. """ bitstream = BitStream() for i in range(0, self.get_length()): bitstream.put_num(self.gamma[i], self.nbits) bitstream.put_num(self.delta[i], self.nbits) return bitstream
def test_get_hex_zero_bits(self): """ Test that reading zero bits from the stream as hex data results in getting the empty string: \"\". """ bitstream = BitStream() self.assertEquals(bitstream.get_hex(0), "") # Store some hex data in the stream. bitstream.put_hex("DfF7CE69fF5478A") # 15 digits, 60 bits bitstream.seek(0) self.assertEquals(bitstream.get_hex(0), "")
def test_get_string_zero_bits(self): """ Test that reading zero bits from the stream as a string results in getting the empty string: \"\". """ bitstream = BitStream() self.assertEquals(bitstream.get_string(0), "") # Store a string in the stream. bitstream.put_string("Hello World!") bitstream.seek(0) self.assertEquals(bitstream.get_string(0), "")
def test_get_bit_dump_string_zero_bits(self): """ Test that reading zero bits from the stream as a bit dump string results in getting the empty string: \"\". """ bitstream = BitStream() self.assertEquals(bitstream.get_bit_dump_string(0), "") # Store some bits in the stream. bitstream.put_bit_dump_string("0001110101") # 10 bits bitstream.seek(0) self.assertEquals(bitstream.get_bit_dump_string(0), "")
def test_put_hex_invalid_format(self): """ Test that put_hex raises an exception when passed any string not of the form ('0'-'9'|'a'-'f'|'A'-'F')* """ bitstream = BitStream() # Try some fixed examples self.assertRaises(ValueError, bitstream.put_hex, "aF92G2") self.assertRaises(ValueError, bitstream.put_hex, "Hello") self.assertRaises(ValueError, bitstream.put_hex, "3354 F") self.assertRaises(ValueError, bitstream.put_hex, "ケコサシス") # Generate a random string conforming to ('0'-'9'|'a'-'f'|'A'-'F')* # except for one (ascii) character. valid_hex_digits = "0123456789abcdefABCDEF" num_digits = 50 pos_wrong_char = random.randint(1, 48) r_invalid_hex_string = "" for i in range(0, pos_wrong_char): r_invalid_hex_string += random.choice(valid_hex_digits) non_hex_char = "0" while (non_hex_char in valid_hex_digits): non_hex_char = \ random.choice(string.letters + string.digits + string.whitespace) r_invalid_hex_string += non_hex_char for i in range(0, num_digits - pos_wrong_char - 1): r_invalid_hex_string += random.choice(valid_hex_digits) # Try the randomly generated example self.assertRaises(ValueError, bitstream.put_hex, r_invalid_hex_string) # Calls that throw exceptions should not alter the contents or position # of the bitstream: self.assertEquals(bitstream.get_length(), 0) self.assertEquals(bitstream.get_current_pos(), 0)
def test_put_hex_invalid_format(self): """ Test that put_hex raises an exception when passed any string not of the form ('0'-'9'|'a'-'f'|'A'-'F')* """ bitstream = BitStream() # Try some fixed examples self.assertRaises(ValueError, bitstream.put_hex, "aF92G2") self.assertRaises(ValueError, bitstream.put_hex, "Hello") self.assertRaises(ValueError, bitstream.put_hex, "3354 F") self.assertRaises(ValueError, bitstream.put_hex, "ケコサシス") # Generate a random string conforming to ('0'-'9'|'a'-'f'|'A'-'F')* # except for one (ascii) character. valid_hex_digits = "0123456789abcdefABCDEF" num_digits = 50 pos_wrong_char = random.randint(1,48) r_invalid_hex_string = "" for i in range(0,pos_wrong_char): r_invalid_hex_string += random.choice(valid_hex_digits) non_hex_char = "0" while(non_hex_char in valid_hex_digits): non_hex_char = \ random.choice(string.letters + string.digits + string.whitespace) r_invalid_hex_string += non_hex_char for i in range(0,num_digits - pos_wrong_char - 1): r_invalid_hex_string += random.choice(valid_hex_digits) # Try the randomly generated example self.assertRaises(ValueError, bitstream.put_hex, r_invalid_hex_string) # Calls that throw exceptions should not alter the contents or position # of the bitstream: self.assertEquals(bitstream.get_length(),0) self.assertEquals(bitstream.get_current_pos(),0)
def test_seek_beyond_eos(self): """ Test that seeking beyond the end of the bitstream results in an exception raised. """ bitstream = BitStream() self.assertRaises(SeekOutOfRangeError, bitstream.seek, 1) # empty stream # An invalid seek(...) call should not move the position indicator. self.assertEqual(bitstream.get_current_pos(), 0) # Store a 64-bit integer in the stream. num = int('00000000000000110101011001010011' '00101100001000101000100110101111', 2) bitstream.put_num(num, 64) # Test seeking past 64 bits self.assertRaises(SeekOutOfRangeError, bitstream.seek, 65) self.assertRaises(SeekOutOfRangeError, bitstream.seek, random.randint(66,2**128)) # An invalid seek(...) call should not move the position indicator. self.assertEqual(bitstream.get_current_pos(), 64)
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_get_string_utf8_read_partial_characters(self): """ Test that it is possible to read "partial" utf-8 characters from the bitstream and that concatenation restores the full character/glyph. """ bitstream = BitStream() string = "てすとアイウエオカキクケコサシスセソタチツテ" # 22 chars, 66 bytes bitstream.put_string(string) bitstream.seek(0) # First read 32 bytes readString = bitstream.get_string(32 * 8) self.assertEquals(readString, "てすとアイウエオカキ\xef\xbd") # Now read the rest readString += bitstream.get_string(34 * 8) self.assertEquals(readString, "てすとアイウエオカキクケコサシスセソタチツテ")
def test_put_bit_dump_string_invalid_format(self): """ Test that put_bit_dump_string raises an exception when passed any string not of the form ('0'|'1')* """ bitstream = BitStream() # Try some fixed examples self.assertRaises(ValueError, bitstream.put_bit_dump_string, "23234") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "Hello") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "0101 0") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "ケコサシス") # Generate a random string conforming to ('0'|'1')* except for one # (ascii) character. num_bits = 50 pos_wrong_char = random.randint(1, 48) r_invalid_bit_string = "" for i in range(0, pos_wrong_char): r_invalid_bit_string += random.choice(('0', '1')) r_invalid_bit_string += \ random.choice(string.letters + "23456789_-/\\" + string.whitespace) for i in range(0, num_bits - pos_wrong_char - 1): r_invalid_bit_string += random.choice(('0', '1')) # Try the randomly generated example self.assertRaises(ValueError, bitstream.put_bit_dump_string, r_invalid_bit_string) # Calls that throw exceptions should not alter the contents or position # of the bitstream: self.assertEquals(bitstream.get_length(), 0) self.assertEquals(bitstream.get_current_pos(), 0)
def test_get_base64_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_base64(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_base64, 8) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0) bitstream.put_base64("Zm9vYmE=") # 40 bits bitstream.seek(0) # Read beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_base64, 48) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0)
def test_get_byte_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_byte(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_byte) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0) bitstream.put_byte(14) bitstream.seek(1) # Read a single bit beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_byte) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 1)
def test_get_hex_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_hex(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_hex, 1) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0) bitstream.put_hex("DfF7CE69fF5478A") # 15 digits, 60 bits bitstream.seek(0) # Read beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_hex, 61) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0)
def test_put_bit_dump_string_invalid_format(self): """ Test that put_bit_dump_string raises an exception when passed any string not of the form ('0'|'1')* """ bitstream = BitStream() # Try some fixed examples self.assertRaises(ValueError, bitstream.put_bit_dump_string, "23234") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "Hello") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "0101 0") self.assertRaises(ValueError, bitstream.put_bit_dump_string, "ケコサシス") # Generate a random string conforming to ('0'|'1')* except for one # (ascii) character. num_bits = 50 pos_wrong_char = random.randint(1,48) r_invalid_bit_string = "" for i in range(0,pos_wrong_char): r_invalid_bit_string += random.choice(('0','1')) r_invalid_bit_string += \ random.choice(string.letters + "23456789_-/\\" + string.whitespace) for i in range(0,num_bits - pos_wrong_char - 1): r_invalid_bit_string += random.choice(('0','1')) # Try the randomly generated example self.assertRaises(ValueError, bitstream.put_bit_dump_string, r_invalid_bit_string) # Calls that throw exceptions should not alter the contents or position # of the bitstream: self.assertEquals(bitstream.get_length(),0) self.assertEquals(bitstream.get_current_pos(),0)
def test_get_string_non_bytable_size(self): """ Test that calling get_string with a bit_length that is not a multiple of 8 raises an exception. That is, we cannot read partial bytes as string data. """ bitstream = BitStream() bitstream.put_string("Hello World!") bitstream.seek(0) self.assertRaises(ValueError, bitstream.get_string, 18) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(), 0)
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_get_string_utf8_read_partial_characters(self): """ Test that it is possible to read "partial" utf-8 characters from the bitstream and that concatenation restores the full character/glyph. """ bitstream = BitStream() string = "てすとアイウエオカキクケコサシスセソタチツテ" # 22 chars, 66 bytes bitstream.put_string(string) bitstream.seek(0) # First read 32 bytes readString = bitstream.get_string(32*8) self.assertEquals(readString,"てすとアイウエオカキ\xef\xbd") # Now read the rest readString += bitstream.get_string(34*8) self.assertEquals(readString,"てすとアイウエオカキクケコサシスセソタチツテ")
def test_get_byte_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_byte(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_byte) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0) bitstream.put_byte(14) bitstream.seek(1) # Read a single bit beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_byte) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),1)
def test_get_string_non_bytable_size(self): """ Test that calling get_string with a bit_length that is not a multiple of 8 raises an exception. That is, we cannot read partial bytes as string data. """ bitstream = BitStream() bitstream.put_string("Hello World!") bitstream.seek(0) self.assertRaises(ValueError, bitstream.get_string, 18) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0)
def test_get_string_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_string(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_string, 1) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0) bitstream.put_string("Hello World!") # 12 chars/bytes bitstream.seek(0) # Read beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_string, 13*8) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0)
def test_get_base64_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_base64(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_base64, 8) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0) bitstream.put_base64("Zm9vYmE=") # 40 bits bitstream.seek(0) # Read beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_base64, 48) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0)
def test_get_hex_beyond_eos(self): """ Test that trying to read beyond the end of the stream raises an exception when calling get_hex(...). """ bitstream = BitStream() self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_hex, 1) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0) bitstream.put_hex("DfF7CE69fF5478A") # 15 digits, 60 bits bitstream.seek(0) # Read beyond EOS self.assertRaises(NotEnoughBitsInStreamError, bitstream.get_hex, 61) # Current position should not have been changed self.assertEquals(bitstream.get_current_pos(),0)
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_bit_dump_string_basic(self): """ This method tests put_bit_dump_string's and get_bit_dump_string's basic functionality. """ bitstream = BitStream() # Generate a random string of bits, ie: ('0' | '1')* num_bits = 50 bits = "" for i in range(0,num_bits): bits += random.choice(('0','1')) # inefficient, but ok for a test. # Put those bits in the BitStream... bitstream.put_bit_dump_string(bits) # ...and get them back bitstream.seek(0) read_bits = bitstream.get_bit_dump_string(len(bits)) # Check that the bits were recovered correctly self.assertEqual(read_bits, bits)
def test_hex_basic(self): """ This method tests put_hex's and get_hex's basic functionality. """ bitstream = BitStream() # Generate a random string of hex digits, ie: ('0'-'9'|'a'-'f'|'A'-'F')* valid_hex_digits = "0123456789abcdefABCDEF" num_digits = 50 digits = "" for i in range(0,num_digits): digits += random.choice(valid_hex_digits) # Put those digits in the BitStream... bitstream.put_hex(digits) # ...and get them back bitstream.seek(0) read_digits = bitstream.get_hex(len(digits)*4) # 1 hex digit == 4 bits # Check that the hexadecimal digits were recovered correctly # Note that case information may be lost. Comparison must be case # insensitive (ie. 'a9Bc' and 'A9bC' are equal) self.assertEqual(read_digits.lower(), digits.lower())
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 decrypt_to_bitstream(self, ciphertext, task_monitor=None, force=False): """ Decrypts the given ciphertext into a bitstream. If the bitstream was originally encrypted with PublicKey.encrypt_X(), then this method returns a bitstream following the format described in Note 001 of the Ciphertext.py file: [size (64 bits) | message (size bits) | padding (X bits) ] Arguments: ciphertext::Ciphertext -- An encrypted Ciphertext object. task_monitor::TaskMonitor -- A task monitor for this task. force:bool -- Set this to true if you wish to force a decryption attempt, even when the ciphertext's stored public key fingerprint does not match that of the public key associated with this private key. Returns: bitstream::Bitstream -- A bitstream containing the unencrypted data. Throws: IncompatibleCiphertextError -- The given ciphertext does not appear to be decryptable with the selected private key. """ # Check that the public key fingerprint stored in the ciphertext # matches the public key associated with this private key. if(not force): if(ciphertext.nbits != self.cryptosystem.get_nbits()): raise IncompatibleCiphertextError("The given ciphertext is " \ "not decryptable with the selected private key: " \ "incompatible cryptosystem/key sizes.") if(ciphertext.pk_fingerprint != self.public_key.get_fingerprint()): raise IncompatibleCiphertextError("The given ciphertext is " \ "not decryptable with the selected private key: " \ "public key fingerprint mismatch.") # We read and decrypt the ciphertext block by block # See "Handbook of Applied Cryptography" Algorithm 8.18 bitstream = BitStream() block_size = self.cryptosystem.get_nbits() - 1 prime = self.cryptosystem.get_prime() key = self._key # Check if we have a task monitor and register with it if(task_monitor != None): # One tick per block ticks = ciphertext.get_length() decrypt_task_mon = \ task_monitor.new_subtask("Decrypt data", expected_ticks = ticks) for gamma, delta in ciphertext: assert max(gamma, delta) < 2**(block_size + 1), \ "The ciphertext object includes blocks larger than the " \ "expected block size." m = (pow(gamma, prime - 1 - key, prime) * delta) % prime bitstream.put_num(m, block_size) if(task_monitor != None): decrypt_task_mon.tick() return bitstream
def decrypt_to_bitstream(self, task_monitor=None): """ Decrypt the ciphertext to a bitstream, using the partial decryptions. At least (threshold) correctly generated partial decryptions for the ciphertext must be registered with this instance in order for decryption to succeed. Arguments: task_monitor::TaskMonitor -- A task monitor for this task. Returns: bitstream::Bitstream -- A bitstream containing the unencrypted data. Throws: InsuficientPartialDecryptionsError -- If there aren't enough partial decryptions registered with this object to perform combined decryption. """ # Get the indexes of all trustees for which we have a registered # partial decryption. We use 1 based indexes here. trustee_indexes = [] for trustee in range(1, self._num_trustees + 1): decryption = self._trustees_partial_decryptions[trustee - 1] if(decryption != None): trustee_indexes.append(trustee) # Check that we have enough trustees. if (len(trustee_indexes) < self._threshold): raise InsuficientPartialDecryptionsError("Not enough partial " \ "decryptions have been registered with this object to " \ "create a combined decryption. Registered partial " \ "decryptions: %d. Required partial decryptions " \ "(threshold): %d." \ % (len(trustee_indexes), self._threshold)) # We only need threshold trustees, exactly. Select those at random. random.shuffle(trustee_indexes) trustee_indexes = trustee_indexes[0:self._threshold] # We get the number of bits and prime for the cryptosystem nbits = self.cryptosystem.get_nbits() prime = self.cryptosystem.get_prime() # prime = 2q + 1 with q prime by construction (see EGCryptoSystem). q = (prime - 1) / 2 # See PublicKey.encrypt_bitstream for why we use nbits - 1 as the block # size. block_size = self.cryptosystem.get_nbits() - 1 # We initialize our bitstream bitstream = BitStream() # We pre-calculate the lagrange coefficients for the trustees in Z_{q} # for x = 0, to avoid doing so for each block of ciphertext. # See below for an explanation of the use of lagrange coefficients. lagrange_coeffs = [None for i in range(0,self._num_trustees + 1)] for trustee in trustee_indexes: lagrange_coeffs[trustee] = \ _lagrange_coefficient(trustee_indexes, trustee, 0, q) # For each block of partial decryption/(gamma, delta) pair of ciphertext for b_index in range(0, self._ciphertext.get_length()): # Each partial decryption block is of the form g^{rP(i)}, where # (gamma, delta) = (g^r, m*g^{r2P(0)}). # # We must first interpolate val=g^{r2P(0)} from the g^{rP(i)}'s. # Interpolation of val (LaTeX): # $g^{r2P(0)}=g^{\sum_{i\in I}r2P(i)\lambda_{i}(0)}= # \prod_{i\in I}\left(g^{rP\left(i\right)}\right)^{2\lambda_{i}(0)}$ # where \lambda_{i}(0) are the Lagrange Coefficients. # # Note that the polynomials are in the field Z_{q} where q is such # that p = 2*q + 1 and prime. This allows us to use lagrange # interpolation. However, in order for the whole g^{...} values to # be equal mod p, we need to have the exponents be equal mod (p-1) # (rather than mod q), so we multiply by 2. Remember that this # means that 2P(0) is our private key for the threshold encryption # (although it never gets created by any of the parties), and the # reason why g^2P(0) is the threshold public key. # # See (TODO: Add reference) for the full explanation gamma, delta = self._ciphertext[b_index] val = 1 for trustee in trustee_indexes: p_decryption = self._trustees_partial_decryptions[trustee - 1] # Get the value (g^{rP(i)}) of the partial decryption block. # Remember that each PartialDecryptionBlock is an object # containing both the value of the block and its proof of # partial decryption pd_block = p_decryption[b_index].value # We get \lambda_{i}(0) in the field Z_{q} for the trustees l_coeff = lagrange_coeffs[trustee] # We assert that the \lambda_{i}(0) was pre-calculated. assert (l_coeff != None), "lagrange coefficients for " \ "trustees in trustee_indexes " \ "should have been pre-calculated." # factor: $\left(g^{rP\left(i\right)}\right)^{2\lambda_{i}(0)}$ factor = pow(pd_block, 2*l_coeff, prime) val = (val*factor) % prime # We decrypt a block of message as m = delta/val = delta*(val)^{-1}. # (val)^{-1} the inverse of val in Z_{p} inv_val = pow(val, prime - 2, prime) m = (delta*inv_val) % prime # ... and add it to the bitstream. bitstream.put_num(m, block_size) # Return the decrypted bitstream return bitstream
def generate_commitment(self): """ Generate a ThresholdEncryptionCommitment towards the threshold scheme. Returns: commitment::ThresholdEncryptionCommitment Throws: ThresholdEncryptionSetUpStateError -- If public keys are not loaded. """ # 0. Verify that all public keys are available for 1-to-1 encryption. for trustee in range(0, self._num_trustees): pk = self._trustees_simple_public_keys[trustee] if(pk == None): raise ThresholdEncryptionSetUpStateError( "generate_commitment() must only be called after all the " \ "trustees' public keys have been registered with this " \ "ThresholdEncryptionSetUp instance. Missing public key " \ "for trustee %d." % trustee) # 1. Construct a new random polynomial of degree (threshold - 1) # Note: A polynomial of degree (t - 1) is determined by any t # (distinct) points. degree = self._threshold - 1 nbits = self.cryptosystem.get_nbits() prime = self.cryptosystem.get_prime() generator = self.cryptosystem.get_generator() # All calculations inside the polynomial are performed modulus q # where q is such that p = 2*q + 1 (q is prime because of how we # construct p when generating an EGCryptoSystem). # We do this, because the values of the polynomial coefficients or its # value at a certain point are always used as the exponent of elements # in Z_{p}^{*}, and we know: # a = b mod (p - 1) => g^a = g^b mod p # (because g^{a-b} = g^{x(p-1)} = (g^x)^{p-1} = 1 mod p) # We use q and not p - 1, because Z_{q} is a field (because q is # prime), while the same would not be true for p-1, and we need the # polynomials to be on a field in order to perform Lagrange # Interpolation (see ThresholdDecryptionCombinator.decrypt_to_X()). # This also means that 2*P(0) will be the threshold private key # (never actually seen directly by the parties), and g^(2*P(0)) # the threshold public key. # For the full explanation, see: (TODO: Add reference) q = (prime - 1) / 2 polynomial = \ CoefficientsPolynomial.new_random_polynomial(q, degree) # 2. Generate the public "coefficients" (actually g^coefficient for # each coefficient of the polynomial). public_coeficients = [] for coeff in polynomial.get_coefficients(): public_coeficients.append(pow(generator, coeff, prime)) # 3. Generate the partial private keys for each trustee. # The partial private key for trustee j is P_{i}(j+1), with i the # trustee generating the commitment, its full private key is the sum # of the P_{i}(j+1) values generated by all trustees (including its own). # We must add 1 to the trustee index, since we want the trustee # indexes in the polynomial to start on 1, while our lists are 0 # based. # (We must never reveal P(0) as this is the complete private key # of the full threshold scheme) # IMPORTANT: We encrypt each partial private key so that only its # intended recipient may read it. enc_partial_priv_keys = [] for trustee in range(0, self._num_trustees): ppriv_key = polynomial(trustee + 1) # P_{i}(j) trustee_pk = self._trustees_simple_public_keys[trustee] # Note that trustee public keys need not use the same cryptosystem # as the threshold encryption. In fact, they might not even have # the same bit length. bitstream = BitStream() bitstream.put_num(ppriv_key, nbits) ciphertext = trustee_pk.encrypt_bitstream(bitstream) enc_partial_priv_keys.append(ciphertext) # 4. Construct a ThresholdEncryptionCommitment object storing this # commitment and return it. return ThresholdEncryptionCommitment(self.cryptosystem, self._num_trustees, self._threshold, public_coeficients, enc_partial_priv_keys)