mirror of
https://github.com/python-kasa/python-kasa.git
synced 2026-10-06 12:53:58 +00:00
A `credentials_hash` is specific to the transport that produced it, but nothing checked that the hash a transport was handed was its own. This matters because a device can change its encryption type without the credentials changing. For example, toggling **Third-Party Compatibility** in the Tapo app moves a plug or strip between TPAP and KLAP, and firmware updates are known to introduce new transports. This introduces a per-transport check that the hash has the shape that transport produces, and treats a foreign hash as absent rather than as a bad password. This makes the failure honest but still ends in a reauth. This also changes how credentials are read back out when the hash is one of the plaintext forms and lets the transport derive its own, so the change of encryption type needs no reauth in all cases.
57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
"""Credentials class for username / passwords."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from dataclasses import dataclass, field
|
|
|
|
from kasa.json import loads as json_loads
|
|
|
|
|
|
@dataclass
|
|
class Credentials:
|
|
"""Credentials for authentication."""
|
|
|
|
#: Username (email address) of the cloud account
|
|
username: str = field(default="", repr=False)
|
|
#: Password of the cloud account
|
|
password: str = field(default="", repr=False)
|
|
|
|
@classmethod
|
|
def _from_plaintext_hash(cls, credentials_hash: str) -> Credentials | None:
|
|
"""Recover the credentials from a hash that stores them in plaintext.
|
|
|
|
The ssl aes and tpap transports store base64 json of the plaintext
|
|
credentials, so a transport handed one of those after a device changed
|
|
its encryption type can derive its own hash rather than failing to
|
|
authenticate. Klap and aes hashes are one way, so this only works in
|
|
that direction.
|
|
"""
|
|
try:
|
|
decoded = json_loads(base64.b64decode(credentials_hash.encode()))
|
|
except (ValueError, UnicodeDecodeError):
|
|
return None
|
|
if not isinstance(decoded, dict):
|
|
return None
|
|
username = decoded.get("un")
|
|
password = decoded.get("pwd")
|
|
if isinstance(username, str) and isinstance(password, str):
|
|
return cls(username, password)
|
|
return None
|
|
|
|
|
|
def get_default_credentials(crdentials: tuple[str, str]) -> Credentials:
|
|
"""Return decoded default credentials."""
|
|
un = base64.b64decode(crdentials[0].encode()).decode()
|
|
pw = base64.b64decode(crdentials[1].encode()).decode()
|
|
return Credentials(un, pw)
|
|
|
|
|
|
DEFAULT_CREDENTIALS = {
|
|
"KASA": ("a2FzYUB0cC1saW5rLm5ldA==", "a2FzYVNldHVw"),
|
|
"KASACAMERA": ("YWRtaW4=", "MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM="),
|
|
"TAPO": ("dGVzdEB0cC1saW5rLm5ldA==", "dGVzdA=="),
|
|
"TAPOCAMERA": ("YWRtaW4=", "YWRtaW4="),
|
|
"TAPOCAMERA_LV3": ("YWRtaW4=", "VFBMMDc1NTI2NDYwNjAz"),
|
|
}
|