socketry 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: socketry
3
+ Version: 0.1.0
4
+ Summary: Python API and CLI for controlling Jackery portable power stations
5
+ Author: Jesus Lopez
6
+ Author-email: Jesus Lopez <jesus@jesusla.com>
7
+ License-Expression: MIT
8
+ Requires-Dist: typer>=0.9
9
+ Requires-Dist: paho-mqtt>=2.0
10
+ Requires-Dist: requests>=2.31
11
+ Requires-Dist: pycryptodome>=3.19
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+
15
+ # socketry
16
+
17
+ [![CI](https://github.com/jlopez/socketry/actions/workflows/ci.yml/badge.svg)](https://github.com/jlopez/socketry/actions/workflows/ci.yml)
18
+ ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
19
+
20
+ Python API and CLI for controlling Jackery portable power stations.
21
+
22
+ Reverse-engineered from the Jackery Android APK (v1.0.7) and iOS app (v1.2.0).
23
+ Communicates via Jackery's cloud MQTT broker and HTTP API — no modifications to
24
+ the device or its firmware.
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ uvx --from git+https://github.com/jlopez/socketry socketry login --email you@example.com --password 'yourpass'
30
+ uvx --from git+https://github.com/jlopez/socketry socketry get
31
+ ```
32
+
33
+ Or install it once and use `socketry` directly:
34
+
35
+ ```bash
36
+ uv tool install git+https://github.com/jlopez/socketry
37
+ socketry login --email you@example.com --password 'yourpass'
38
+ socketry get
39
+ ```
40
+
41
+ ## Supported devices
42
+
43
+ All 10 models in the current Jackery app share the same protocol:
44
+
45
+ | Model | Code |
46
+ |-------|------|
47
+ | Explorer 3000 Pro | 1 |
48
+ | Explorer 2000 Plus | 2 |
49
+ | Explorer 300 Plus | 4 |
50
+ | Explorer 1000 Plus | 5 |
51
+ | Explorer 700 Plus | 6 |
52
+ | Explorer 280 Plus | 7 |
53
+ | Explorer 1000 Pro2 | 8 |
54
+ | Explorer 600 Plus | 9 |
55
+ | Explorer 240 | 10 |
56
+ | Explorer 2000 | 12 |
57
+
58
+ Properties and MQTT action IDs are exhaustive for this APK version. Unknown
59
+ properties returned by newer firmware are displayed as raw key/value pairs.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ # Install as a CLI tool (from GitHub)
65
+ uv tool install git+https://github.com/jlopez/socketry
66
+
67
+ # Or from PyPI (once published)
68
+ uv tool install socketry
69
+
70
+ # Or install as a library
71
+ pip install socketry
72
+ ```
73
+
74
+ ## CLI usage
75
+
76
+ ### Login
77
+
78
+ ```bash
79
+ # Authenticates and discovers all devices (owned + shared with you)
80
+ socketry login --email you@example.com --password 'yourpass'
81
+
82
+ # List devices and select the active one
83
+ socketry devices
84
+ socketry select 0
85
+ ```
86
+
87
+ Credentials are saved to `~/.config/socketry/credentials.json` (mode 0600).
88
+
89
+ ### Reading properties (`get`)
90
+
91
+ ```bash
92
+ # All properties (colored + grouped on a TTY)
93
+ socketry get
94
+
95
+ # Single property — by CLI name or raw protocol key
96
+ socketry get battery # Battery: 85%
97
+ socketry get rb # Battery: 85% (same thing)
98
+ socketry get ac # AC output: ON
99
+
100
+ # JSON output (indented on TTY, compact when piped)
101
+ socketry get --json
102
+ socketry get ac --json # {"oac": 1}
103
+ socketry get --json | jq .rb # pipe-friendly
104
+ ```
105
+
106
+ Available properties:
107
+
108
+ | Group | Names |
109
+ |-------|-------|
110
+ | Battery & Power | `battery`, `battery-temp`, `battery-state`, `input-power`, `output-power`, `input-time`, `output-time` |
111
+ | I/O State | `ac`, `dc`, `usb`, `car`, `ac-in`, `dc-in`, `light`, `wireless` |
112
+ | Settings | `charge-speed`, `auto-shutdown`, `energy-saving`, `battery-protection`, `sfc`, `ups`, `screen-timeout` |
113
+ | AC / Power Detail | `ac-input-power`, `car-input-power`, `ac-voltage`, `ac-freq`, `ac-power`, `ac-power-2`, `ac-socket-power` |
114
+ | Other / Alarms | `error-code`, `temp-alarm`, `power-alarm`, `power-mode-battery`, `total-temp`, `system-status`, `power-capacity` |
115
+
116
+ Raw protocol keys (`rb`, `oac`, `bt`, ...) are also accepted.
117
+
118
+ ### Changing settings (`set`)
119
+
120
+ ```bash
121
+ # I/O toggles
122
+ socketry set ac on
123
+ socketry set dc off
124
+ socketry set usb on
125
+ socketry set car off
126
+
127
+ # Light
128
+ socketry set light high # off | low | high | sos
129
+
130
+ # Device settings
131
+ socketry set charge-speed mute # fast | mute
132
+ socketry set battery-protection eco # full | eco
133
+ socketry set ups on
134
+ socketry set sfc on
135
+
136
+ # Integer settings
137
+ socketry set screen-timeout 30
138
+ socketry set auto-shutdown 60
139
+ socketry set energy-saving 30
140
+
141
+ # Wait for device confirmation
142
+ socketry set ac on --wait
143
+
144
+ # Show available settings
145
+ socketry set
146
+ socketry set light # "expects a value: off | low | high | sos"
147
+ ```
148
+
149
+ Writable settings:
150
+
151
+ | Setting | Values | Description |
152
+ |---------|--------|-------------|
153
+ | `ac` | on / off | AC output |
154
+ | `dc` | on / off | DC output |
155
+ | `usb` | on / off | USB output |
156
+ | `car` | on / off | Car (12V) output |
157
+ | `ac-in` | on / off | AC input |
158
+ | `dc-in` | on / off | DC input |
159
+ | `light` | off / low / high / sos | Light mode |
160
+ | `screen-timeout` | integer | Screen timeout |
161
+ | `auto-shutdown` | integer | Auto shutdown timer |
162
+ | `charge-speed` | fast / mute | Charge speed mode |
163
+ | `battery-protection` | full / eco | Battery protection level |
164
+ | `energy-saving` | integer | Energy saving timeout |
165
+ | `sfc` | on / off | Super fast charge |
166
+ | `ups` | on / off | UPS mode |
167
+
168
+ ## Library usage
169
+
170
+ ```python
171
+ from socketry import Client
172
+
173
+ # Authenticate (or load saved credentials)
174
+ client = Client.login("email@example.com", "password")
175
+ client.save_credentials()
176
+
177
+ # Or load previously saved credentials
178
+ client = Client.from_saved()
179
+
180
+ # List and select devices
181
+ devices = client.fetch_devices()
182
+ client.select_device(0)
183
+
184
+ # Read properties
185
+ props = client.get_all_properties()
186
+ setting, value = client.get_property("battery")
187
+ print(f"{setting.name}: {setting.format_value(value)}")
188
+
189
+ # Control
190
+ client.set_property("ac", "on")
191
+ result = client.set_property("light", "high", wait=True)
192
+ ```
193
+
194
+ ## How it works
195
+
196
+ ```
197
+ socketry ──HTTP──> iot.jackeryapp.com (login, device list, properties)
198
+ socketry ──MQTT──> emqx.jackeryapp.com (device control via encrypted TLS)
199
+ ```
200
+
201
+ Login uses AES-192/ECB + RSA-1024 encrypted HTTP POST. Device control commands
202
+ are published over MQTT (TLS 1.2 with a self-signed CA). Status polling uses the
203
+ HTTP property endpoint. See [docs/protocol.md](docs/protocol.md) for the full
204
+ protocol specification.
205
+
206
+ ## Roadmap
207
+
208
+ - [ ] MQTT real-time monitor (subscribe to live property changes)
209
+ - [ ] Token auto-refresh (JWT expires ~30 days)
210
+
211
+ ## License
212
+
213
+ MIT
@@ -0,0 +1,199 @@
1
+ # socketry
2
+
3
+ [![CI](https://github.com/jlopez/socketry/actions/workflows/ci.yml/badge.svg)](https://github.com/jlopez/socketry/actions/workflows/ci.yml)
4
+ ![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
5
+
6
+ Python API and CLI for controlling Jackery portable power stations.
7
+
8
+ Reverse-engineered from the Jackery Android APK (v1.0.7) and iOS app (v1.2.0).
9
+ Communicates via Jackery's cloud MQTT broker and HTTP API — no modifications to
10
+ the device or its firmware.
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ uvx --from git+https://github.com/jlopez/socketry socketry login --email you@example.com --password 'yourpass'
16
+ uvx --from git+https://github.com/jlopez/socketry socketry get
17
+ ```
18
+
19
+ Or install it once and use `socketry` directly:
20
+
21
+ ```bash
22
+ uv tool install git+https://github.com/jlopez/socketry
23
+ socketry login --email you@example.com --password 'yourpass'
24
+ socketry get
25
+ ```
26
+
27
+ ## Supported devices
28
+
29
+ All 10 models in the current Jackery app share the same protocol:
30
+
31
+ | Model | Code |
32
+ |-------|------|
33
+ | Explorer 3000 Pro | 1 |
34
+ | Explorer 2000 Plus | 2 |
35
+ | Explorer 300 Plus | 4 |
36
+ | Explorer 1000 Plus | 5 |
37
+ | Explorer 700 Plus | 6 |
38
+ | Explorer 280 Plus | 7 |
39
+ | Explorer 1000 Pro2 | 8 |
40
+ | Explorer 600 Plus | 9 |
41
+ | Explorer 240 | 10 |
42
+ | Explorer 2000 | 12 |
43
+
44
+ Properties and MQTT action IDs are exhaustive for this APK version. Unknown
45
+ properties returned by newer firmware are displayed as raw key/value pairs.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ # Install as a CLI tool (from GitHub)
51
+ uv tool install git+https://github.com/jlopez/socketry
52
+
53
+ # Or from PyPI (once published)
54
+ uv tool install socketry
55
+
56
+ # Or install as a library
57
+ pip install socketry
58
+ ```
59
+
60
+ ## CLI usage
61
+
62
+ ### Login
63
+
64
+ ```bash
65
+ # Authenticates and discovers all devices (owned + shared with you)
66
+ socketry login --email you@example.com --password 'yourpass'
67
+
68
+ # List devices and select the active one
69
+ socketry devices
70
+ socketry select 0
71
+ ```
72
+
73
+ Credentials are saved to `~/.config/socketry/credentials.json` (mode 0600).
74
+
75
+ ### Reading properties (`get`)
76
+
77
+ ```bash
78
+ # All properties (colored + grouped on a TTY)
79
+ socketry get
80
+
81
+ # Single property — by CLI name or raw protocol key
82
+ socketry get battery # Battery: 85%
83
+ socketry get rb # Battery: 85% (same thing)
84
+ socketry get ac # AC output: ON
85
+
86
+ # JSON output (indented on TTY, compact when piped)
87
+ socketry get --json
88
+ socketry get ac --json # {"oac": 1}
89
+ socketry get --json | jq .rb # pipe-friendly
90
+ ```
91
+
92
+ Available properties:
93
+
94
+ | Group | Names |
95
+ |-------|-------|
96
+ | Battery & Power | `battery`, `battery-temp`, `battery-state`, `input-power`, `output-power`, `input-time`, `output-time` |
97
+ | I/O State | `ac`, `dc`, `usb`, `car`, `ac-in`, `dc-in`, `light`, `wireless` |
98
+ | Settings | `charge-speed`, `auto-shutdown`, `energy-saving`, `battery-protection`, `sfc`, `ups`, `screen-timeout` |
99
+ | AC / Power Detail | `ac-input-power`, `car-input-power`, `ac-voltage`, `ac-freq`, `ac-power`, `ac-power-2`, `ac-socket-power` |
100
+ | Other / Alarms | `error-code`, `temp-alarm`, `power-alarm`, `power-mode-battery`, `total-temp`, `system-status`, `power-capacity` |
101
+
102
+ Raw protocol keys (`rb`, `oac`, `bt`, ...) are also accepted.
103
+
104
+ ### Changing settings (`set`)
105
+
106
+ ```bash
107
+ # I/O toggles
108
+ socketry set ac on
109
+ socketry set dc off
110
+ socketry set usb on
111
+ socketry set car off
112
+
113
+ # Light
114
+ socketry set light high # off | low | high | sos
115
+
116
+ # Device settings
117
+ socketry set charge-speed mute # fast | mute
118
+ socketry set battery-protection eco # full | eco
119
+ socketry set ups on
120
+ socketry set sfc on
121
+
122
+ # Integer settings
123
+ socketry set screen-timeout 30
124
+ socketry set auto-shutdown 60
125
+ socketry set energy-saving 30
126
+
127
+ # Wait for device confirmation
128
+ socketry set ac on --wait
129
+
130
+ # Show available settings
131
+ socketry set
132
+ socketry set light # "expects a value: off | low | high | sos"
133
+ ```
134
+
135
+ Writable settings:
136
+
137
+ | Setting | Values | Description |
138
+ |---------|--------|-------------|
139
+ | `ac` | on / off | AC output |
140
+ | `dc` | on / off | DC output |
141
+ | `usb` | on / off | USB output |
142
+ | `car` | on / off | Car (12V) output |
143
+ | `ac-in` | on / off | AC input |
144
+ | `dc-in` | on / off | DC input |
145
+ | `light` | off / low / high / sos | Light mode |
146
+ | `screen-timeout` | integer | Screen timeout |
147
+ | `auto-shutdown` | integer | Auto shutdown timer |
148
+ | `charge-speed` | fast / mute | Charge speed mode |
149
+ | `battery-protection` | full / eco | Battery protection level |
150
+ | `energy-saving` | integer | Energy saving timeout |
151
+ | `sfc` | on / off | Super fast charge |
152
+ | `ups` | on / off | UPS mode |
153
+
154
+ ## Library usage
155
+
156
+ ```python
157
+ from socketry import Client
158
+
159
+ # Authenticate (or load saved credentials)
160
+ client = Client.login("email@example.com", "password")
161
+ client.save_credentials()
162
+
163
+ # Or load previously saved credentials
164
+ client = Client.from_saved()
165
+
166
+ # List and select devices
167
+ devices = client.fetch_devices()
168
+ client.select_device(0)
169
+
170
+ # Read properties
171
+ props = client.get_all_properties()
172
+ setting, value = client.get_property("battery")
173
+ print(f"{setting.name}: {setting.format_value(value)}")
174
+
175
+ # Control
176
+ client.set_property("ac", "on")
177
+ result = client.set_property("light", "high", wait=True)
178
+ ```
179
+
180
+ ## How it works
181
+
182
+ ```
183
+ socketry ──HTTP──> iot.jackeryapp.com (login, device list, properties)
184
+ socketry ──MQTT──> emqx.jackeryapp.com (device control via encrypted TLS)
185
+ ```
186
+
187
+ Login uses AES-192/ECB + RSA-1024 encrypted HTTP POST. Device control commands
188
+ are published over MQTT (TLS 1.2 with a self-signed CA). Status polling uses the
189
+ HTTP property endpoint. See [docs/protocol.md](docs/protocol.md) for the full
190
+ protocol specification.
191
+
192
+ ## Roadmap
193
+
194
+ - [ ] MQTT real-time monitor (subscribe to live property changes)
195
+ - [ ] Token auto-refresh (JWT expires ~30 days)
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,115 @@
1
+ [project]
2
+ name = "socketry"
3
+ version = "0.1.0"
4
+ description = "Python API and CLI for controlling Jackery portable power stations"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Jesus Lopez", email = "jesus@jesusla.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ dependencies = [
12
+ "typer>=0.9",
13
+ "paho-mqtt>=2.0",
14
+ "requests>=2.31",
15
+ "pycryptodome>=3.19",
16
+ ]
17
+
18
+ [project.scripts]
19
+ socketry = "socketry.cli:app"
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.9.15,<0.10.0"]
23
+ build-backend = "uv_build"
24
+
25
+ [dependency-groups]
26
+ dev = [
27
+ "mypy>=1.19.1",
28
+ "pre-commit>=4.5.1",
29
+ "pytest>=9.0.2",
30
+ "pytest-cov>=6.0.0",
31
+ "ruff>=0.14.14",
32
+ "types-requests>=2.31.0",
33
+ ]
34
+
35
+ [tool.ruff]
36
+ line-length = 100
37
+ target-version = "py311"
38
+ exclude = [
39
+ ".git",
40
+ ".mypy_cache",
41
+ ".pytest_cache",
42
+ ".ruff_cache",
43
+ ".venv",
44
+ "venv",
45
+ "__pycache__",
46
+ "build",
47
+ "dist",
48
+ ]
49
+
50
+ [tool.ruff.lint]
51
+ select = [
52
+ "E", # pycodestyle errors
53
+ "W", # pycodestyle warnings
54
+ "F", # pyflakes
55
+ "I", # isort
56
+ "B", # flake8-bugbear
57
+ "C4", # flake8-comprehensions
58
+ "SIM", # flake8-simplify
59
+ "UP", # pyupgrade
60
+ ]
61
+ ignore = []
62
+ fixable = ["ALL"]
63
+ unfixable = []
64
+
65
+ [tool.ruff.format]
66
+ quote-style = "double"
67
+ indent-style = "space"
68
+ line-ending = "auto"
69
+
70
+ [tool.mypy]
71
+ python_version = "3.11"
72
+ strict = true
73
+ warn_return_any = true
74
+ warn_unused_configs = true
75
+ disallow_untyped_defs = true
76
+ disallow_incomplete_defs = true
77
+ check_untyped_defs = true
78
+ no_implicit_optional = true
79
+ warn_redundant_casts = true
80
+ warn_unused_ignores = true
81
+ warn_no_return = true
82
+ ignore_missing_imports = false
83
+ disable_error_code = ["import-untyped"]
84
+
85
+ [[tool.mypy.overrides]]
86
+ module = ["tests.*"]
87
+ disallow_untyped_defs = false
88
+
89
+ [tool.pytest.ini_options]
90
+ testpaths = ["tests"]
91
+ python_files = ["test_*.py"]
92
+ python_classes = ["Test*"]
93
+ python_functions = ["test_*"]
94
+ addopts = [
95
+ "-v",
96
+ "--cov=socketry",
97
+ "--cov-report=term-missing",
98
+ "--cov-report=html",
99
+ "--cov-report=json",
100
+ ]
101
+
102
+ [tool.coverage.run]
103
+ source = ["src/socketry"]
104
+ omit = ["tests/*", "**/__pycache__/*"]
105
+
106
+ [tool.coverage.report]
107
+ fail_under = 20
108
+ exclude_lines = [
109
+ "pragma: no cover",
110
+ "def __repr__",
111
+ "raise AssertionError",
112
+ "raise NotImplementedError",
113
+ "if __name__ == .__main__.:",
114
+ "if TYPE_CHECKING:",
115
+ ]
@@ -0,0 +1,6 @@
1
+ """Python API and CLI for controlling Jackery portable power stations."""
2
+
3
+ from socketry.client import Client
4
+ from socketry.properties import MODEL_NAMES, PROPERTIES, Setting
5
+
6
+ __all__ = ["Client", "MODEL_NAMES", "PROPERTIES", "Setting"]
@@ -0,0 +1,5 @@
1
+ """Allow running with ``python -m socketry``."""
2
+
3
+ from socketry.cli import app
4
+
5
+ app()
@@ -0,0 +1,52 @@
1
+ """Internal constants extracted from the decompiled Jackery APK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ API_BASE = "https://iot.jackeryapp.com/v1"
8
+
9
+ MQTT_HOST = "emqx.jackeryapp.com"
10
+ MQTT_PORT = 8883
11
+
12
+ RSA_PUBLIC_KEY_B64 = (
13
+ "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVmzgJy/4XolxPnkfu32YtJqYG"
14
+ "FLYqf9/rnVgURJED+8J9J3Pccd6+9L97/+7COZE5OkejsgOkqeLNC9C3r5mhpE4z"
15
+ "k/HStss7Q8/5DqkGD1annQ+eoICo3oi0dITZ0Qll56Dowb8lXi6WHViVDdih/oeU"
16
+ "wVJY89uJNtTWrz7t7QIDAQAB"
17
+ )
18
+
19
+ # Self-signed CA cert for emqx.jackeryapp.com (from APK res/raw/ca.crt)
20
+ CA_CERT_PEM = """\
21
+ -----BEGIN CERTIFICATE-----
22
+ MIIDtTCCAp2gAwIBAgIJAPvYSRLMmPACMA0GCSqGSIb3DQEBCwUAMHAxCzAJBgNV
23
+ BAYTAkNOMRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMRQw
24
+ EgYDVQQKDAtqYWNrZXJ5LmNvbTELMAkGA1UECwwCY2ExFzAVBgNVBAMMDmNhLmph
25
+ Y2tlcnkuY29tMCAXDTIyMTIyMzEwMTc0N1oYDzIwNzcwOTI1MTAxNzQ3WjBwMQsw
26
+ CQYDVQQGEwJDTjESMBAGA1UECAwJR3Vhbmdkb25nMREwDwYDVQQHDAhTaGVuemhl
27
+ bjEUMBIGA1UECgwLamFja2VyeS5jb20xCzAJBgNVBAsMAmNhMRcwFQYDVQQDDA5j
28
+ YS5qYWNrZXJ5LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOrf
29
+ QltVp+PDphQ20tfQbCh/YlqIAK8VkIcYXq7DlVsX1HGl5x+6UkEahzLRtZFaWRkH
30
+ HiHSvol8I+cvq6BHte0VsjKAzl7Mae7P/UyQXwpgNa+hliZHoEqflghzYvxjlZeP
31
+ eOGGcHxg1p2M8+PeNWkX5VkVTSYi/abDz86+D5y1gq7S8n+tYk1WhKFHvIrfX3nN
32
+ 4QXfDO7vAQMd1uc6YdDqRanWjxIgOSDk9B+Mblz0TxCR+hnuDDQpAE4ONjByjArS
33
+ MC/QS8BIq/TL6nixzA8y0vOHySmuOLfuhFpNoO2mujhBGN/Dmq/pZwmsKSK91PxE
34
+ dn3YO8N8q7flHd/Qw4UCAwEAAaNQME4wHQYDVR0OBBYEFL/rQk0x4WclVgw3WLsl
35
+ YH3k0dvgMB8GA1UdIwQYMBaAFL/rQk0x4WclVgw3WLslYH3k0dvgMAwGA1UdEwQF
36
+ MAMBAf8wDQYJKoZIhvcNAQELBQADggEBALZM+xA4bUnO/7/0giZ3xUPEKzwFDp4G
37
+ 5UPI/5grLYxp38t2M84tlJ94W/HKH+f1CYbJ6m28dSZfWtnRzQ3Tgq0whrsmYiK9
38
+ 1Txcl3HPBiL7yAn3yE8DjHV+S2eSnN0o26/rcXCe+9bghSqqGaVDOJyk+Fm4l17e
39
+ Hzx99PvPGkpGUglun3UEp/Vp5ZUl9uDYT813HJ9jK80i1MDlzBJWmg7gzh27/Qls
40
+ UJLtYvgsxiBKAnK8YkAyu51Jm8uLz1BZ1RANf22vv0QUTW+SGdgc5Q1h610G9N1i
41
+ 4BaijfWnto9ka32QKgZA0gHXsT3wiwdbEow0lp7y40aiXq4kazDT7ws=
42
+ -----END CERTIFICATE-----
43
+ """
44
+
45
+ CRED_DIR = Path.home() / ".config" / "socketry"
46
+ CRED_FILE = CRED_DIR / "credentials.json"
47
+
48
+ APP_HEADERS: dict[str, str] = {
49
+ "platform": "2",
50
+ "app_version": "1.2.0",
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ }
@@ -0,0 +1,51 @@
1
+ """Internal cryptographic helpers for Jackery API authentication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import uuid
7
+
8
+ from Crypto.Cipher import AES, PKCS1_v1_5
9
+ from Crypto.PublicKey import RSA
10
+ from Crypto.Util.Padding import pad
11
+
12
+
13
+ def aes_ecb_encrypt(plaintext: bytes, key: bytes) -> bytes:
14
+ """AES/ECB/PKCS5Padding encryption (used for HTTP login body)."""
15
+ cipher = AES.new(key, AES.MODE_ECB)
16
+ result: bytes = cipher.encrypt(pad(plaintext, AES.block_size))
17
+ return result
18
+
19
+
20
+ def rsa_encrypt(plaintext: bytes, pub_key_b64: str) -> bytes:
21
+ """RSA/ECB/PKCS1Padding encryption (used for encrypting AES key)."""
22
+ der = base64.b64decode(pub_key_b64)
23
+ key = RSA.import_key(der)
24
+ cipher = PKCS1_v1_5.new(key)
25
+ result: bytes = cipher.encrypt(plaintext)
26
+ return result
27
+
28
+
29
+ def aes_cbc_encrypt(plaintext: bytes, key: bytes, iv: bytes) -> bytes:
30
+ """AES/CBC/PKCS5Padding encryption (used for MQTT password derivation)."""
31
+ cipher = AES.new(key, AES.MODE_CBC, iv)
32
+ result: bytes = cipher.encrypt(pad(plaintext, AES.block_size))
33
+ return result
34
+
35
+
36
+ def derive_mqtt_password(username: str, mqtt_password_b64: str) -> str:
37
+ """Derive the MQTT connection password from the stored mqttPassWord.
38
+
39
+ The app does: AES/CBC/PKCS5Padding(username, key=b64decode(mqttPassWord), iv=key[:16])
40
+ then base64-encodes the result and sends it as the MQTT password string.
41
+ """
42
+ key = base64.b64decode(mqtt_password_b64)
43
+ iv = key[:16]
44
+ encrypted = aes_cbc_encrypt(username.encode("utf-8"), key, iv)
45
+ return base64.b64encode(encrypted).decode("ascii")
46
+
47
+
48
+ def get_mac_id() -> str:
49
+ """Generate a stable MAC-like identifier for this machine."""
50
+ node = uuid.getnode()
51
+ return ":".join(f"{(node >> (8 * i)) & 0xFF:02x}" for i in range(5, -1, -1))