Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
3f82910
Initial TPAP Implementation
ZeliardM Oct 18, 2025
c7a3405
Clean up and coverage testing
ZeliardM Oct 19, 2025
3b15fdd
Try to fix CodeQL Security Warnings
ZeliardM Oct 19, 2025
4afa9eb
Change request headers and ssl context
ZeliardM Oct 19, 2025
65fe12b
Correct sessionId
ZeliardM Oct 21, 2025
6a1c037
Update ciphers
ZeliardM Oct 21, 2025
802b8a1
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Nov 4, 2025
be03dff
tpaptransport re-write and test coverage
ZeliardM Nov 5, 2025
4582ec1
Merge branch 'feature/tpap' of https://github.com/ZeliardM/python-kas…
ZeliardM Nov 5, 2025
5baddb0
TPAP Certificate handling changes and logging
ZeliardM Dec 8, 2025
43b779d
Fix NOC Authentication Flow
ZeliardM Dec 8, 2025
835c411
Fix test_tpaptransport.py tests
ZeliardM Dec 8, 2025
a2f0df6
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Dec 8, 2025
d6eeba9
Changes to TSLP Wrapper
ZeliardM Dec 8, 2025
f6837d7
TSLP Decode for Authentication
ZeliardM Dec 9, 2025
86a4ed3
Debug logging for tpaptransport.py
ZeliardM Dec 10, 2025
646ca65
Correct debug logging statements
ZeliardM Dec 10, 2025
8d19e6a
Revert to json for login
ZeliardM Dec 10, 2025
be146b2
Update test coverage
ZeliardM Dec 10, 2025
f4d81f2
Additional logging
ZeliardM Dec 10, 2025
99e0288
Raw response logging instead
ZeliardM Dec 10, 2025
78016e4
Correct noc session parameters
ZeliardM Dec 10, 2025
9005319
Update tpaptransport.py NOC encryption
ZeliardM Dec 10, 2025
21cda17
Test tpap parameter names
ZeliardM Dec 10, 2025
6bdad67
Return key values to correct format and add logging
ZeliardM Dec 11, 2025
ade20d7
Use base64 instead of hex for user_pk
ZeliardM Dec 11, 2025
f5d0da5
Updates and fixes for SPAKE2+ handling
ZeliardM Dec 12, 2025
79bcf3d
Update test coverage
ZeliardM Dec 12, 2025
022775a
Update test coverage
ZeliardM Dec 12, 2025
5f9fc5e
Cleaned up logging and test coverages
ZeliardM Dec 12, 2025
7e5fae8
Add http for TPAP
ZeliardM Dec 13, 2025
3a8eddd
Add correct http port handling for tpap
ZeliardM Dec 13, 2025
230b822
Update spake2+ handling
ZeliardM Dec 13, 2025
d15cbf9
Initial plan
Copilot Dec 13, 2025
c297f54
Add TPAP debug logs documentation file
Copilot Dec 13, 2025
9e1da3d
Add comprehensive debug logging to Spake2pAuthContext and remove mark…
Copilot Dec 13, 2025
a876e4f
Add comprehensive debug logging coverage for all Spake2pAuthContext s…
Copilot Dec 13, 2025
f331495
Merge pull request #1 from ZeliardM/copilot/add-tpap-debug-logs
ZeliardM Dec 13, 2025
31e337f
Initial plan
Copilot Dec 14, 2025
0f5a468
Fix line length violations in tpaptransport.py
Copilot Dec 14, 2025
c1f92ea
Fix final line length violation in tpaptransport.py
Copilot Dec 14, 2025
3a72145
Add coverage.xml to .gitignore and remove from tracking
Copilot Dec 14, 2025
547ead9
Merge pull request #2 from ZeliardM/copilot/check-ci-workflows
ZeliardM Dec 14, 2025
d51788b
Fix initial cipher_suites
ZeliardM Dec 14, 2025
7db0f2c
Update tpap test coverage
ZeliardM Dec 14, 2025
cf5abbd
Update test logging
ZeliardM Dec 14, 2025
6c27973
Fix encoding error
ZeliardM Dec 14, 2025
5ea34bb
Extra logging
ZeliardM Dec 15, 2025
faad4c7
Update hkdf_expand
ZeliardM Dec 15, 2025
f90f4a0
Additional logging
ZeliardM Dec 15, 2025
e8681b5
Fix hkdf expand
ZeliardM Dec 15, 2025
3b1b699
Update logging
ZeliardM Dec 15, 2025
67cc57f
Fix logging
ZeliardM Dec 15, 2025
f55868c
Fix confirmation keys length
ZeliardM Dec 16, 2025
ed8e57f
Starting cleanup from testing
ZeliardM Dec 16, 2025
b8d0387
Update test coverage
ZeliardM Dec 16, 2025
34c89f3
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Jan 27, 2026
7c6bc64
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Feb 22, 2026
a88995e
Merge branch 'master' into feature/tpap
ZeliardM Feb 27, 2026
a170141
Update for additional SPAKE2+ Curves
ZeliardM Feb 27, 2026
60c7a00
Possible CodeQL Scanning Fix
ZeliardM Feb 27, 2026
987097c
Remove inline code security scanning blocks
ZeliardM Feb 27, 2026
18736c2
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Mar 4, 2026
84e2aad
Complete re-write of TPAP Implementation
ZeliardM Mar 20, 2026
894db97
Revert httpclient.py handling and cleanup tpaptransport.py
ZeliardM Mar 21, 2026
0e9f22e
Revert test_discovery.py changes
ZeliardM Mar 21, 2026
341103c
Merge branch 'python-kasa:master' into feature/tpap
ZeliardM Apr 5, 2026
2640c28
Rework of tpaptransport.py and test coverages
ZeliardM Apr 9, 2026
1a5970d
Add Tapo H110 IR Air Conditioner support
Andrei965 Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use base64 instead of hex for user_pk
  • Loading branch information
