def authenticate_as_client(self, session_socket):
        # authenticates an external server connected via session_socket

        iv = self.receive_iv(session_socket)
        master_encrypter = Encrypter(self.master_key, iv)
        m = Messenger(session_socket, master_encrypter, self.continueHandler)

        client_challenge = genStr(CHALLENGE_LENGTH)
        client_challenge_hash = str(create_key(client_challenge))
        hash_len = len(client_challenge_hash)
        secretA = generateAorB()
        publicA = pow(g, secretA, p)
        m.send(client_challenge + str(publicA))

        response = m.recv()
        while not response:
            response = m.recv()

        if response[:hash_len] != client_challenge_hash:
            m.close()
            raise Exception('client could not authenticate')

        server_challenge_hash = str(create_key(response[hash_len:hash_len + CHALLENGE_LENGTH]))
        m.send(server_challenge_hash)
        public_b = int(response[hash_len + CHALLENGE_LENGTH:])
        self.log.info('g^b mod p is {}'.format(public_b))
        session_key = create_key(str(pow(public_b, secretA, p)))
        self.log.info('Session key generated by the client is {}'.format(session_key))

        session_encrypter = Encrypter(session_key, iv)
        session_m = Messenger(session_socket, session_encrypter, self.continueHandler)

        self._messenger = session_m
    def authenticate_as_server(self, session_socket):
        # authenticates an external client connected via session_socket

        iv = self.generate_and_send_iv(session_socket) # the server should generate a random iv

        master_encrypter = Encrypter(self.master_key, iv)
        m_messenger = Messenger(session_socket, master_encrypter, self.continueHandler)

        secret_b = generateAorB()
        public_b = str(pow(g, secret_b, p))
        server_challenge = genStr(CHALLENGE_LENGTH)
        server_challenge_hash = str(create_key(server_challenge))

        response = m_messenger.recv()
        while not response:
            response = m_messenger.recv()

        client_challenge = response[:CHALLENGE_LENGTH]
        client_challenge_hash = str(create_key(client_challenge))
        public_a = response[CHALLENGE_LENGTH:]
        self.log.info('publicA is {}'.format(public_a))
        m_messenger.send(client_challenge_hash + server_challenge + public_b)
        session_key = create_key(str(pow(int(public_a), secret_b, p)))
        self.log.info('session key is {}'.format(session_key))

        response = m_messenger.recv()
        while not response:
            response = m_messenger.recv()

        if response != server_challenge_hash:
            self.log.warn('Client could not be authenticated. Session will be terminated!')
            m_messenger.close()
        else:
            print('Server Authentication Successful!!!')

        session_encrypter = Encrypter(session_key, iv)
        self._messenger = Messenger(session_socket, session_encrypter, self.continueHandler)
