mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Fix parse_pcap_klap on windows and support default credentials (#1068)
- Fixes issue running pyshark on new thread in windows - Fixes bug if handshake repeated during capture - Tries the default tplink hardcoded credentials as per the library
This commit is contained in:
parent
e17ca21a83
commit
c19389f236
@ -6,6 +6,9 @@ It will output the decrypted data to a file.
|
|||||||
This was designed and tested with a Tapo light strip setup using a cloud account.
|
This was designed and tested with a Tapo light strip setup using a cloud account.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import codecs
|
import codecs
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@ -23,6 +26,7 @@ from kasa.deviceconfig import (
|
|||||||
DeviceFamily,
|
DeviceFamily,
|
||||||
)
|
)
|
||||||
from kasa.klaptransport import KlapEncryptionSession, KlapTransportV2
|
from kasa.klaptransport import KlapEncryptionSession, KlapTransportV2
|
||||||
|
from kasa.protocol import DEFAULT_CREDENTIALS, get_default_credentials
|
||||||
|
|
||||||
|
|
||||||
class MyEncryptionSession(KlapEncryptionSession):
|
class MyEncryptionSession(KlapEncryptionSession):
|
||||||
@ -42,16 +46,34 @@ class Operator:
|
|||||||
"""A class that handles the data decryption, and the encryption session updating."""
|
"""A class that handles the data decryption, and the encryption session updating."""
|
||||||
|
|
||||||
def __init__(self, klap, creds):
|
def __init__(self, klap, creds):
|
||||||
self._local_seed: bytes = None
|
self._local_seed: bytes | None = None
|
||||||
self._remote_seed: bytes = None
|
self._remote_seed: bytes | None = None
|
||||||
self._session: MyEncryptionSession = None
|
self._session: MyEncryptionSession | None = None
|
||||||
self._creds = creds
|
self._creds = creds
|
||||||
self._klap: KlapTransportV2 = klap
|
self._klap: KlapTransportV2 = klap
|
||||||
self._auth_hash = self._klap.generate_auth_hash(self._creds)
|
self._auth_hash = self._klap.generate_auth_hash(self._creds)
|
||||||
self._local_auth_hash = None
|
self._local_seed_auth_hash = None
|
||||||
self._remote_auth_hash = None
|
self._remote_seed_auth_hash = None
|
||||||
self._seq = 0
|
self._seq = 0
|
||||||
|
|
||||||
|
def check_default_credentials(self):
|
||||||
|
"""Check whether default credentials were used.
|
||||||
|
|
||||||
|
Devices sometimes randomly accept the hardcoded default credentials
|
||||||
|
and the library handles that.
|
||||||
|
"""
|
||||||
|
for value in DEFAULT_CREDENTIALS.values():
|
||||||
|
default_credentials = get_default_credentials(value)
|
||||||
|
default_auth_hash = self._klap.generate_auth_hash(default_credentials)
|
||||||
|
default_credentials_seed_auth_hash = self._klap.handshake1_seed_auth_hash(
|
||||||
|
self._local_seed,
|
||||||
|
self._remote_seed,
|
||||||
|
default_auth_hash, # type: ignore
|
||||||
|
)
|
||||||
|
if self._remote_seed_auth_hash == default_credentials_seed_auth_hash:
|
||||||
|
return default_auth_hash
|
||||||
|
return None
|
||||||
|
|
||||||
def update_encryption_session(self):
|
def update_encryption_session(self):
|
||||||
"""Update the encryption session used for decrypting data.
|
"""Update the encryption session used for decrypting data.
|
||||||
|
|
||||||
@ -66,21 +88,25 @@ class Operator:
|
|||||||
if self._local_seed is None or self._remote_seed is None:
|
if self._local_seed is None or self._remote_seed is None:
|
||||||
self._session = None
|
self._session = None
|
||||||
else:
|
else:
|
||||||
self._local_auth_hash = self._klap.handshake1_seed_auth_hash(
|
self._local_seed_auth_hash = self._klap.handshake1_seed_auth_hash(
|
||||||
self._local_seed, self._remote_seed, self._auth_hash
|
self._local_seed, self._remote_seed, self._auth_hash
|
||||||
)
|
)
|
||||||
if (self._remote_auth_hash is not None) and (
|
auth_hash = None
|
||||||
self._local_auth_hash != self._remote_auth_hash
|
if self._remote_seed_auth_hash is not None:
|
||||||
):
|
if self._local_seed_auth_hash == self._remote_seed_auth_hash:
|
||||||
raise ValueError(
|
auth_hash = self._auth_hash
|
||||||
"Local and remote auth hashes do not match.\
|
else:
|
||||||
This could mean an incorrect username and/or password."
|
auth_hash = self.check_default_credentials()
|
||||||
|
if not auth_hash:
|
||||||
|
raise ValueError(
|
||||||
|
"Local and remote auth hashes do not match. "
|
||||||
|
"This could mean an incorrect username and/or password."
|
||||||
|
)
|
||||||
|
self._session = MyEncryptionSession(
|
||||||
|
self._local_seed, self._remote_seed, auth_hash
|
||||||
)
|
)
|
||||||
self._session = MyEncryptionSession(
|
self._session._seq = self._seq
|
||||||
self._local_seed, self._remote_seed, self._auth_hash
|
self._session._generate_cipher()
|
||||||
)
|
|
||||||
self._session._seq = self._seq
|
|
||||||
self._session._generate_cipher()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def seq(self) -> int:
|
def seq(self) -> int:
|
||||||
@ -95,24 +121,27 @@ This could mean an incorrect username and/or password."
|
|||||||
self.update_encryption_session()
|
self.update_encryption_session()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def local_seed(self) -> bytes:
|
def local_seed(self) -> bytes | None:
|
||||||
"""Get the local seed."""
|
"""Get the local seed."""
|
||||||
return self._local_seed
|
return self._local_seed
|
||||||
|
|
||||||
@local_seed.setter
|
@local_seed.setter
|
||||||
def local_seed(self, value: bytes):
|
def local_seed(self, value: bytes):
|
||||||
|
print("setting local_seed")
|
||||||
if not isinstance(value, bytes):
|
if not isinstance(value, bytes):
|
||||||
raise ValueError("local_seed must be bytes")
|
raise ValueError("local_seed must be bytes")
|
||||||
elif len(value) != 16:
|
elif len(value) != 16:
|
||||||
raise ValueError("local_seed must be 16 bytes")
|
raise ValueError("local_seed must be 16 bytes")
|
||||||
else:
|
else:
|
||||||
self._local_seed = value
|
self._local_seed = value
|
||||||
|
self._remote_seed_auth_hash = None
|
||||||
|
self._remote_seed = None
|
||||||
self.update_encryption_session()
|
self.update_encryption_session()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_auth_hash(self) -> bytes:
|
def remote_auth_hash(self) -> bytes | None:
|
||||||
"""Get the remote auth hash."""
|
"""Get the remote auth hash."""
|
||||||
return self._remote_auth_hash
|
return self._remote_seed_auth_hash
|
||||||
|
|
||||||
@remote_auth_hash.setter
|
@remote_auth_hash.setter
|
||||||
def remote_auth_hash(self, value: bytes):
|
def remote_auth_hash(self, value: bytes):
|
||||||
@ -122,11 +151,11 @@ This could mean an incorrect username and/or password."
|
|||||||
elif len(value) != 32:
|
elif len(value) != 32:
|
||||||
raise ValueError("remote_auth_hash must be 32 bytes")
|
raise ValueError("remote_auth_hash must be 32 bytes")
|
||||||
else:
|
else:
|
||||||
self._remote_auth_hash = value
|
self._remote_seed_auth_hash = value
|
||||||
self.update_encryption_session()
|
self.update_encryption_session()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_seed(self) -> bytes:
|
def remote_seed(self) -> bytes | None:
|
||||||
"""Get the remote seed."""
|
"""Get the remote seed."""
|
||||||
return self._remote_seed
|
return self._remote_seed
|
||||||
|
|
||||||
@ -160,9 +189,17 @@ def bad_chars_replacement(exception):
|
|||||||
codecs.register_error("bad_chars_replacement", bad_chars_replacement)
|
codecs.register_error("bad_chars_replacement", bad_chars_replacement)
|
||||||
|
|
||||||
|
|
||||||
def main(username, password, device_ip, pcap_file_path, output_json_name=None):
|
def main(
|
||||||
|
loop: asyncio.AbstractEventLoop,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
device_ip,
|
||||||
|
pcap_file_path,
|
||||||
|
output_json_name=None,
|
||||||
|
):
|
||||||
"""Run the main function."""
|
"""Run the main function."""
|
||||||
capture = pyshark.FileCapture(pcap_file_path, display_filter="http")
|
asyncio.set_event_loop(loop)
|
||||||
|
capture = pyshark.FileCapture(pcap_file_path, display_filter="http", eventloop=loop)
|
||||||
|
|
||||||
# In an effort to keep this code tied into the original code
|
# In an effort to keep this code tied into the original code
|
||||||
# (so that this can hopefully leverage any future codebase updates inheriently),
|
# (so that this can hopefully leverage any future codebase updates inheriently),
|
||||||
@ -262,6 +299,9 @@ def main(username, password, device_ip, pcap_file_path, output_json_name=None):
|
|||||||
f.write("\n" * 1)
|
f.write("\n" * 1)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
# Call close method which cleans up event loop
|
||||||
|
capture.close()
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -292,12 +332,15 @@ def main(username, password, device_ip, pcap_file_path, output_json_name=None):
|
|||||||
required=False,
|
required=False,
|
||||||
help="The name of the output file, relative to the current directory.",
|
help="The name of the output file, relative to the current directory.",
|
||||||
)
|
)
|
||||||
def cli(username, password, host, pcap_file_path, output):
|
async def cli(username, password, host, pcap_file_path, output):
|
||||||
"""Export KLAP data in JSON format from a PCAP file."""
|
"""Export KLAP data in JSON format from a PCAP file."""
|
||||||
# pyshark does not work within a running event loop and we don't want to
|
# pyshark does not work within a running event loop and we don't want to
|
||||||
# install click as well as asyncclick so run in a new thread.
|
# install click as well as asyncclick so run in a new thread.
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=main, args=[username, password, host, pcap_file_path, output]
|
target=main,
|
||||||
|
args=[loop, username, password, host, pcap_file_path, output],
|
||||||
|
daemon=True,
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
thread.join()
|
thread.join()
|
||||||
|
Loading…
Reference in New Issue
Block a user