ZeliardM committed Dec 11, 2025
commit ade20d7aa1714f85fbeec864c817b0c139dd1f79
47 changes: 23 additions & 24 deletions kasa/transports/tpaptransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import asyncio
import base64
import binascii
import contextlib
import hashlib
import hmac
Expand Down Expand Up @@ -341,13 +340,13 @@
"""Split intermediate and root certificates from a chain PEM."""
inter_pem, root_pem = chain_pem.split("-----END CERTIFICATE-----", 1)
inter_pem += "-----END CERTIFICATE-----"
return inter_pem, root_pem

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.

def apply(self, username: str, password: str) -> TpapNOCData:
"""Apply for a new NOC and cache materials."""
if self._key_pem and self._cert_pem and self._inter_pem and self._root_pem:
return self.get()
try:

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (id)
is used in a hashing algorithm (SHA1) that is insecure.
token, account_id = self._login(username, password)
url = self._get_url(account_id, token, username)
priv = ec.generate_private_key(ec.SECP256R1())
Expand Down Expand Up @@ -468,7 +467,7 @@
raise KasaException(
f"{self._transport._host} {step_name} bad status/body: "
f"{status} {type(data)}"
)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
Sensitive data (id)
is used in a hashing algorithm (MD5) that is insecure.
resp = cast(dict[str, Any], data)
self._authenticator._handle_response_error_code(
resp, f"TPAP {step_name} failed"
Expand All @@ -488,7 +487,7 @@
raise KasaException("NOC materials unavailable")
self.noc_cert_pem = noc.nocCertificate
self.noc_key_pem = noc.nocPrivateKey
self.user_icac_pem = noc.nocIntermediateCertificate or ""
self.user_icac_pem = noc.nocIntermediateCertificate
self.device_root_pem = noc.nocRootCertificate
self._ephemeral_priv: ec.EllipticCurvePrivateKey | None = None
self._ephemeral_pub_bytes: bytes | None = None
Expand All @@ -499,21 +498,21 @@
self._hkdf_hash = "SHA256"

@staticmethod
def _hex(b: bytes) -> str:
return binascii.hexlify(b).decode()
def _base64(b: bytes) -> str:
return base64.b64encode(b).decode()

@staticmethod
def _unhex(s: str) -> bytes:
return binascii.unhexlify(s.encode())
def _unbase64(s: str) -> bytes:
return base64.b64decode(s)

def _gen_ephemeral(self) -> bytes:
if self._ephemeral_priv is None:
if self._ephemeral_pub_bytes is None:
self._ephemeral_priv = ec.generate_private_key(ec.SECP256R1())
self._ephemeral_pub_bytes = self._ephemeral_priv.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint,
)
return cast(bytes, self._ephemeral_pub_bytes)
return self._ephemeral_pub_bytes

