def handle_observe(self, path, request): plen = len(path) if plen == 1: obs = f'observe_{path[0]}' elif plen == 2: obs = f'observe_{path[0]}_{path[1]}' elif plen == 3: obs = f'observe_{path[0]}_{path[1]}_{path[2]}' else: return Message(code=Code.BAD_REQUEST) def _notifier(): self.updated_state(response=self.encoder.encode(path)) try: obs_method = eval(obs) cancel = request.opt.observe == '0' _kwargs = dict(model=self.model, path=path, payload=request.payload, content_format=request.opt.content_format, cancel=cancel, notifier=_notifier) obs_method(None, **_kwargs) return self.encoder.encode(path) except NameError: pass return Message(code=Code.METHOD_NOT_ALLOWED)
def _split_message(self, message): """Given a protected message, return the outer message that contains all Class I and Class U options (but without payload or Object-Security option), and a proto-inner message that contains all Class E options.""" # not trying to preserve token or mid, they're up to the transport outer_message = Message(code=message.code) inner_message = message.copy() if inner_message.code.is_request(): protected_uri = inner_message.get_request_uri() if protected_uri.count('/') >= 3: protected_uri = protected_uri[:protected_uri.index( '/', protected_uri.index('/', protected_uri.index('/') + 1) + 1)] outer_message.set_request_uri(protected_uri) if inner_message.opt.proxy_uri: # pack them into the separatable fields ... hopefully (FIXME # use a set_request_uri that *never* uses Proxy-Uri) inner_message.set_request_uri(protected_uri) inner_message.opt.uri_host = None inner_message.opt.uri_port = None inner_message.opt.proxy_uri = None inner_message.opt.proxy_scheme = None outer_message.opt.observe = inner_message.opt.observe if inner_message.code.is_response(): inner_message.opt.observe = None return outer_message, inner_message
def encode_resource(model, obj, inst, res): if not model.is_resource_readable(obj, inst, res): return Message(code=Code.METHOD_NOT_ALLOWED) _payload = TlvEncoder._resource_to_tlv(model, obj, inst, res) logger.debug("encode_resource(): %s" % hexdump(_payload, result="return")) msg = Message(code=Code.CONTENT, payload=_payload) msg.opt.content_format = MediaType.TLV.value return msg
def _split_message(self, message): """Given a protected message, return the outer message that contains all Class I and Class U options (but without payload or Object-Security option), and a proto-inner message that contains all Class E options. This leaves the messages' remotes unset.""" if message.code.is_request(): outer_host = message.opt.uri_host proxy_uri = message.opt.proxy_uri inner_message = message.copy( uri_host=None, uri_port=None, proxy_uri=None, proxy_scheme=None, ) inner_message.remote = None if proxy_uri is not None: # Use set_request_uri to split up the proxy URI into its # components; extract, preserve and clear them. inner_message.set_request_uri(proxy_uri, set_uri_host=False) if inner_message.opt.proxy_uri is not None: raise ValueError("Can not split Proxy-URI into options") outer_uri = inner_message.remote.uri_base inner_message.remote = None inner_message.opt.proxy_scheme = None if message.opt.observe is None: outer_code = POST else: outer_code = FETCH else: outer_host = None proxy_uri = None inner_message = message.copy() outer_code = CHANGED # no max-age because these are always successsful responses outer_message = Message( code=outer_code, uri_host=outer_host, observe=None if message.code.is_response() else message.opt.observe, ) if proxy_uri is not None: outer_message.set_request_uri(outer_uri) return outer_message, inner_message
def _split_message(self, message): """Given a protected message, return the outer message that contains all Class I and Class U options (but without payload or Object-Security option), and a proto-inner message that contains all Class E options. This leaves the messages' remotes unset.""" if message.code.is_request(): outer_host = message.opt.uri_host proxy_uri = message.opt.proxy_uri inner_message = message.copy( uri_host=None, uri_port=None, proxy_uri=None, proxy_scheme=None, ) inner_message.remote = None if proxy_uri is not None: # Use set_request_uri to split up the proxy URI into its # components; extract, preserve and clear them. inner_message.set_request_uri(proxy_uri, set_uri_host=False) if inner_message.opt.proxy_uri is not None: raise ValueError("Can not split Proxy-URI into options") outer_uri = inner_message.remote.uri_base inner_message.remote = None inner_message.opt.proxy_scheme = None if message.opt.observe is None: outer_code = POST else: outer_code = FETCH else: outer_host = None proxy_uri = None inner_message = message.copy() outer_code = CHANGED # no max-age because these are always successsful responses outer_message = Message(code=outer_code, uri_host=outer_host, observe=None if message.code.is_response() else message.opt.observe, ) if proxy_uri is not None: outer_message.set_request_uri(outer_uri) return outer_message, inner_message
def encode(self, path): if not self.model.is_path_valid(path): return Message(code=Code.NOT_FOUND) path_len = len(path) if path_len == 1: # read on whole object (TLV) return TlvEncoder.encode_object(self.model, path[0]) elif path_len == 2: # read on instance (TLV) return TlvEncoder.encode_instance(self.model, path[0], path[1]) elif path_len == 3: return TlvEncoder.encode_resource(self.model, path[0], path[1], path[2]) else: return Message(code=Code.BAD_REQUEST)
def run(self): self.context = yield from Context.create_server_context(self, bind=("::", 0)) # send POST (registration) request = Message(code=Code.POST, payload=",".join( self.model.get_object_links()).encode()) request.opt.uri_host = self.server request.opt.uri_port = self.server_port request.opt.uri_path = ("rd", ) request.opt.uri_query = ("ep=%s" % self.endpoint, "b=%s" % self.binding_mode, "lt=%d" % self.lifetime) response = yield from self.context.request(request).response # expect ACK if response.code != Code.CREATED: raise BaseException( "unexpected code received: %s. Unable to register!" % response.code) # we receive resource path ('rd', 'xyz...') self.rd_resource = response.opt.location_path[1].decode() log.info("client registered at location %s" % self.rd_resource) yield from asyncio.sleep(self.lifetime - 1) asyncio.ensure_future(self.update_register())
def to_message(self): inner = Message( code=UNAUTHORIZED, echo=self.echo, ) outer, _ = self.secctx.protect(inner, request_id=self.request_id) return outer
async def run(self): self.context = await Context.create_server_context(self, bind=('::', 0)) # send POST (registration) request = Message(code=Code.POST, payload=','.join( self.model.get_object_links()).encode()) request.opt.uri_host = self.server request.opt.uri_port = self.server_port request.opt.uri_path = ('rd', ) request.opt.uri_query = (f'ep={self.endpoint}', f'b={self.binding_mode}', f'lt={self.lifetime}') response = await self.context.request(request).response # expect ACK if response.code != Code.CREATED: raise BaseException( f'unexpected code received: {response.code}. Unable to register!' ) # we receive resource path ('rd', 'xyz...') self.rd_resource = response.opt.location_path[1] log.info(f'client registered at location {self.rd_resource}') await asyncio.sleep(self.lifetime - 1) asyncio.ensure_future(self.update_register())
def encode_instance(model, obj, inst): _buf = bytearray() for res in model.resources(obj, inst): if model.is_resource_readable(obj, inst, res): _buf.extend(TlvEncoder._resource_to_tlv(model, obj, inst, res)) logger.debug("encode_instance(): %s" % hexdump(_buf, result="return")) msg = Message(code=Code.CONTENT, payload=_buf) msg.opt.content_format = MediaType.TLV.value return msg
def add_reply(self, rawdata, dst_ipv4): jobdata = Message.decode(rawdata) int_unpack = unpack("ii", jobdata.payload[0:8]) count = int_unpack[0] job_id = int_unpack[1] params = jobdata.payload[8:] for j in self.job_list: if j.ipv4 == dst_ipv4 and j.job_id == job_id: j.count += 1 print('get job reply for ', j.ipv4, j.job_id, j.count, "the packet in packet_list", len(j.pkt))
def handle_exec(self, path, request): if len(path) != 3 or not self.model.is_path_valid(path): return Message(code=Code.BAD_REQUEST) if not self.model.is_resource_executable(path[0], path[1], path[2]): return Message(code=Code.METHOD_NOT_ALLOWED) _op = str(self.model.resource(path[0], path[1], path[2])) try: _op_method = eval(_op) except NameError: log.error( f'handler "{_op}" for {"/".join(path)} is not implemented. Please implement it in handlers.py' ) return Message(code=Code.NOT_IMPLEMENTED) _kwargs = dict(model=self.model, payload=request.payload, path=path, content_format=request.opt.content_format) result = _op_method(None, **_kwargs) return Message(code=Code.CHANGED, payload=result) if result is not None else Message( code=Code.CHANGED)
def encode_resource(model, obj, inst, res): if not model.is_resource_multi_instance(obj, inst, res): if model.is_resource_readable(obj, inst, res): # single resource queries are returned as TEXT (plain) _r = model.resource(obj, inst, res) _payload = str(_r).encode() logging.debug("encode_resource(): %s" % hexdump(_payload, result="return")) return Message(code=Code.CONTENT, payload=_payload) else: return Message(code=Code.METHOD_NOT_ALLOWED) else: # multi-resource if not model.is_resource_readable(obj, inst, res): return Message(code=Code.METHOD_NOT_ALLOWED) _payload = TlvEncoder._resource_to_tlv(model, obj, inst, res) logging.debug("encode_resource(): %s" % hexdump(_payload, result="return")) msg = Message(code=Code.CONTENT, payload=_payload) msg.opt.content_format = MediaType.TLV.value return msg
def encode_object(model, obj): if model.is_object_multi_instance(obj): _buf = bytearray() for inst in model.instances(obj): _buf.extend(TlvEncoder._instance_to_tlv(model, obj, inst)) logging.debug(f'encode_object(): {hexdump(_buf, result="return")}') msg = Message(code=Code.CONTENT, payload=_buf) msg.opt.content_format = MediaType.TLV.value return msg else: # directly encode resources _inst = model.instances(obj)[0] _buf = bytearray() for res in model.resources(obj, _inst): if model.is_resource_readable(obj, _inst, res): _buf.extend( TlvEncoder._resource_to_tlv(model, obj, _inst, res)) logging.debug(f'encode_object(): {hexdump(_buf, result="return")}') msg = Message(code=Code.CONTENT, payload=_buf) msg.opt.content_format = MediaType.TLV.value return msg
def decode(self, path, payload, content_format): if not self.model.is_path_valid(path): return Message(code=Code.NOT_FOUND), None try: if content_format == MediaType.TLV.value: return Message(code=Code.CHANGED), TlvDecoder.decode( self.model, path, payload) elif content_format == MediaType.TEXT.value: if len(path) != 3 or self.model.is_resource_multi_instance( path[0], path[1], path[2]): raise Exception( "TEXT format should only be used for single non-multiple resource" ) return Message(code=Code.CHANGED), TextDecoder.decode( self.model, path, payload) else: raise Exception("unsupported content format: %s" % content_format) except DecoderException as e: return Message(code=Code.BAD_REQUEST, payload=e.message.encode()), None
async def update_register(self): logger.debug("update_register()") update = Message(code=Code.POST, uri=self.uri) update.opt.uri_path = ("rd", self.rd_resource) response = await self.context.request(update).response if response.code != Code.CHANGED: # error while update, fallback to re-register logger.warn( "failed to update registration, code {}, falling back to registration" .format(response.code)) asyncio.ensure_future(self.run()) else: logger.info("updated registration for %s" % self.rd_resource) # yield to next update - 1 sec await asyncio.sleep(self.lifetime - 1) asyncio.ensure_future(self.update_register())
def _split_message(self, message): """Given a protected message, return the outer message that contains all Class I and Class U options (but without payload or Object-Security option), and a proto-inner message that contains all Class E options.""" inner_message = message.copy() if message.code.is_request(): outer_uri = message.get_request_uri() if outer_uri.count('/') >= 3: outer_uri = outer_uri[:outer_uri.index( '/', outer_uri.index('/', outer_uri.index('/') + 1) + 1)] inner_message = message.copy( # explicitly passing the .uri so that it gets split up; # FIXME make sure it always is, even in exotic schemes uri=message.get_request_uri(), uri_host=None, uri_port=None, proxy_uri=None, proxy_scheme=None, ) if message.opt.observe is None: outer_code = POST else: outer_code = FETCH else: outer_uri = None inner_message = message.copy() outer_code = CHANGED outer_message = Message( code=outer_code, uri=outer_uri, observe=None if message.code.is_response() else message.opt.observe, max_age=0 if message.code.is_response() and message.opt.observe is not None else None, ) return outer_message, inner_message
async def update_register(self): log.debug('update_register()') update = Message(code=Code.POST) update.opt.uri_host = self.server update.opt.uri_port = self.server_port update.opt.uri_path = ('rd', self.rd_resource) response = await self.context.request(update).response if response.code != Code.CHANGED: # error while update, fallback to re-register log.warning( f'failed to update registration, code {response.code}, falling back to registration' ) asyncio.ensure_future(self.run()) else: log.info(f'updated registration for {self.rd_resource}') # yield to next update - 1 sec await asyncio.sleep(self.lifetime - 1) asyncio.ensure_future(self.update_register())
async def run(self): self.context = await Context.create_server_context(self ) #bind=("::", 0)) if "coaps" in self.uri: if not "tinydtls" in list(defaults.get_default_clienttransports()): raise BaseException("DTLS is not installed!") creds = { self.uri + "/*": { "dtls": { "psk": self.key.encode("utf-8"), "client-identity": self.identity.encode("utf-8") } } } logger.debug("Credentials: %s" % str(creds)) self.context.client_credentials.load_from_dict(creds) # send POST (registration) request = Message(code=Code.POST, uri=self.uri, payload=",".join( self.model.get_object_links()).encode()) request.opt.uri_path = ("rd", ) request.opt.uri_query = ("lwm2m=1.0", "ep=%s" % self.endpoint, "b=%s" % self.binding_mode, "lt=%d" % self.lifetime) response = await self.context.request(request).response # expect ACK if response.code != Code.CREATED: raise BaseException( "unexpected code received: %s. Unable to register!" % response.code) # we receive resource path ('rd', 'xyz...') self.rd_resource = response.opt.location_path[1] logger.info("client registered at location %s" % self.rd_resource) await asyncio.sleep(self.lifetime - 1) asyncio.ensure_future(self.update_register())
def add_job(self, rawdata, src_mac, src_ipv4, src_port): jobdata = Message.decode(rawdata) int_unpack = unpack("ii", jobdata.payload[0:8]) count = int_unpack[0] job_id = int_unpack[1] params = jobdata.payload[8:] if count == 0: job1 = job(job_id=job_id, mac=src_mac, ipv4=src_ipv4, port=src_port) job1.add_pkt(params, rawdata) self.delete_same_id_job(job_id=job_id, ipv4=src_ipv4) self.check_job() # change this to check self.job_list.append(job1) print('get job ir packet from', job1.mac, job1.job_id) else: for j in self.job_list: if j.ipv4 == src_ipv4 and j.job_id == job_id: j.add_pkt(params, rawdata) print('get job cr packet from', j.ipv4, j.job_id)
def unprotect(self, protected_message, request_id=None): assert (request_id is not None) == protected_message.code.is_response() protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0( protected_message) if protected: raise ProtectionInvalid("The protected field is not empty") # FIXME check for duplicate keys in protected if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id: # for most cases, this is caught by the session ID dispatch, but in # responses (where explicit sender IDs are atypical), this is a # valid check raise ProtectionInvalid("Sender ID does not match") if COSE_PIV not in unprotected: if request_id is None: raise ProtectionInvalid( "No sequence number provided in request") nonce = request_id.nonce seqno = None # sentinel for not striking out anyting else: partial_iv_short = unprotected[COSE_PIV] seqno = int.from_bytes(partial_iv_short, 'big') if not self.recipient_replay_window.is_valid(seqno): # If here we ever implement something that accepts memory loss # as in 7.5.2 ("Losing Part of the Context State" / "Replay # window"), or an optimization that accepts replays to avoid # storing responses for EXCHANGE_LIFETIM, can_reuse_nonce a few # lines down needs to take that into consideration. raise ReplayError("Sequence number was re-used") nonce = self._construct_nonce(partial_iv_short, self.recipient_id) if request_id is None: # ie. we're unprotecting a request request_id = RequestIdentifiers( self.recipient_id, partial_iv_short, nonce, can_reuse_nonce=self.is_unicast) # FIXME is it an error for additional data to be present in unprotected? if len( ciphertext ) < self.algorithm.tag_bytes + 1: # +1 assures access to plaintext[0] (the code) raise ProtectionInvalid("Ciphertext too short") enc_structure = [ 'Encrypt0', protected_serialized, self._extract_external_aad(protected_message, request_id.kid, request_id.partial_iv) ] aad = cbor.dumps(enc_structure) plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key, nonce) if seqno is not None: self.recipient_replay_window.strike_out(seqno) # FIXME add options from unprotected unprotected_message = Message(code=plaintext[0]) unprotected_message.payload = unprotected_message.opt.decode( plaintext[1:]) if unprotected_message.code.is_request(): if protected_message.opt.observe != 0: unprotected_message.opt.observe = None else: if protected_message.opt.observe is not None: # -1 ensures that they sort correctly in later reordering # detection. Note that neither -1 nor high (>3 byte) sequence # numbers can be serialized in the Observe option, but they are # in this implementation accepted for passing around. unprotected_message.opt.observe = -1 if seqno is None else seqno return unprotected_message, request_id
def unprotect(self, protected_message, request_id=None): assert (request_id is not None) == protected_message.code.is_response() protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0(protected_message) if protected: raise ProtectionInvalid("The protected field is not empty") # FIXME check for duplicate keys in protected if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id: # for most cases, this is caught by the session ID dispatch, but in # responses (where explicit sender IDs are atypical), this is a # valid check raise ProtectionInvalid("Sender ID does not match") if COSE_PIV not in unprotected: if request_id is None: raise ProtectionInvalid("No sequence number provided in request") nonce = request_id.nonce seqno = None # sentinel for not striking out anyting else: partial_iv_short = unprotected[COSE_PIV] seqno = int.from_bytes(partial_iv_short, 'big') if not self.recipient_replay_window.is_valid(seqno): # If here we ever implement something that accepts memory loss # as in 7.5.2 ("Losing Part of the Context State" / "Replay # window"), or an optimization that accepts replays to avoid # storing responses for EXCHANGE_LIFETIM, can_reuse_nonce a few # lines down needs to take that into consideration. raise ReplayError("Sequence number was re-used") nonce = self._construct_nonce(partial_iv_short, self.recipient_id) if request_id is None: # ie. we're unprotecting a request request_id = RequestIdentifiers(self.recipient_id, partial_iv_short, nonce, can_reuse_nonce=self.is_unicast) # FIXME is it an error for additional data to be present in unprotected? if len(ciphertext) < self.algorithm.tag_bytes + 1: # +1 assures access to plaintext[0] (the code) raise ProtectionInvalid("Ciphertext too short") enc_structure = ['Encrypt0', protected_serialized, self._extract_external_aad(protected_message, request_id.kid, request_id.partial_iv)] aad = cbor.dumps(enc_structure) plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key, nonce) if seqno is not None: self.recipient_replay_window.strike_out(seqno) # FIXME add options from unprotected unprotected_message = Message(code=plaintext[0]) unprotected_message.payload = unprotected_message.opt.decode(plaintext[1:]) if unprotected_message.code.is_request(): if protected_message.opt.observe != 0: unprotected_message.opt.observe = None else: if protected_message.opt.observe is not None: # -1 ensures that they sort correctly in later reordering # detection. Note that neither -1 nor high (>3 byte) sequence # numbers can be serialized in the Observe option, but they are # in this implementation accepted for passing around. unprotected_message.opt.observe = -1 if seqno is None else seqno return unprotected_message, request_id
def unprotect(self, protected_message, request_data=None): assert (request_data is not None) == protected_message.code.is_response() if request_data is not None: request_kid, request_partiv = request_data protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0( protected_message, is_request=request_data is None) if protected: raise ProtectionInvalid("The protected field is not empty") # FIXME check for duplicate keys in protected if unprotected.pop(4, self.recipient_id) != self.recipient_id: # for most cases, this is caught by the session ID dispatch, but in # responses (where explicit sender IDs are atypical), this is a # valid check raise ProtectionInvalid("Sender ID does not match") if 6 not in unprotected: if request_data is None: raise ProtectonInvalid( "No sequence number provided in request") partial_iv = _pad_iv(self.algorithm, request_partiv) iv = _flip_first_bit(_xor_bytes(partial_iv, self.recipient_iv)) seqno = None # sentinel for not striking out anyting else: partial_iv_short = unprotected[6] if request_data is None: request_partiv = partial_iv_short request_kid = self.recipient_id seqno = int.from_bytes(partial_iv_short, 'big') if not self.recipient_replay_window.is_valid(seqno): raise ReplayError("Sequence number was re-used") partial_iv = _pad_iv(self.algorithm, partial_iv_short) iv = _xor_bytes(self.recipient_iv, partial_iv) # FIXME is it an error for additional data to be present in unprotected? if len(ciphertext) < self.algorithm.tag_bytes: raise ProtectionInvalid("Ciphertext shorter than tag length") tag = ciphertext[-self.algorithm.tag_bytes:] ciphertext = ciphertext[:-self.algorithm.tag_bytes] enc_structure = [ 'Encrypt0', protected_serialized, self._extract_external_aad(protected_message, request_kid, request_partiv) ] aad = cbor.dumps(enc_structure) plaintext = self.algorithm.decrypt(ciphertext, tag, aad, self.recipient_key, iv) if seqno is not None: self.recipient_replay_window.strike_out(seqno) # FIXME add options from unprotected unprotected_message = Message(code=protected_message.code) unprotected_message.payload = unprotected_message.opt.decode(plaintext) if unprotected_message.code.is_request: unprotected_message.opt.observe = protected_message.opt.observe else: if protected_message.opt.observe is not None: # is it really be as easy as that? unprotected_message.opt.observe = seqno return unprotected_message, (request_kid, request_partiv)
def protect(self, message, request_partiv=None): # not trying to preserve token or mid, they're up to the transport outer_message = Message(code=message.code) if message.code.is_request(): protected_uri = message.get_request_uri() if protected_uri.count('/') >= 3: protected_uri = protected_uri[:protected_uri.index( '/', protected_uri.index('/', protected_uri.index('/') + 1) + 1)] outer_message.set_request_uri(protected_uri) # FIXME any options to move out? inner_message = message if request_partiv is None: assert inner_message.code.is_request( ), "Trying to protect a response without request IV (possibly this is an observation; that's not supported in this OSCOAP implementation yet)" seqno = self.new_sequence_number() partial_iv = binascii.unhexlify( ("%%0%dx" % (2 * self.algorithm.iv_bytes)) % seqno) partial_iv_short = partial_iv.lstrip(b'\0') iv = _xor_bytes(self.sender_iv, partial_iv) unprotected = { 6: partial_iv_short, 4: self.sender_id, } request_kid = self.sender_id else: assert inner_message.code.is_response() partial_iv = request_partiv partial_iv_short = partial_iv.lstrip(b"\x00") iv = _flip_first_bit(_xor_bytes(partial_iv, self.sender_iv)) unprotected = {} # FIXME: better should mirror what was used in request request_kid = self.recipient_id protected = {} assert protected == {} protected_serialized = b'' # were it into an empty dict, it'd be the cbor dump enc_structure = [ 'Encrypt0', protected_serialized, self._extract_external_aad(outer_message, request_kid, partial_iv_short) ] aad = cbor.dumps(enc_structure) key = self.sender_key plaintext = inner_message.opt.encode() if inner_message.payload: plaintext += bytes([0xFF]) plaintext += inner_message.payload ciphertext, tag = self.algorithm.encrypt(plaintext, aad, key, iv) if USE_COMPRESSION: if protected: raise RuntimeError( "Protection produced a message that has uncompressable fields." ) if sorted(unprotected.keys()) == [4, 6]: shortarray = [unprotected[6], unprotected[4]] shortarray = cbor.dumps(shortarray) # we're using a shortarray shortened by one because that makes # it easier to then "exclude [...] the type and length for the # ciphertext"; the +1 on shortarray[0] makes it appear like a # 3-long array again. if (shortarray[0] + 1) & 0b11111000 != 0b10000000 or \ shortarray[1] & 0b11000000 != 0b01000000: raise RuntimeError( "Protection produced a message that has uncmpressable lengths" ) shortarray = bytes( ((((shortarray[0] + 1) & 0b111) << 3) | (shortarray[1] & 0b111), )) + shortarray[2:] oscoap_data = shortarray + ciphertext + tag elif unprotected == {}: oscoap_data = ciphertext + tag else: raise RuntimeError( "Protection produced a message that has uncompressable fields." ) else: cose_encrypt0 = [ protected_serialized, unprotected, ciphertext + tag ] oscoap_data = cbor.dumps(cose_encrypt0) if inner_message.code.can_have_payload(): outer_message.opt.object_security = b'' outer_message.payload = oscoap_data else: outer_message.opt.object_security = oscoap_data # FIXME go through options section return outer_message, partial_iv
def unprotect(self, protected_message, request_id=None): assert (request_id is not None) == protected_message.code.is_response() is_response = protected_message.code.is_response() # Set to a raisable exception on replay check failures; it will be # raised, but the package may still be processed in the course of Echo handling. replay_error = None protected_serialized, protected, unprotected, ciphertext = self._extract_encrypted0( protected_message) if protected: raise ProtectionInvalid("The protected field is not empty") # FIXME check for duplicate keys in protected if unprotected.pop(COSE_KID, self.recipient_id) != self.recipient_id: # for most cases, this is caught by the session ID dispatch, but in # responses (where explicit sender IDs are atypical), this is a # valid check raise ProtectionInvalid("Sender ID does not match") if COSE_PIV not in unprotected: if not is_response: raise ProtectionInvalid( "No sequence number provided in request") nonce = request_id.nonce seqno = None # sentinel for not striking out anyting else: partial_iv_short = unprotected[COSE_PIV] nonce = self._construct_nonce(partial_iv_short, self.recipient_id) seqno = int.from_bytes(partial_iv_short, 'big') if not is_response: if not self.recipient_replay_window.is_initialized(): replay_error = ReplayError( "Sequence number check unavailable") elif not self.recipient_replay_window.is_valid(seqno): replay_error = ReplayError("Sequence number was re-used") if replay_error is not None and self.echo_recovery is None: # Don't even try decoding if there is no reason to raise replay_error request_id = RequestIdentifiers(self.recipient_id, partial_iv_short, nonce, can_reuse_nonce=self.is_unicast and replay_error is None) # FIXME is it an error for additional data to be present in unprotected? if len( ciphertext ) < self.algorithm.tag_bytes + 1: # +1 assures access to plaintext[0] (the code) raise ProtectionInvalid("Ciphertext too short") enc_structure = [ 'Encrypt0', protected_serialized, self._extract_external_aad(protected_message, request_id.kid, request_id.partial_iv) ] aad = cbor.dumps(enc_structure) plaintext = self.algorithm.decrypt(ciphertext, aad, self.recipient_key, nonce) if not is_response and seqno is not None and replay_error is None: self.recipient_replay_window.strike_out(seqno) # FIXME add options from unprotected unprotected_message = Message(code=plaintext[0]) unprotected_message.payload = unprotected_message.opt.decode( plaintext[1:]) try_initialize = not self.recipient_replay_window.is_initialized() and \ self.echo_recovery is not None if try_initialize: if protected_message.code.is_request(): # Either accept into replay window and clear replay error, or raise # something that can turn into a 4.01,Echo response if unprotected_message.opt.echo == self.echo_recovery: self.recipient_replay_window.initialize_from_freshlyseen( seqno) replay_error = None else: raise ReplayErrorWithEcho(secctx=self, request_id=request_id, echo=self.echo_recovery) else: # We can initialize the replay window from a response as well. # The response is guaranteed fresh as it was AEAD-decoded to # match a request sent by this process. # # This is rare, as it only works when the server uses an own # sequence number, eg. when sending a notification or when # acting again on a retransmitted safe request whose response # it did not cache. # # Nothing bad happens if we can't make progress -- we just # don't initialize the replay window that wouldn't have been # checked for a response anyway. if seqno is not None: self.recipient_replay_window.initialize_from_freshlyseen( seqno) if replay_error is not None: raise replay_error if unprotected_message.code.is_request(): if protected_message.opt.observe != 0: unprotected_message.opt.observe = None else: if protected_message.opt.observe is not None: # -1 ensures that they sort correctly in later reordering # detection. Note that neither -1 nor high (>3 byte) sequence # numbers can be serialized in the Observe option, but they are # in this implementation accepted for passing around. unprotected_message.opt.observe = -1 if seqno is None else seqno return unprotected_message, request_id