Skip to content

Commit a6d8db8

Browse files
fix(enphase): keep livestream credentials out of the log
The livestream bootstrap response carries aws_token_value and aws_digest, the live credentials for the account's AWS IoT stream. Debug API logging redacted only token/auth_token/access_token, so both were written out in full - and Predbat logs are routinely shared for debugging. The endpoint and topic are still logged so the call stays diagnosable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b47d23c commit a6d8db8

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

apps/predbat/enphase.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1625,7 +1625,9 @@ def _log_api_call(self, method, path, params, status, json_data, text):
16251625
return
16261626
if isinstance(json_data, dict):
16271627
redacted = dict(json_data)
1628-
for key in ("token", "auth_token", "access_token"):
1628+
# aws_token_value/aws_digest are the livestream's AWS IoT credentials - short-lived,
1629+
# but Predbat logs get shared for debugging, so they must never be written out.
1630+
for key in ("token", "auth_token", "access_token", "aws_token_value", "aws_digest"):
16291631
if key in redacted:
16301632
redacted[key] = "***redacted***"
16311633
preview = json.dumps(redacted, default=str)

apps/predbat/tests/test_enphase_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,31 @@ def test_log_api_call_redacts_token():
982982
assert captured == []
983983

984984

985+
def test_log_api_call_redacts_livestream_credentials():
986+
"""The livestream bootstrap's token and signature must never reach the log.
987+
988+
Predbat logs are routinely shared for debugging, and this response carries live credentials
989+
for the account's AWS IoT stream.
990+
"""
991+
api = MockEnphaseAPI()
992+
captured = []
993+
api.log = lambda message: captured.append(message)
994+
api.debug_api = True
995+
api._log_api_call(
996+
"GET",
997+
"/pv/aws_sigv4/livestream.json",
998+
{"serial_num": "122530006866"},
999+
200,
1000+
{"aws_token_value": "token-must-not-be-logged", "aws_digest": "signature-must-not-be-logged", "aws_iot_endpoint": "iot.example.com", "live_stream_topic": "v1/live-stream/abc123"},
1001+
"",
1002+
)
1003+
assert "token-must-not-be-logged" not in captured[0]
1004+
assert "signature-must-not-be-logged" not in captured[0]
1005+
# Non-secret fields are still logged, so the call remains diagnosable
1006+
assert "iot.example.com" in captured[0]
1007+
assert "v1/live-stream/abc123" in captured[0]
1008+
1009+
9851010
def test_login_dedupes_sites():
9861011
"""Duplicate sites in the search response collapse to a single entry (no double-publish)."""
9871012
api = MockEnphaseAPI()
@@ -1977,6 +2002,7 @@ def run_enphase_api_tests(my_predbat):
19772002
test_get_battery_status_handles_na()
19782003
test_reads_handle_na_values()
19792004
test_log_api_call_redacts_token()
2005+
test_log_api_call_redacts_livestream_credentials()
19802006
test_login_dedupes_sites()
19812007
test_run_single_site_publishes_once()
19822008
test_run_no_battery_returns_false_without_raising()

0 commit comments

Comments
 (0)