Esempio n. 1
0
    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)
Esempio n. 2
0
    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
Esempio n. 3
0
 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
Esempio n. 4
0
    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
Esempio n. 5
0
    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
Esempio n. 6
0
 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)
Esempio n. 7
0
    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())
Esempio n. 8
0
 def to_message(self):
     inner = Message(
         code=UNAUTHORIZED,
         echo=self.echo,
     )
     outer, _ = self.secctx.protect(inner, request_id=self.request_id)
     return outer
Esempio n. 9
0
    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())
Esempio n. 10
0
 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
Esempio n. 11
0
 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))
Esempio n. 12
0
 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)
Esempio n. 13
0
 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
Esempio n. 14
0
 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
Esempio n. 15
0
 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
Esempio n. 16
0
 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())
Esempio n. 17
0
    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
Esempio n. 18
0
 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())
Esempio n. 19
0
    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())
Esempio n. 20
0
 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)
Esempio n. 21
0
    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
Esempio n. 22
0
    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)
Esempio n. 24
0
    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
Esempio n. 25
0
    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