def validate_received_invocation_counter( self, received_invocation_counter: int) -> None: """ The recevied invocation counter must be larger than the last one we registered. """ if received_invocation_counter <= self.meter_invocation_counter: raise exceptions.LocalDlmsProtocolError( "Received invocation counter is not larger than the previous " "received one. ")
def associate( self, association_request: Optional[ acse.ApplicationAssociationRequest] = None, ) -> acse.ApplicationAssociationResponse: # the aarq can be overridden or the standard one from the connection is used. aarq = association_request or self.dlms_connection.get_aarq() self.send(aarq) response = self.next_event() # we could have received an exception from the meter. if isinstance(response, xdlms.ExceptionResponse): raise exceptions.DlmsClientException( f"DLMS Exception: {response.state_error!r}:{response.service_error!r}" ) # the association might not be accepted by the meter if isinstance(response, acse.ApplicationAssociationResponse): if response.result is not enumerations.AssociationResult.ACCEPTED: # there could be an error suppled with the reject. extra_error = None if response.user_information: if isinstance(response.user_information.content, ConfirmedServiceError): extra_error = response.user_information.content.error raise exceptions.DlmsClientException( f"Unable to perform Association: {response.result!r} and " f"{response.result_source_diagnostics!r}, extra info: {extra_error}" ) else: raise exceptions.LocalDlmsProtocolError( "Did not receive an AARE after sending AARQ") if self.should_send_hls_reply(): try: hls_response = self.send_hls_reply() except ActionError as e: raise HLSError from e if not hls_response: raise HLSError("No HLS data in response") hls_data = utils.parse_as_dlms_data(hls_response) if not hls_response: raise HLSError("Did not receive any HLS response data") if not self.dlms_connection.hls_response_valid(hls_data): raise HLSError( f"Meter did not respond with correct challenge calculation" ) return response
def next_event(self): """ Will parse the buffer into an APDU. In lower levels we need the case to get more data. But this is not needed in the DLMS connections as it is the lower layers responisbility to make sure the data is complete before handing the control back to the dlms-layer. In the HDLC case the data is complete when we get the last nonsegmented informtion frame. And in the IP case we know the length of the data from the IP wrapper so we can keep on trying until we get all data. """ apdu = XDlmsApduFactory.apdu_from_bytes(self.buffer) if self.use_protection: apdu = self.unprotect(apdu) self.update_negotiated_parameters(apdu) self.state.process_event(apdu) self.clear_buffer() if isinstance(apdu, acse.ApplicationAssociationResponseApdu): if apdu.result in [ enums.AssociationResult.REJECTED_PERMANENT, enums.AssociationResult.REJECTED_TRANSIENT, ]: # reset the association on a reject self.state.process_event(dlms_state.RejectAssociation()) # we need to start the HLS auth. elif apdu.authentication == enums.AuthenticationMechanism.HLS_GMAC: self.state.process_event(dlms_state.HlsStart()) # Handle HLS verification if self.state.current_state == dlms_state.HLS_DONE: if isinstance(apdu, xdlms.ActionResponseNormalWithData): if apdu.status != enums.ActionResultStatus.SUCCESS: self.state.process_event(dlms_state.HlsFailed()) if self.hls_response_valid(utils.parse_as_dlms_data( apdu.data)): self.state.process_event(dlms_state.HlsSuccess()) else: self.state.process_event(dlms_state.HlsFailed()) elif isinstance(apdu, (xdlms.ActionResponseNormalWithError, xdlms.ActionResponseNormal)): self.state.process_event(dlms_state.HlsFailed()) else: raise exceptions.LocalDlmsProtocolError( "Received a non Action response when in HLS DONE") return apdu
def get_hls_reply(self) -> bytes: """ When the meter has enterted the HLS procedure the client firsts sends a reply to the server (meter) challenge. It is done with an ActionRequest to the current LN Association object in the meter. Method 2, Reply_to_HLS. Depending on the HLS type the data looks a bit different HLS_GMAC: SC + IC + GMAC(SC + AK + Challenge) """ if not self.meter_to_client_challenge: raise exceptions.LocalDlmsProtocolError( "Meter has not send challenge") if not self.global_encryption_key: raise ProtectionError( "Unable to create GMAC. Missing global_encryption_key") if not self.global_authentication_key: raise ProtectionError( "Unable to create GMAC. Missing global_authentication_key") if self.authentication_method == enums.AuthenticationMechanism.HLS_GMAC: only_auth_security_control = security.SecurityControlField( security_suite=self.security_suite, authenticated=True, encrypted=False) gmac_result = security.gmac( security_control=only_auth_security_control, system_title=self.client_system_title, invocation_counter=self.client_invocation_counter, key=self.global_encryption_key, auth_key=self.global_authentication_key, challenge=self.meter_to_client_challenge, ) return (only_auth_security_control.to_bytes() + self.client_invocation_counter.to_bytes(4, "big") + gmac_result) else: raise NotImplementedError( f"No implementation for HSL: {self.authentication_method!r}")
def send(self, event) -> bytes: """ Returns the bytes to be sent over the connection and changes the state depending on event sent. """ if self.is_pre_established: # When we are in a pre established association state starts as READY. # Only invalid state change is to send the ReleaseRequestApdu. But it is not # possible to close a pre-established association. if isinstance( event, (acse.ReleaseRequest, acse.ApplicationAssociationRequest)): raise exceptions.PreEstablishedAssociationError( f"It is not allowed to send a {type(event)} when the association is" f"pre-established ") self.state.process_event(event) LOG.debug(f"Preparing to send: {event}") if self.use_protection: event = self.protect(event) # if self.use_blocks: # blocks = self.make_blocks(event) # # TODO: How to handle the subcase of sending blocks? LOG.info(f"Sending : {event}") out = event.to_bytes() if len(out) > self.max_pdu_size: raise exceptions.LocalDlmsProtocolError( f"PDU size too big. Max PDU size for association is {self.max_pdu_size} " f"bytes. PDU to be sent is {len(out)}") return out
def next_event(self): """ Will parse the buffer into an APDU. In lower levels we need the case to get more data. But this is not needed in the DLMS connections as it is the lower layers responsibility to make sure the data is complete before handing the control back to the dlms-layer. In the HDLC case the data is complete when we get the last unsegmented information frame. And in the IP case the length is known from the IP wrapper element so it is possible to can keep on trying until all data is received. """ apdu = XDlmsApduFactory.apdu_from_bytes(self.buffer) if isinstance(apdu, acse.ApplicationAssociationResponse): # To be able to run the decryption we need to know some things about the # meter and that has to be extracted first self.update_meter_info(apdu) if self.use_protection: apdu = self.unprotect(apdu) if self.is_pre_established: if isinstance( apdu, (acse.ApplicationAssociationResponse, acse.ReleaseResponse), ): raise exceptions.PreEstablishedAssociationError( f"Received a {apdu.__class__.__name__}. In a pre-established " f"association it is not possible to handle ACSE services.") self.state.process_event(apdu) self.clear_buffer() if isinstance(apdu, acse.ApplicationAssociationResponse): self.update_negotiated_parameters(apdu) if apdu.result in [ enums.AssociationResult.REJECTED_PERMANENT, enums.AssociationResult.REJECTED_TRANSIENT, ]: # reset the association on a reject self.state.process_event(dlms_state.RejectAssociation()) # we need to start the HLS auth. elif apdu.authentication == enums.AuthenticationMechanism.HLS_GMAC: self.state.process_event(dlms_state.HlsStart()) # Handle HLS verification if self.state.current_state == dlms_state.HLS_DONE: if isinstance(apdu, xdlms.ActionResponseNormalWithData): if apdu.status != enums.ActionResultStatus.SUCCESS: self.state.process_event(dlms_state.HlsFailed()) if self.hls_response_valid(utils.parse_as_dlms_data( apdu.data)): self.state.process_event(dlms_state.HlsSuccess()) else: self.state.process_event(dlms_state.HlsFailed()) elif isinstance(apdu, (xdlms.ActionResponseNormalWithError, xdlms.ActionResponseNormal)): self.state.process_event(dlms_state.HlsFailed()) else: raise exceptions.LocalDlmsProtocolError( "Received a non Action response when in HLS DONE") return apdu