# Copyright 2014, 2015, Nik Kinkel and David Johnston
# See LICENSE for licensing information
import struct
import time
from oppy.cell.cell import Cell
import oppy.cell.definitions as DEF
from oppy.cell.exceptions import BadCellHeader, BadPayloadData
from oppy.cell.util import TLVTriple
[docs]class FixedLenCell(Cell):
'''A container class for representing fixed-length cells.'''
_subclass_map = None
def __init__(self, header, payload=None):
'''Create a :class:`~oppy.cell.fixedlen.FixedLenCell` with the
using *header*.
:param :class:`oppy.cell.fixedlen.FixedLenCell.Header` header: The
cell header information to be used.
:param str payload: Cell payload to be used.
'''
if not isinstance(header, FixedLenCell.Header):
msg = 'Expected cell header type FixedLenCell.Header, but '
msg += 'received header of type: {}'.format(type(header))
raise BadCellHeader(msg)
self.header = header
self.payload = payload
@staticmethod
[docs] def padCellBytes(cell_bytes, link_version=3):
'''Pad cell_bytes to uniform length.
Length depends on the Link Protocol version in use.
:param str cell_bytes: byte string to pad
:param int link_version: Link Protocol version in use
:returns: **str** cell_bytes padded to a fixed-length
'''
PAD_BYTE = '\x00'
if link_version <= 3:
pad_len = DEF.FIXED_LEN_V3_LEN
else:
pad_len = DEF.FIXED_LEN_V4_LEN
return cell_bytes + PAD_BYTE * (pad_len - len(cell_bytes))
[docs] def getBytes(self, trimmed=False):
'''Build and return the raw bytes this cell represents.
:param bool trimmed: ignored
:returns: **str** byte representation of this cell.
'''
return self.header.getBytes() + self.payload
[docs] def payloadRange(self):
'''Return a two-tuple representing the (start, end) positions of this
cell's payload data (based on Link Protocol version in use).
:returns: **tuple, int** (start, end) indices of payload.
'''
if 1 <= self.header.link_version <= 3:
return DEF.PAYLOAD_START_V3, DEF.FIXED_LEN_V3_LEN
elif self.header.link_version <= 4:
return DEF.PAYLOAD_START_V4, DEF.FIXED_LEN_V4_LEN
else:
fmt = "The cell's link version is invalid: {}"
raise ValueError(fmt.format(self.header.link_version))
def _parseHeader(self, data):
# This check is only useful for debugging purposes. For fixed-length
# cells, this method actually doesn't need to do anything, because
# all header fields have already been parsed.
already_parsed = (self.header.circ_id,
self.header.cmd,
self.header.link_version)
for field in already_parsed:
assert field is not None
def _parsePayload(self, data):
start, end = self.payloadRange()
self.payload = data[start:end]
@staticmethod
def _initSubclassMap():
FixedLenCell._subclass_map = {
DEF.PADDING_CMD : PaddingCell,
DEF.CREATE_CMD : CreateCell,
DEF.CREATED_CMD : CreatedCell,
DEF.RELAY_CMD : EncryptedCell,
DEF.DESTROY_CMD : DestroyCell,
DEF.CREATE_FAST_CMD : CreateFastCell,
DEF.CREATED_FAST_CMD: CreatedFastCell,
DEF.NETINFO_CMD : NetInfoCell,
DEF.RELAY_EARLY_CMD : EncryptedCell,
DEF.CREATE2_CMD : Create2Cell,
DEF.CREATED2_CMD : Created2Cell
}
@staticmethod
def _extractCmd(data, header):
return header.cmd
HTYPE_LEN = 2
HLEN_LEN = 2
[docs]class Create2Cell(FixedLenCell):
'''.. note:: tor-spec, Section 5.1'''
def __init__(self, header, htype=None, hlen=None, hdata=None):
'''
:param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
initialized header to use with this cell
:param int htype: Handshake type in use
:param int hlen: Length of the handshake data
:param str hdata: Actual handshake data to use (onion skin)
'''
self.header = header
self.htype = htype
self.hlen = hlen
self.hdata = hdata
@staticmethod
[docs] def make(circ_id, htype=DEF.NTOR_HTYPE, hlen=DEF.NTOR_HLEN, hdata='',
link_version=3):
'''Build and return a Create2 cell, using default values where
possible.
Automatically create and use an appropriate FixedLenCell.Header.
.. note: oppy only supports the NTor handshake, so *make()* will
currently reject any *htype*'s or *hlen*'s that are not
recognized as used in the NTor handshake.
:param int circ_id: Circuit ID to use for this cell
:param int hlen: Length of **hdata** segment
:param str hdata: Actual handshake data to use (an *onion skin*)
:param int link_version: Link Protocol version in use
:returns: :class:`~oppy.cell.fixedlen.Create2Cell`
'''
if htype != DEF.NTOR_HTYPE:
msg = '`htype` was {}, but we currently only can do '
msg += '{} (NTor)'
raise BadPayloadData(msg.format(htype, DEF.NTOR_HTYPE))
if hlen != DEF.NTOR_HLEN:
msg = '`htype` was NTor but hlen was {}, expected {}.'
raise BadPayloadData(msg.format(hlen, DEF.NTOR_HLEN))
if hlen != len(hdata):
msg = '`hlen` was {}, but `len(hdata)` was {}.'
raise BadPayloadData(msg.format(hlen, len(hdata)))
h = FixedLenCell.Header(circ_id=circ_id,
cmd=DEF.CREATE2_CMD,
link_version=link_version)
return Create2Cell(h, htype=htype, hlen=hlen, hdata=hdata)
[docs] def getBytes(self, trimmed=False):
'''Construct and return the byte string represented by this cell.
:param bool trimmed: If **True**, return just the cell bytes with no
padding. Otherwise, pad cell bytes out to fixed-length size
according to Link Protocol version in use.
:returns: **str** formatted byte string represented by this cell
'''
ret = self.header.getBytes()
ret += struct.pack('!H', self.htype) + struct.pack('!H', self.hlen)
ret += self.hdata
if trimmed is True:
return ret
else:
return FixedLenCell.padCellBytes(ret, self.header.link_version)
def _parsePayload(self, data):
'''Parse the string *data* and extract cell fields.
Set this cell's attributes from extracted values.
:param str data: string to parse
'''
start, end = self.payloadRange()
offset = start
if end - start < HTYPE_LEN + HLEN_LEN:
msg = "Create2Cell payload was not enough bytes to construct "
msg += "a valid Create2Cell."
raise BadPayloadData(msg)
self.htype = struct.unpack('!H', data[offset:offset + HTYPE_LEN])[0]
offset += HTYPE_LEN
self.hlen = struct.unpack('!H', data[offset:offset + HLEN_LEN])[0]
offset += HLEN_LEN
try:
self.hdata = data[offset:offset + self.hlen]
except IndexError:
msg = "Create2Cell hlen was specified to be {} bytes, but actual "
msg += "but hdata was {} bytes."
raise BadPayloadData(msg.format(self.hlen, len(data) - offset))
def __repr__(self):
fmt = '{}, htype={}, hlen={}, hdata={}'
fmt = 'Create2Cell({})'.format(fmt)
return fmt.format(repr(self.header), repr(self.htype),
repr(self.hlen), repr(self.hdata))
[docs]class Created2Cell(FixedLenCell):
'''.. note:: tor-spec, Section 5.1'''
def __init__(self, header, hlen=None, hdata=None):
'''
:param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
Initialized header to use in this cell.
:param int hlen: Length of this cell's hdata field
:param str hdata: Actual handshake data (*onion skin*)
'''
self.header = header
self.hlen = hlen
self.hdata = hdata
[docs] def getBytes(self, trimmed=False):
'''Construct and return the byte string represented by this cell.
:param bool trimmed: If **True**, return just the bytes without
padding. Otherwise, pad length out to fixed-length cell size
according to Link Protocol version in use.
:returns: **str** raw byte string this cell represents.
'''
ret = self.header.getBytes()
ret += struct.pack('!H', self.hlen)
ret += self.hdata
if trimmed is True:
return ret
else:
return FixedLenCell.padCellBytes(ret, self.header.link_version)
def _parsePayload(self, data):
'''Parse the string *data* and extract cell fields.
Set the attributes of this cell.
:param str data: string to parse
'''
start, _ = self.payloadRange()
offset = start
self.hlen = struct.unpack('!H', data[offset:offset + HLEN_LEN])[0]
offset += HLEN_LEN
self.hdata = data[offset:offset + self.hlen]
def __repr__(self):
fmt = '{}, hlen={}, hdata={}'
fmt = 'Created2Cell({})'.format(fmt)
return fmt.format(repr(self.header), repr(self.hlen),
repr(self.hdata))
[docs]class CreatedFastCell(FixedLenCell):
'''.. note:: Not Implemented.'''
def __init__(self, header):
raise NotImplementedError("Can't make CreatedFastCell yet.")
[docs]class CreatedCell(FixedLenCell):
'''.. note:: Not Implemented.'''
def __init__(self, header):
raise NotImplementedError("Can't make CreatedCell yet.")
[docs]class CreateFastCell(FixedLenCell):
'''.. note:: Not Implemented.'''
def __init__(self, header):
raise NotImplementedError("Can't make CreateFastCell yet.")
[docs]class CreateCell(FixedLenCell):
'''.. note:: Not Implemented.'''
def __init__(self, header):
raise NotImplementedError("Can't make CreateCell yet.")
REASON_LEN = 1
[docs]class DestroyCell(FixedLenCell):
'''.. note:: tor-spec, Section 5.4'''
def __init__(self, header, reason=None):
'''
:param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
Initialized header to use in this cell
:param int reason: Reason this DestroyCell was being sent.
'''
self.header = header
self.reason = reason
# DESTROY_NONE should always be sent forward to avoid leaking version
@staticmethod
[docs] def make(circ_id, reason=DEF.DESTROY_NONE, link_version=3):
'''Build and return a Destroy cell, using default values where
possible.
Automatically create and use an appropriate FixedLenCell.Header.
.. warning: reason 0 (DESTROY_NONE in oppy.cell.definitions)
should always be sent forward to avoid leaking version
information.
:param int circ_id: Circuit ID to use for this cell
:param int reason: Reason this DESTROY cell is being sent
:param int link_version: Link Protocol version in use
:returns: :class:`~oppy.cell.fixedlen.DestroyCell`
'''
h = FixedLenCell.Header(circ_id=circ_id,
cmd=DEF.DESTROY_CMD,
link_version=link_version)
if reason not in DEF.DESTROY_TRUNCATE_REASONS:
msg = 'Unrecognized DESTROY reason: {}'.format(reason)
raise BadPayloadData(msg)
return DestroyCell(h, reason=reason)
[docs] def getBytes(self, trimmed=False):
'''Construct and return the byte string represented by this cell.
:param bool trimmed: If **True**, return just the bytes without
padding. Otherwise, pad length out to fixed-length cell size
according to Link Protocol version in use.
:returns: **str** raw byte string this cell represents
'''
ret = self.header.getBytes()
ret += struct.pack('!B', self.reason)
if trimmed is True:
return ret
else:
return FixedLenCell.padCellBytes(ret, self.header.link_version)
def _parsePayload(self, data):
'''Parse the string *data* and extract cell fields.
Set this cell's attributes.
:param str data: string to parse
'''
start, _ = self.payloadRange()
self.reason = struct.unpack('!B', data[start:start + REASON_LEN])[0]
def __repr__(self):
fmt = '{}, reason={}'
fmt = 'DestroyCell({})'.format(fmt)
return fmt.format(repr(self.header), repr(self.reason))
[docs]class EncryptedCell(FixedLenCell):
'''
.. note::
EncryptedCell is not a defined cell type in tor-spec, but
we use it as a convenient way to represent RELAY cells or
RELAY_EARLY cells that have either been encrypted by oppy or
received from the network and have not been decrypted yet.
'''
def __init__(self, header, enc_payload=None):
'''
:param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
header to use with this cell
:param str enc_payload: encrypted payload for use with this cell
'''
self.header = header
self.enc_payload = enc_payload
@staticmethod
[docs] def make(circ_id, payload, link_version=3, early=False):
'''Build and return a Destroy cell, using default values where
possible.
Automatically create and use an appropriate FixedLenCell.Header. The
*early* parameter specifies whether we should send a RELAY cell or
a RELAY_EARLY cell.
.. warning::
RELAY_EARLY cells should always be used during circuit creation
to avoid certain classes of attacks. That is, whenever oppy
sends a relay EXTEND2 cell, it would be sent as a RELAY_EARLY
cell instead of a RELAY cell.
Reference: tor-spec, Section 5.6
.. note: *payload* field should be fully padded and equal to
maximum relay cell payload length (498).
:param int circ_id: Circuit ID to use for this cell
:param str payload: Payload bytes to use in this cell
:param int link_version: Link Protocol version in use
:param bool early: Dictate whether or not to use a RELAY_EARLY cell
:returns: :class:`~oppy.cell.fixedlen.EncryptedCell`
'''
if len(payload) != DEF.MAX_PAYLOAD_LEN:
msg = 'EncryptedCell enc_payload should be padding to length {}; '
msg += 'found enc_payload length {} instead.'
msg = msg.format(DEF.FIXED_LEN_V3_LEN, len(payload))
raise BadPayloadData(msg)
cmd = DEF.RELAY_EARLY_CMD if early is True else DEF.RELAY_CMD
h = FixedLenCell.Header(circ_id=circ_id, cmd=cmd,
link_version=link_version)
return EncryptedCell(h, enc_payload=payload)
[docs] def getBytes(self, trimmed=False):
'''Construct and return the byte string represented by this cell.
:param bool trimmed: ignored, encrypted cell's don't know anything
about their payload or its length
:returns: **str** raw byte string this cell represents
'''
return self.header.getBytes() + self.enc_payload
def _parsePayload(self, data):
'''Parse the string *data* and extract cell fields.
.. note::
EncryptedCell does not try to interpret the payload,
assuming that it is encrypted and unreadable and will be
decrypted and parsed somewhere else.
:param str data: string to parse
'''
start, end = self.payloadRange()
self.enc_payload = data[start:end]
def __repr__(self):
fmt = "EncryptedCell({}, enc_payload={})"
return fmt.format(repr(self.header), repr(self.enc_payload))
TIMESTAMP_LEN = 4
NUM_ADDRESSES_LEN = 1
[docs]class NetInfoCell(FixedLenCell):
'''.. note:: tor-spec.txt, Section 4.5'''
def __init__(self, header, timestamp=None, other_or_address=None,
num_addresses=None, this_or_addresses=None):
'''
.. note: Addresses here are represented as type/length/value
structures, defined in :class:`~oppy.cell.util.TLVTriple`.
Reference: tor-spec.txt
Section: 6.4
:param :class:`~oppy.cell.fixedlen.FixedLenCell.Header` header:
header to use with this cell
:param str timestamp: Time this NetInfoCell was created. Big-endian
unsigned integer of seconds since the Unix epoch (in packed
byte format).
:param :class:`~oppy.cell.util.TLVTriple` other_or_address: Remote
address associated with this NetInfoCell. If we are the
initiator of this cell, this is the relay's address that we're
communicating with. If we are the recipient, this is our public
IP address.
:param int num_addresses: the number of this_or_addresses included
in this NetInfoCell.
:param list, `~oppy.cell.util.TLVTriple` this_or_addresses: List of
originating public IP addresses of this NetInfoCell.
'''
self.header = header
self.timestamp = timestamp
self.other_or_address = other_or_address
self.num_addresses = num_addresses
self.this_or_addresses = this_or_addresses
@staticmethod
[docs] def make(circ_id, other_or_address, this_or_addresses, timestamp=None,
link_version=3):
'''Build and return a Destroy cell, using default values where
possible.
Automatically create and use an appropriate FixedLenCell.Header.
:param int circ_id: Circuit ID to use for this cell
:param str timestamp: Time this `NetInfoCell` was created. Big-endian
unsigned integer of seconds since the Unix epoch (packed
format).
:param oppy.cell.util.TLVTriple other_or_address: Public IP
address of the recipient of this NetInfoCell.
:param list, oppy.cell.util.TLVTriple this_or_addresses: List
of the public IP address(es) of the originator of this
NetInfoCell.
:returns: :class:`~oppy.cell.fixedlen.NetInfoCell`
'''
h = FixedLenCell.Header(circ_id=circ_id,
cmd=DEF.NETINFO_CMD,
link_version=link_version)
if timestamp is None:
timestamp = struct.pack('!I', int(time.time()))
return NetInfoCell(h, timestamp=timestamp,
other_or_address=other_or_address,
num_addresses=len(this_or_addresses),
this_or_addresses=this_or_addresses)
[docs] def getBytes(self, trimmed=False):
'''Construct and return the byte string represented by this cell.
:param bool trimmed: Whether or not we should pad this cell's bytes.
If **True**, pad based on Link Protocol version in use.
:returns: **str** raw bytes this cell represents
'''
ret = self.header.getBytes()
ret += self.timestamp
ret += self.other_or_address.getBytes()
ret += struct.pack('!B', self.num_addresses)
for TLVaddr in self.this_or_addresses:
ret += TLVaddr.getBytes()
if trimmed is True:
return ret
else:
return FixedLenCell.padCellBytes(ret, self.header.link_version)
def _parsePayload(self, data):
'''Parse the string data and extract cell fields.
:param str data: string to parse
'''
start, _ = self.payloadRange()
offset = start
self.timestamp = data[offset:offset + TIMESTAMP_LEN]
offset += TIMESTAMP_LEN
self.other_or_address = TLVTriple.parse(data, offset)
offset += len(self.other_or_address)
self.num_addresses = data[offset:offset + NUM_ADDRESSES_LEN]
offset += NUM_ADDRESSES_LEN
n = struct.unpack('!B', self.num_addresses)[0]
self.this_or_addresses = []
for i in xrange(n):
t = TLVTriple.parse(data, offset)
self.this_or_addresses.append(t)
offset += len(t)
def __repr__(self):
fmt = '{}, timestamp={}, other_or_addresses={}, '
fmt += 'num_addresses={}, this_or_addresses={}'
fmt = 'NetInfoCell({})'.format(fmt)
return fmt.format(repr(self.header), repr(self.timestamp),
repr(self.other_or_address),
repr(self.num_addresses),
repr(self.this_or_addresses))
[docs]class PaddingCell(FixedLenCell):
'''.. note:: tor-spec, Section 3, 7.2.
.. note:: Padding has no cell payload fields so, we just use inherited
fields.
'''
pass