class SessionManager:
    def __init__(self, port, ip_address, secret_value, continueHandler):
        # can be either a server or client. if ip_address=None, be a server on port. Otherwise, try to connect to
        # ip_address:port

        self.port = port
        self.ip_address = ip_address
        self.master_key = create_key(secret_value)
        self.log = logging.getLogger(__name__)
        self.continueHandler = continueHandler

        self._messenger = None
        self.reset_messenger()

    def generate_and_send_iv(self, session_socket):
        iv = urandom(16)

        self.continueHandler(iv)
        # send iv over socket first!
        sent_len = 0
        while sent_len < len(iv):
            sent = session_socket.send(iv[sent_len:])
            if sent == 0:
                raise RuntimeError("socket send connection issue")
            sent_len += sent  # how much of the message we have sent
        logging.getLogger(__name__).info("sent iv: " + str(iv))
        return iv

    def receive_iv(self, session_socket):
        iv = b''
        while len(iv) < IV_LENGTH:
            chunk = session_socket.recv(IV_LENGTH - len(iv))
            if chunk == b'':
                session_socket.close()
                raise RuntimeError("socket closed")
            iv += chunk
        logging.getLogger(__name__).info('received iv: {}'.format(str(iv)))
        return iv

    def authenticate_as_server(self, session_socket):
        # authenticates an external client connected via session_socket

        iv = self.generate_and_send_iv(session_socket) # the server should generate a random iv

        master_encrypter = Encrypter(self.master_key, iv)
        m_messenger = Messenger(session_socket, master_encrypter, self.continueHandler)

        secret_b = generateAorB()
        public_b = str(pow(g, secret_b, p))
        server_challenge = genStr(CHALLENGE_LENGTH)
        server_challenge_hash = str(create_key(server_challenge))

        response = m_messenger.recv()
        while not response:
            response = m_messenger.recv()

        client_challenge = response[:CHALLENGE_LENGTH]
        client_challenge_hash = str(create_key(client_challenge))
        public_a = response[CHALLENGE_LENGTH:]
        self.log.info('publicA is {}'.format(public_a))
        m_messenger.send(client_challenge_hash + server_challenge + public_b)
        session_key = create_key(str(pow(int(public_a), secret_b, p)))
        self.log.info('session key is {}'.format(session_key))

        response = m_messenger.recv()
        while not response:
            response = m_messenger.recv()

        if response != server_challenge_hash:
            self.log.warn('Client could not be authenticated. Session will be terminated!')
            m_messenger.close()
        else:
            print('Server Authentication Successful!!!')

        session_encrypter = Encrypter(session_key, iv)
        self._messenger = Messenger(session_socket, session_encrypter, self.continueHandler)

    def authenticate_as_client(self, session_socket):
        # authenticates an external server connected via session_socket

        iv = self.receive_iv(session_socket)
        master_encrypter = Encrypter(self.master_key, iv)
        m = Messenger(session_socket, master_encrypter, self.continueHandler)

        client_challenge = genStr(CHALLENGE_LENGTH)
        client_challenge_hash = str(create_key(client_challenge))
        hash_len = len(client_challenge_hash)
        secretA = generateAorB()
        publicA = pow(g, secretA, p)
        m.send(client_challenge + str(publicA))

        response = m.recv()
        while not response:
            response = m.recv()

        if response[:hash_len] != client_challenge_hash:
            m.close()
            raise Exception('client could not authenticate')

        server_challenge_hash = str(create_key(response[hash_len:hash_len + CHALLENGE_LENGTH]))
        m.send(server_challenge_hash)
        public_b = int(response[hash_len + CHALLENGE_LENGTH:])
        self.log.info('g^b mod p is {}'.format(public_b))
        session_key = create_key(str(pow(public_b, secretA, p)))
        self.log.info('Session key generated by the client is {}'.format(session_key))

        session_encrypter = Encrypter(session_key, iv)
        session_m = Messenger(session_socket, session_encrypter, self.continueHandler)

        self._messenger = session_m

    def reset_messenger(self):
        if self._messenger is not None:
            self._messenger.close()
            self._messenger = None

        # AF_INET = ipv4, SOCK_STREAM = tcp
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # assuming we can use the same "client socket" for both reading and writing
        if self.ip_address is None:  # server init
            s.bind(('', self.port))
            self.log.info("Listening for connection on port {}".format(self.port))
            s.listen(1) # listen for only one connection

            session_socket, addr = s.accept()
            self.log.info("Accepted connection from {}".format(addr))

            self.authenticate_as_server(session_socket)
            s.close()
        else:
            # client init: specify ip address and port to try to ping
            self.log.info("Trying to connect to {}:{}".format(self.ip_address, self.port))
            s.connect((self.ip_address, self.port))

            self.authenticate_as_client(s)


    def checkReceivedMessages(self):
        try:
            nextReceivedMessage = self.recv()
            # Get and Send Messages
            return nextReceivedMessage
        except Exception as e:
            self.log.warning("Session closed: {}".format(e))
            self.reset_messenger()
            # Return 0 as default
            return 0  
               

    def send(self, msg):
        self._messenger.send(msg)

    def recv(self):
        data_in = self._messenger.recv()
        if len(data_in) > 0:
            self.log.info('received data: ' + data_in)
        return data_in

    def close(self):
        self._messenger.close()