def encode_P2SH_encoding(buf, key, valid_pubkey): txouts = [] txins = [] # First push needs to be skipped as it stores the iv, also we're going to # the txin side of the tx. chunk_ctrl = blockpop.ChunkControl(txout_txin_mode=0, skip_num=0, open_p2sh=1, pubkey_steganography=1) # Setup iv iv_push = b'iv' txouts.append(CTxOut(int(chunk_ctrl), CScript([OP_RETURN, iv_push]))) chunk_ctrl.skip_num = 0 chunk_generator = ChunkGenerator(io.BytesIO(buf), cipher=blockpop.AESCipher(key, Hash(iv_push)[0:16])) script_encoder = BareMultisig_ScriptEncoder(valid_pubkey_generator=lambda:[valid_pubkey], pubkey_steganography=True, min_length=33, max_length=33) while not chunk_generator.done: next_chunk_ctrl = blockpop.ChunkControl(txout_txin_mode=0, pubkey_steganography=1, end_script=1, open_p2sh=1) redeemScript = script_encoder(chunk_generator, next_chunk_ctrl) txin = CTxIn(COutPoint(), CScript([redeemScript])) txins.append(txin) return CTransaction(txins, txouts)
class P2SH_ScriptEncoder(ScriptEncoder): ctrl_flag_mask = blockpop.ChunkControl(skip_num=0, pubkey_steganography=0) required_ctrl_flags = blockpop.ChunkControl(skip_num=blockpop.ChunkControl.SKIP_NUM_MASK, pubkey_steganography=1) def __call__(self, chunk_generator, next_ctrl_byte): push = chunk_generator.get(20, 20, next_ctrl_byte) return CScript([OP_HASH160, push, OP_EQUALVERIFY, OP_CHECKSIG])
class OP_Return_ScriptEncoder(ScriptEncoder): ctrl_flag_mask = blockpop.ChunkControl(skip_num=0, pubkey_steganography=0) required_ctrl_flags = blockpop.ChunkControl(skip_num=blockpop.ChunkControl.SKIP_NUM_MASK, pubkey_steganography=1) def __init__(self, max_length=40): self.max_length = max_length def __call__(self, chunk_generator, next_ctrl_byte): data = chunk_generator.get(0, self.max_length, next_ctrl_byte) return CScript([OP_RETURN, data])
def encode_all_op_return(buf, key): txouts = [] # First push needs to be skipped as it stores the iv chunk_ctrl = blockpop.ChunkControl(txout_txin_mode=1, skip_num=1) # Setup iv iv_push = b'iv' txouts.append(CTxOut(int(chunk_ctrl), CScript([OP_RETURN, iv_push]))) chunk_ctrl.skip_num = 0 chunk_generator = ChunkGenerator(io.BytesIO(buf), cipher=blockpop.AESCipher(key, Hash(iv_push)[0:16])) script_encoder = OP_Return_ScriptEncoder() chunk_ctrl.pubkey_steganography = False while not chunk_generator.done: scriptPubKey = script_encoder(chunk_generator, chunk_ctrl) txout = CTxOut(0, scriptPubKey) txouts.append(txout) return CTransaction([], txouts)
def __call__(self, chunk_generator, next_chunk_ctrl): valid_pubkeys = list(self.valid_pubkey_generator()) pubkeys = [] chunk_ctrl = blockpop.ChunkControl(skip_num=0, pubkey_steganography=self.pubkey_steganography) while not chunk_generator.done and len(pubkeys) < self.max_pubkeys - len(valid_pubkeys): # The last data pubkey is handled specially using the requested # next_chunk_ctrl. if len(pubkeys) == self.max_pubkeys - len(valid_pubkeys) - 1: chunk_ctrl = next_chunk_ctrl # If the end_script bit hasn't been set we need to skip past the # non-valid pubkeys. if not chunk_ctrl.end_script: chunk_ctrl.skip_num += len(valid_pubkeys) chunk = chunk_generator.get(self.min_length, self.max_length, chunk_ctrl) pubkeys.append(chunk) pubkeys = valid_pubkeys + list(reversed(pubkeys)) return CScript([self.m] + pubkeys + [len(pubkeys), OP_CHECKMULTISIG])
def __init__(self, m=1, valid_pubkey_generator=lambda:(), max_pubkeys=3, min_length=33, max_length=65, pubkey_steganography=False): if pubkey_steganography: if not (min_length == max_length == 33): raise ValueError('pubkey_steganography requires min_length == max_length == 33') self.m = m self.valid_pubkey_generator = valid_pubkey_generator self.pubkey_steganography = 1 if pubkey_steganography else 0 self.min_length = min_length self.max_length = max_length self.max_pubkeys = max_pubkeys self.ctrl_flag_mask = blockpop.ChunkControl(skip_num=0, pubkey_steganography=self.pubkey_steganography) self.required_ctrl_flags = blockpop.ChunkControl(skip_num=blockpop.ChunkControl.SKIP_NUM_MASK, pubkey_steganography=1)
def test(self): chunk_ctrl = blockpop.ChunkControl() chunk_gen = blockpop.encode.ChunkGenerator(io.BytesIO(b'')) self.assertEqual(chunk_gen.get(0, 100, chunk_ctrl), b'\x80') self.assertEqual(chunk_gen.done, True) chunk_ctrl = blockpop.ChunkControl() chunk_gen = blockpop.encode.ChunkGenerator(io.BytesIO(b'abcd')) self.assertEqual(chunk_gen.get(0, 5, chunk_ctrl), b'\x00abcd') self.assertEqual(chunk_gen.done, False) self.assertEqual(chunk_gen.get(0, 5, chunk_ctrl), b'\x80') self.assertEqual(chunk_gen.done, True) chunk_ctrl = blockpop.ChunkControl(pubkey_steganography=1) chunk_gen = blockpop.encode.ChunkGenerator(io.BytesIO(b'abcd')) self.assertEqual( chunk_gen.get(33, 33, chunk_ctrl), b'\x02\xc7\x9aabcd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ) self.assertEqual(chunk_gen.done, True) chunk_ctrl = blockpop.ChunkControl(pubkey_steganography=1) chunk_gen = blockpop.encode.ChunkGenerator(io.BytesIO(b'\x00' * 31)) self.assertEqual( chunk_gen.get(33, 33, chunk_ctrl), b'\x03\xb1@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ) self.assertEqual(chunk_gen.done, False) self.assertEqual( chunk_gen.get(33, 33, chunk_ctrl), b'\x03)\x9d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ) self.assertEqual(chunk_gen.done, True)
def get(self, min_length, max_length, chunk_ctrl): """Get the next chunk min_length - Minimum length of the chunk; nothing smaller will be returned. max_length - Max length of the chunk, including control byte and pubkey/nonce (if applicable) chunk_ctrl - Current control byte. Will be updated as required. When the end of data is reached self.done is set to True """ # FIXME: change these asserts to something more friendly if min_length == 0: min_length = 1 assert 1 < max_length assert 0 < min_length <= max_length assert min_length <= 128 # max representable padding assert not self.done if chunk_ctrl.pubkey_steganography: assert min_length == max_length == 33 min_length -= 2 max_length -= 2 chunk = bytes(chunk_ctrl) + self.src_stream.read(max_length-1) # If we're at the end, pad if required. if len(chunk) < max_length-1: padding = b'\x00' * (min_length - len(chunk)) # The last chunk is handled specially. The 'done' flag is set and # the other 7 bits are used to encode the length of padding added. end_chunk_ctrl = blockpop.ChunkControl(done=1, padding_length=len(padding)) chunk = bytes(end_chunk_ctrl) + chunk[1:] + padding self.done = True encrypted_chunk = self.cipher.encrypt(chunk) if chunk_ctrl.pubkey_steganography: # Coerce the chunk into a valid-looking compressed pubkey using a # nonce. Valid pubkeys are uniformally distributed with 50% # probability, so we can just increment a nonce until we find a # valid one. # Create some deterministic random bytes to chose the pubkey prefix # and initial nonce. We could use random() here, but better to be # deterministic for the sake of unit tests. det_rand = Hash(encrypted_chunk + self.cipher.key + self.cipher.iv) # Either 0x02 or 0x03 pubkey_prefix = (det_rand[0] & 0b1) + 2 pubkey_nonce = initial_pubkey_nonce = det_rand[1] pubkey = bitcoin.core.key.CPubKey(b'') # invalid while not pubkey.is_fullyvalid: pubkey_nonce = (pubkey_nonce + 1) % 256 if pubkey_nonce == initial_pubkey_nonce: raise Exception("Ran out of nonce! This should not happen.") pubkey = bitcoin.core.key.CPubKey(bytes([pubkey_prefix, pubkey_nonce]) + encrypted_chunk) encrypted_chunk = bytes(pubkey) return encrypted_chunk