def _derive_shared_secret(self, dev_pub_uncompressed: bytes) -> bytes:
if self._ephemeral_priv is None:
Expand Down Expand Up @@ -551,8 +550,8 @@
dev_ica_pem = dev_proof_obj.get("dev_icac") or dev_proof_obj.get(
"devIcacCertificate"
)
proof_hex = dev_proof_obj.get("proof")
if not dev_noc_pem or not proof_hex:
proof_base64 = dev_proof_obj.get("proof")
if not dev_noc_pem or not proof_base64:
raise KasaException("Device proof missing fields")

dev_cert = x509.load_pem_x509_certificate(dev_noc_pem.encode())
Expand All @@ -571,7 +570,7 @@
+ self._dev_pub_bytes
+ self._ephemeral_pub_bytes
)
signature = binascii.unhexlify(proof_hex)
signature = self._unbase64(proof_base64)
dev_pub = cast(ec.EllipticCurvePublicKey, dev_cert.public_key())
dev_pub.verify(signature, message, ec.ECDSA(hashes.SHA256()))
except InvalidSignature as exc:
Expand All @@ -581,21 +580,21 @@

async def start(self) -> TlaSession | None:
"""Run NOC KEX + proof exchange and return session."""
user_pk_hex = self._hex(self._gen_ephemeral())
user_pk_base64 = self._base64(self._gen_ephemeral())
admin_md5 = self._md5_hex("admin")
params = {
"sub_method": "noc_kex",
"username": admin_md5,
"encryption": ["aes_128_ccm", "chacha20_poly1305", "aes_256_ccm"],
"user_pk": user_pk_hex,
"user_pk": user_pk_base64,
"stok": None,
}
resp = await self._login(params, step_name="noc_kex")
_LOGGER.debug("NOC KEX response: %r", resp)
dev_pk_hex = resp.get("dev_pk")
if not dev_pk_hex:
dev_pk_base64 = resp.get("dev_pk")
if not dev_pk_base64:
raise KasaException(f"NOC KEX response missing dev_pk, got {resp!r}")
self._dev_pub_bytes = self._unhex(dev_pk_hex)
self._dev_pub_bytes = self._unbase64(dev_pk_base64)
chosen = (resp.get("encryption") or "aes_128_ccm").lower().replace("-", "_")
self._chosen_cipher = (
cast(_CipherId, chosen)
Expand Down Expand Up @@ -626,7 +625,7 @@
{
"user_noc": self.noc_cert_pem,
"user_icac": self.user_icac_pem,
"proof": self._hex(signature),
"proof": self._base64(signature),
},
separators=(",", ":"),
).encode("utf-8")
Expand All @@ -635,16 +634,16 @@
)
proof_params = {
"sub_method": "noc_proof",
"user_proof_encrypt": self._hex(ct_body),
"tag": self._hex(tag),
"user_proof_encrypt": self._base64(ct_body),
"tag": self._base64(tag),
}
proof_res = await self._login(proof_params, step_name="noc_proof")

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
dev_proof_hex = proof_res.get("dev_proof_encrypt")
tag_hex = proof_res.get("tag")
if not dev_proof_hex:
dev_proof_base64 = proof_res.get("dev_proof_encrypt")
tag_base64 = proof_res.get("tag")
if not dev_proof_base64:
raise KasaException("NOC proof response missing device proof")
dev_ct = self._unhex(dev_proof_hex)
dev_tag = self._unhex(tag_hex) if tag_hex else b""
dev_ct = self._unbase64(dev_proof_base64)
dev_tag = self._unbase64(tag_base64) if tag_base64 else b""
dev_plain = _SessionCipher.sec_decrypt(
self._chosen_cipher, key, base_nonce, dev_ct, dev_tag, seq=1
)
Expand Down Expand Up @@ -862,11 +861,11 @@
)
.derive(ikm)
.hex()
.upper()

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function.
)

