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
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() 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 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