def unpack(content): if len(content) < ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH: raise ValueError('Ed25519 certificate was %i bytes, but should be at least %i' % (len(content), ED25519_HEADER_LENGTH + ED25519_SIGNATURE_LENGTH)) header, signature = split(content, len(content) - ED25519_SIGNATURE_LENGTH) version, header = Size.CHAR.pop(header) cert_type, header = Size.CHAR.pop(header) expiration_hours, header = Size.LONG.pop(header) key_type, header = Size.CHAR.pop(header) key, header = split(header, ED25519_KEY_LENGTH) extension_count, extension_data = Size.CHAR.pop(header) if version != 1: raise ValueError('Ed25519 v1 parser cannot read version %i certificates' % version) extensions = [] for i in range(extension_count): extension, extension_data = Ed25519Extension.pop(extension_data) extensions.append(extension) if extension_data: raise ValueError('Ed25519 certificate had %i bytes of unused extension data' % len(extension_data)) return Ed25519CertificateV1(cert_type, datetime.datetime.utcfromtimestamp(expiration_hours * 3600), key_type, key, extensions, signature)
def send(self, command, data = '', stream_id = 0): """ Sends a message over the circuit. :param stem.client.RelayCommand command: command to be issued :param bytes data: message payload :param int stream_id: specific stream this concerns :returns: **list** of :class:`~stem.client.cell.RelayCell` responses """ with self.relay._orport_lock: orig_digest = self.forward_digest.copy() orig_key = copy.copy(self.forward_key) # Digests and such are computed using the RELAY cell payload. This # doesn't include the initial circuit id and cell type fields. # Circuit ids vary in length depending on the protocol version. header_size = self.relay.link_protocol.circ_id_size.size + 1 try: cell = stem.client.cell.RelayCell(self.id, command, data, 0, stream_id) payload_without_digest = cell.pack(self.relay.link_protocol)[header_size:] self.forward_digest.update(payload_without_digest) cell = stem.client.cell.RelayCell(self.id, command, data, self.forward_digest, stream_id) header, payload = split(cell.pack(self.relay.link_protocol), header_size) encrypted_payload = header + self.forward_key.update(payload) reply_cells = [] self.relay._orport.send(encrypted_payload) reply = self.relay._orport.recv() # Check that we got the correct number of bytes for a series of RELAY cells relay_cell_size = header_size + stem.client.cell.FIXED_PAYLOAD_LEN relay_cell_cmd = stem.client.cell.RelayCell.VALUE if len(reply) % relay_cell_size != 0: raise stem.ProtocolError('Circuit response should be a series of RELAY cells, but received an unexpected size for a response: %i' % len(reply)) while reply: circ_id, reply = self.relay.link_protocol.circ_id_size.pop(reply) command, reply = Size.CHAR.pop(reply) payload, reply = split(reply, stem.client.cell.FIXED_PAYLOAD_LEN) if command != relay_cell_cmd: raise stem.ProtocolError('RELAY cell responses should be %i but was %i' % (relay_cell_cmd, command)) elif circ_id != self.id: raise stem.ProtocolError('Response should be for circuit id %i, not %i' % (self.id, circ_id)) decrypted = self.backward_key.update(payload) reply_cells.append(stem.client.cell.RelayCell._unpack(decrypted, self.id, self.relay.link_protocol)) return reply_cells except: self.forward_digest = orig_digest self.forward_key = orig_key raise
def _unpack(cls, content, circ_id, link_protocol): if len(content) < HASH_LEN * 2: raise ValueError('Key material and derivatived key should be %i bytes, but was %i' % (HASH_LEN * 2, len(content))) key_material, content = split(content, HASH_LEN) derivative_key, content = split(content, HASH_LEN) return CreatedFastCell(circ_id, derivative_key, key_material, content)
def _unpack( cls, content: bytes, circ_id: int, link_protocol: 'stem.client.datatype.LinkProtocol' ) -> 'stem.client.cell.CreatedFastCell': if len(content) < HASH_LEN * 2: raise ValueError( 'Key material and derivatived key should be %i bytes, but was %i' % (HASH_LEN * 2, len(content))) key_material, content = split(content, HASH_LEN) derivative_key, content = split(content, HASH_LEN) return CreatedFastCell(circ_id, derivative_key, key_material, content)
def _unpack( cls: Type['stem.client.cell.AuthChallengeCell'], content: bytes, circ_id: int, link_protocol: 'stem.client.datatype.LinkProtocol' ) -> 'stem.client.cell.AuthChallengeCell': min_size = AUTH_CHALLENGE_SIZE + Size.SHORT.size if len(content) < min_size: raise ValueError( 'AUTH_CHALLENGE payload should be at least %i bytes, but was %i' % (min_size, len(content))) challenge, content = split(content, AUTH_CHALLENGE_SIZE) method_count, content = Size.SHORT.pop(content) if len(content) < method_count * Size.SHORT.size: raise ValueError( 'AUTH_CHALLENGE should have %i methods, but only had %i bytes for it' % (method_count, len(content))) methods = [] for i in range(method_count): method, content = Size.SHORT.pop(content) methods.append(method) return AuthChallengeCell(methods, challenge, unused=content)
def pop( content: bytes, link_protocol: 'stem.client.datatype.LinkProtocol' ) -> Tuple['stem.client.cell.Cell', bytes]: """ Unpacks the first cell. :param content: payload to decode :param link_protocol: link protocol version :returns: (:class:`~stem.client.cell.Cell`, remainder) tuple :raises: * ValueError if content is malformed * NotImplementedError if unable to unpack this cell type """ link_protocol = LinkProtocol(link_protocol) circ_id, content = link_protocol.circ_id_size.pop(content) command, content = CELL_TYPE_SIZE.pop(content) cls = Cell.by_value(command) if cls.IS_FIXED_SIZE: payload_len = FIXED_PAYLOAD_LEN else: payload_len, content = PAYLOAD_LEN_SIZE.pop(content) if len(content) < payload_len: raise ValueError( '%s cell should have a payload of %i bytes, but only had %i' % (cls.NAME, payload_len, len(content))) payload, content = split(content, payload_len) return cls._unpack(payload, circ_id, link_protocol), content
def _unpack(cls, content, circ_id, link_protocol): key_material, unused = split(content, HASH_LEN) if len(key_material) != HASH_LEN: raise ValueError('Key material should be %i bytes, but was %i' % (HASH_LEN, len(key_material))) return CreateFastCell(circ_id, key_material, unused)
def encrypt(self, link_protocol, key, digest): """ Encrypts our cell content to be sent with the given key. This provides back a tuple of the form... :: (payload (bytes), new_key (CipherContext), new_digest (HASH)) :param int link_protocol: link protocol version :param cryptography.hazmat.primitives.ciphers.CipherContext key: key established with the relay we're sending this cell to :param hashlib.HASH digest: running digest held with the relay :returns: **tuple** with our encrypted payload and updated key/digest """ new_key = copy.copy(key) new_digest = digest.copy() # Digests are computed from our payload, not including our header's circuit # id (2 or 4 bytes) and command (1 byte). header_size = link_protocol.circ_id_size.size + 1 payload_without_digest = self.pack(link_protocol)[header_size:] new_digest.update(payload_without_digest) # Pack a copy of ourselves with our newly calculated digest, and encrypt # the payload. Header remains plaintext. cell = RelayCell(self.circ_id, self.command, self.data, new_digest, self.stream_id, self.recognized, self.unused) header, payload = split(cell.pack(link_protocol), header_size) return header + new_key.update(payload), new_key, new_digest
def _unpack( cls, content: bytes, circ_id: int, link_protocol: 'stem.client.datatype.LinkProtocol' ) -> 'stem.client.cell.CreateFastCell': key_material, unused = split(content, HASH_LEN) if len(key_material) != HASH_LEN: raise ValueError('Key material should be %i bytes, but was %i' % (HASH_LEN, len(key_material))) return CreateFastCell(circ_id, key_material, unused)
def _unpack(cls, content, circ_id, link_protocol): command, content = Size.CHAR.pop(content) recognized, content = Size.SHORT.pop(content) # 'recognized' field stream_id, content = Size.SHORT.pop(content) digest, content = Size.LONG.pop(content) data_len, content = Size.SHORT.pop(content) data, unused = split(content, data_len) if len(data) != data_len: raise ValueError('%s cell said it had %i bytes of data, but only had %i' % (cls.NAME, data_len, len(data))) return RelayCell(circ_id, command, data, digest, stream_id, recognized, unused)
def pop(content): if len(content) < 4: raise ValueError('Ed25519 extension is missing header fields') data_size, content = Size.SHORT.pop(content) ext_type, content = Size.CHAR.pop(content) flags, content = Size.CHAR.pop(content) data, content = split(content, data_size) if len(data) != data_size: raise ValueError("Ed25519 extension is truncated. It should have %i bytes of data but there's only %i." % (data_size, len(data))) return Ed25519Extension(ext_type, flags, data), content
def _recv(self, raw=False): """ Reads the next cell from our ORPort. If none is present this blocks until one is available. :param bool raw: provides bytes rather than parsing as a cell if **True** :returns: next :class:`~stem.client.cell.Cell` """ with self._orport_lock: # cells begin with [circ_id][cell_type][...] circ_id_size = self.link_protocol.circ_id_size.size while len(self._orport_buffer) < (circ_id_size + CELL_TYPE_SIZE.size): self._orport_buffer += self._orport.recv( ) # read until we know the cell type cell_type = Cell.by_value( CELL_TYPE_SIZE.pop(self._orport_buffer[circ_id_size:])[0]) if cell_type.IS_FIXED_SIZE: cell_size = circ_id_size + CELL_TYPE_SIZE.size + FIXED_PAYLOAD_LEN else: # variable length, our next field is the payload size while len(self._orport_buffer) < (circ_id_size + CELL_TYPE_SIZE.size + FIXED_PAYLOAD_LEN.size): self._orport_buffer += self._orport.recv( ) # read until we know the cell size payload_len = FIXED_PAYLOAD_LEN.pop( self._orport_buffer[circ_id_size + CELL_TYPE_SIZE.size:])[0] cell_size = circ_id_size + CELL_TYPE_SIZE.size + FIXED_PAYLOAD_LEN.size + payload_len while len(self._orport_buffer) < cell_size: self._orport_buffer += self._orport.recv( ) # read until we have the full cell if raw: content, self._orport_buffer = split(self._orport_buffer, cell_size) return content else: cell, self._orport_buffer = Cell.pop(self._orport_buffer, self.link_protocol) return cell
def send(self, command, data = '', stream_id = 0): """ Sends a message over the circuit. :param stem.client.datatype.RelayCommand command: command to be issued :param bytes data: message payload :param int stream_id: specific stream this concerns :returns: **list** of :class:`~stem.client.cell.RelayCell` responses """ with self.relay._orport_lock: # Encrypt and send the cell. Our digest/key only updates if the cell is # successfully sent. cell = stem.client.cell.RelayCell(self.id, command, data, stream_id = stream_id) payload, forward_key, forward_digest = cell.encrypt(self.relay.link_protocol, self.forward_key, self.forward_digest) self.relay._orport.send(payload) self.forward_digest = forward_digest self.forward_key = forward_key # Decrypt relay cells received in response. Again, our digest/key only # updates when handled successfully. reply = self.relay._orport.recv() reply_cells = [] if len(reply) % self.relay.link_protocol.fixed_cell_length != 0: raise stem.ProtocolError('Circuit response should be a series of RELAY cells, but received an unexpected size for a response: %i' % len(reply)) while reply: encrypted_cell, reply = split(reply, self.relay.link_protocol.fixed_cell_length) decrypted_cell, backward_key, backward_digest = stem.client.cell.RelayCell.decrypt(self.relay.link_protocol, encrypted_cell, self.backward_key, self.backward_digest) if self.id != decrypted_cell.circ_id: raise stem.ProtocolError('Response should be for circuit id %i, not %i' % (self.id, decrypted_cell.circ_id)) self.backward_digest = backward_digest self.backward_key = backward_key reply_cells.append(decrypted_cell) return reply_cells