diff --git a/tikapy/__init__.py b/tikapy/__init__.py index 41df88e..6b0bece 100644 --- a/tikapy/__init__.py +++ b/tikapy/__init__.py @@ -151,18 +151,23 @@ def _connect(self): self._connect_socket() self._sock = self._base_sock - def login(self, user, password): + def login(self, user, password, allow_insecure_auth_without_tls=False): """ Connects to the API and tries to login the user. :param user: Username for API connections :param password: Password for API connections + :param allow_insecure_auth_without_tls: Allow plaintext authentication + against RouterOS 6.43+. This is false by default to avoid accidental + downgrade in security when the router is upgraded. :raises: ClientError - if login failed """ self._connect() self._api = ApiRos(self._sock) try: - self._api.login(user, password) + socket_is_tls = hasattr(self._sock, "getpeercert") + send_plain_password = (socket_is_tls or allow_insecure_auth_without_tls) + self._api.login(user, password, send_plain_password) except (ApiError, ApiUnrecoverableError) as exc: raise ClientError('could not login') from exc diff --git a/tikapy/api/__init__.py b/tikapy/api/__init__.py index 712a11e..20dcfd2 100644 --- a/tikapy/api/__init__.py +++ b/tikapy/api/__init__.py @@ -57,32 +57,48 @@ def __init__(self, sock): self.sock = sock self.currenttag = 0 - def login(self, username, password): + def login(self, username, password, send_plain_password=True): """ Perform API login Args: username - Username used to login password - Password used to login + send_plain_password - Whether to send plaintext password (new-style) + without requiring MD5 CRAM """ + # RouterOS <= 6.43rc17 uses MD5 challenge-response: + # --> /login{} + # <-- {ret} + # --> /login{name, response} + # <-- {} + # + # RouterOS >= 6.43rc19 uses plaintext authentication: + # --> /login{name, password} + # <-- {} + # request login - # Mikrotik answers with a challenge in the 'ret' attribute - # 'ret' attribute accessible as attrs['ret'] - _, attrs = self.talk(["/login"])[0] - - # Prepare response for challenge-response login - # response is MD5 of 0-char + plaintext-password + challange - response = hashlib.md5() - response.update(b'\x00') - response.update(password.encode('UTF-8')) - response.update(binascii.unhexlify((attrs['ret']).encode('UTF-8'))) - response = "00" + binascii.hexlify(response.digest()).decode('UTF-8') - - # send response & login request - self.talk(["/login", - "=name=%s" % username, - "=response=%s" % response]) + if send_plain_password: + _, attrs = self.talk(["/login", + "=name=%s" % username, + "=password=%s" % password])[0] + else: + _, attrs = self.talk(["/login"])[0] + + if "ret" in attrs: + # Prepare response for challenge-response login + # response is MD5 of 0-char + plaintext-password + challange + response = hashlib.md5() + response.update(b'\x00') + response.update(password.encode('UTF-8')) + response.update(binascii.unhexlify((attrs['ret']).encode('UTF-8'))) + response = "00" + binascii.hexlify(response.digest()).decode('UTF-8') + + # send response & login request + self.talk(["/login", + "=name=%s" % username, + "=response=%s" % response]) def talk(self, words): """