async def start(self) -> TlaSession | None:
"""Run SPAKE2+ register/share and return session."""

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data High

Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
Sensitive data (password)
is used in a hashing algorithm (SHA256) that is insecure for password hashing, since it is not a computationally expensive hash function.
params = {
"sub_method": "pake_register",
"username": self.username,
Expand Down
30 changes: 18 additions & 12 deletions tests/transports/test_tpaptransport.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import base64
import binascii
import hashlib
import logging
import os
Expand Down Expand Up @@ -399,7 +398,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_pk": binascii.hexlify(b"\x04" + b"\x01" * 64).decode(),
"dev_pk": base64.b64encode(b"\x04" + b"\x01" * 64).decode(),
"encryption": "aes_128_ccm",
"expired": 99,
},
Expand All @@ -408,8 +407,8 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_proof_encrypt": "00",
"tag": "00" * 16,
"dev_proof_encrypt": base64.b64encode(b"\x00").decode(),
"tag": base64.b64encode(b"\x00" * 16).decode(),
"sessionId": "SID",
"start_seq": 5,
"expired": 123,
Expand Down Expand Up @@ -555,7 +554,11 @@ def bad_sec_dec(cls, *a, **k): # noqa: ARG001
ctx_verify._ephemeral_pub_bytes = b"\x04" + b"\x04" * 64
with pytest.raises(KasaException, match="Invalid NOC device proof signature"):
ctx_verify._verify_device_proof(
{"dev_noc": dev_cert_pem, "dev_icac": inter_pem, "proof": bad_sig.hex()}
{
"dev_noc": dev_cert_pem,
"dev_icac": inter_pem,
"proof": base64.b64encode(bad_sig).decode(),
}
)


Expand Down Expand Up @@ -638,7 +641,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_pk": binascii.hexlify(b"\x04" + b"\x02" * 64).decode(),
"dev_pk": base64.b64encode(b"\x04" + b"\x02" * 64).decode(),
"encryption": "unknown",
"expired": 7,
},
Expand All @@ -647,7 +650,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_proof_encrypt": "00",
"dev_proof_encrypt": base64.b64encode(b"\x00").decode(),
"stok": "STK",
"startSeq": 3,
"sessionExpired": 321,
Expand Down Expand Up @@ -729,7 +732,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_pk": binascii.hexlify(b"\x04" + b"\x03" * 64).decode(),
"dev_pk": base64.b64encode(b"\x04" + b"\x03" * 64).decode(),
"encryption": "aes_128_ccm",
"expired": 1,
},
Expand All @@ -738,7 +741,10 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_proof": "00",
# This test covers the case where the response has a
# non-encrypted `dev_proof` field; keep it base64 for
# realism, but the transport expects `dev_proof_encrypt`.
"dev_proof": base64.b64encode(b"\x00").decode(),
"sessionId": "SIDN",
"start_seq": 2,
"expired": 5,
Expand Down Expand Up @@ -816,7 +822,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_pk": binascii.hexlify(b"\x04" + b"\x05" * 64).decode(),
"dev_pk": base64.b64encode(b"\x04" + b"\x05" * 64).decode(),
"encryption": "aes_128_ccm",
"expired": 42,
},
Expand All @@ -825,7 +831,7 @@ async def post(self, url, *, json=None, data=None, headers=None, ssl=None):
return 200, {
"error_code": 0,
"result": {
"dev_proof_encrypt": "00",
"dev_proof_encrypt": base64.b64encode(b"\x00").decode(),
"session_id": "SID_ALT",
},
}
Expand Down Expand Up @@ -982,7 +988,7 @@ def _ensure_noc(self):

wrong_message = b"not-the-expected-message"
bad_sig = priv.sign(wrong_message, ec.ECDSA(hashes.SHA256()))
dev_proof_obj = {"dev_noc": cert_pem, "proof": binascii.hexlify(bad_sig).decode()}
dev_proof_obj = {"dev_noc": cert_pem, "proof": base64.b64encode(bad_sig).decode()}
with pytest.raises(KasaException, match="Invalid NOC device proof signature"):
ctx._verify_device_proof(dev_proof_obj)

Expand Down
Loading