esp-rainmaker-cli 1.14.0__tar.gz → 1.14.1__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.
Files changed (126) hide show
  1. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/PKG-INFO +14 -1
  2. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/PKG-INFO +14 -1
  3. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rainmaker/rainmaker.py +12 -0
  4. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rainmaker/version.py +1 -1
  5. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/node.py +22 -2
  6. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/provision.py +9 -2
  7. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/node_cache.py +4 -1
  8. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security2.py +66 -0
  9. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/integration.py +105 -21
  10. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/raw_config.py +4 -2
  11. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/raw_params.py +5 -2
  12. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/esp_rainmaker_prov.py +35 -16
  13. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/LICENSE +0 -0
  14. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/README.md +0 -0
  15. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/SOURCES.txt +0 -0
  16. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/dependency_links.txt +0 -0
  17. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/entry_points.txt +0 -0
  18. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/requires.txt +0 -0
  19. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/esp_rainmaker_cli.egg-info/top_level.txt +0 -0
  20. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rainmaker/__init__.py +0 -0
  21. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/__init__.py +0 -0
  22. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/automation.py +0 -0
  23. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/browserlogin.py +0 -0
  24. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/cache.py +0 -0
  25. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/cmd_response.py +0 -0
  26. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/group.py +0 -0
  27. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/html/welcome.html +0 -0
  28. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/stream.py +0 -0
  29. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/test.py +0 -0
  30. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_cmd/user.py +0 -0
  31. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/__init__.py +0 -0
  32. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/aws_credentials.py +0 -0
  33. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/cmd_response.py +0 -0
  34. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/configmanager.py +0 -0
  35. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/constants.py +0 -0
  36. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/device.py +0 -0
  37. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/envval.py +0 -0
  38. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/exceptions.py +0 -0
  39. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/kvs_streaming.py +0 -0
  40. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/local_control.py +0 -0
  41. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/logger.py +0 -0
  42. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/node.py +0 -0
  43. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/profile_manager.py +0 -0
  44. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/profile_utils.py +0 -0
  45. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/schedule_utils.py +0 -0
  46. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/serverconfig.py +0 -0
  47. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/service.py +0 -0
  48. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/session.py +0 -0
  49. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/session_store.py +0 -0
  50. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/simple_local_control.py +0 -0
  51. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_lib/user.py +0 -0
  52. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/__init__.py +0 -0
  53. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/__init__.py +0 -0
  54. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/discovery/__init__.py +0 -0
  55. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/discovery/mdns_discovery.py +0 -0
  56. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/__init__.py +0 -0
  57. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_ctrl.py +0 -0
  58. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_prov.py +0 -0
  59. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/prov/wifi_scan.py +0 -0
  60. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/__init__.py +0 -0
  61. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security.py +0 -0
  62. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security0.py +0 -0
  63. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/security1.py +0 -0
  64. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/security/srp6a.py +0 -0
  65. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/__init__.py +0 -0
  66. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/ble_cli.py +0 -0
  67. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport.py +0 -0
  68. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_ble.py +0 -0
  69. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_console.py +0 -0
  70. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/transport/transport_http.py +0 -0
  71. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/utils/__init__.py +0 -0
  72. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/common/utils/convenience.py +0 -0
  73. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/__init__.py +0 -0
  74. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/claim.py +0 -0
  75. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_claim/claim_config.py +0 -0
  76. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/__init__.py +0 -0
  77. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/constants_pb2.py +0 -0
  78. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_local_ctrl.py +0 -0
  79. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_local_ctrl_pb2.py +0 -0
  80. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_prov.py +0 -0
  81. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_rainmaker_ctrl.py +0 -0
  82. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/esp_rmaker_prov_local_ctrl_pb2.py +0 -0
  83. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/proto/__init__.py +0 -0
  84. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/proto/proto_lc.py +0 -0
  85. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/__init__.py +0 -0
  86. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/custom_prov.py +0 -0
  87. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/prov/wifi_ctrl.py +0 -0
  88. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec0_pb2.py +0 -0
  89. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec1_pb2.py +0 -0
  90. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/sec2_pb2.py +0 -0
  91. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/security/__init__.py +0 -0
  92. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/session_pb2.py +0 -0
  93. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/transport/__init__.py +0 -0
  94. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_local_ctrl/utils/__init__.py +0 -0
  95. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/__init__.py +0 -0
  96. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/challenge_response.py +0 -0
  97. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/__init__.py +0 -0
  98. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/custom_cloud_config_pb2.py +0 -0
  99. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_chal_resp_pb2.py +0 -0
  100. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_claim_pb2.py +0 -0
  101. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/config/esp_rmaker_user_mapping_pb2.py +0 -0
  102. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/on_network_chal_resp.py +0 -0
  103. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/proto/__init__.py +0 -0
  104. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/__init__.py +0 -0
  105. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/__init__.py +0 -0
  106. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/constants_pb2.py +0 -0
  107. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec0_pb2.py +0 -0
  108. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec1_pb2.py +0 -0
  109. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/sec2_pb2.py +0 -0
  110. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/protocomm/python/session_pb2.py +0 -0
  111. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/__init__.py +0 -0
  112. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/prov_util.py +0 -0
  113. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/prov/user_mapping.py +0 -0
  114. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/security/__init__.py +0 -0
  115. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/transport/__init__.py +0 -0
  116. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/utils/__init__.py +0 -0
  117. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/__init__.py +0 -0
  118. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/__init__.py +0 -0
  119. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_config_pb2.py +0 -0
  120. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_constants_pb2.py +0 -0
  121. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_ctrl_pb2.py +0 -0
  122. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/rmaker_tools/rmaker_prov/wifi_provisioning/python/wifi_scan_pb2.py +0 -0
  123. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/server_cert/__init__.py +0 -0
  124. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/server_cert/server_cert.pem +0 -0
  125. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/setup.cfg +0 -0
  126. {esp_rainmaker_cli-1.14.0 → esp_rainmaker_cli-1.14.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esp-rainmaker-cli
3
- Version: 1.14.0
3
+ Version: 1.14.1
4
4
  Summary: A python utility to perform host based claiming
5
5
  Home-page: https://github.com/espressif/esp-rainmaker-cli
6
6
  Author: Espressif Systems
@@ -122,6 +122,19 @@ Changelog
122
122
 
123
123
  All major changes to ESP RainMaker CLI will be documented in this file.
124
124
 
125
+ ## [1.14.1] - 05-Jun-2026
126
+ ### Bugfixes
127
+ - Security 2 fixes across provisioning and local control:
128
+ - Provisioning auto-detect now reads `prov.sec_ver` from device capabilities
129
+ (was forcing sec1 on sec2 devices, causing firmware "Security version mismatch")
130
+ - `sec_patch_ver` discovered from device capabilities / `esp_local_ctrl/version`,
131
+ enabling the counter-in-IV scheme on ESP-IDF v5.4+ firmware
132
+ - `--sec2_username` option added to `--local` commands (`getnodeconfig`,
133
+ `getparams`, `setparams`); defaults to `wifiprov`, picks up cached
134
+ `local_control_username` from cloud params; `--pop` used as the SRP6a password
135
+ - QR code `username` / `password` fields now honoured by `provision`
136
+ - `Security2.serialize`/`deserialize` for local-control session resume parity with Security 1
137
+
125
138
  ## [1.14.0] - 24-Apr-2026
126
139
  ### Added
127
140
  - New `group sharing` subcommand for sharing device groups / Matter fabrics between users:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esp-rainmaker-cli
3
- Version: 1.14.0
3
+ Version: 1.14.1
4
4
  Summary: A python utility to perform host based claiming
5
5
  Home-page: https://github.com/espressif/esp-rainmaker-cli
6
6
  Author: Espressif Systems
@@ -122,6 +122,19 @@ Changelog
122
122
 
123
123
  All major changes to ESP RainMaker CLI will be documented in this file.
124
124
 
125
+ ## [1.14.1] - 05-Jun-2026
126
+ ### Bugfixes
127
+ - Security 2 fixes across provisioning and local control:
128
+ - Provisioning auto-detect now reads `prov.sec_ver` from device capabilities
129
+ (was forcing sec1 on sec2 devices, causing firmware "Security version mismatch")
130
+ - `sec_patch_ver` discovered from device capabilities / `esp_local_ctrl/version`,
131
+ enabling the counter-in-IV scheme on ESP-IDF v5.4+ firmware
132
+ - `--sec2_username` option added to `--local` commands (`getnodeconfig`,
133
+ `getparams`, `setparams`); defaults to `wifiprov`, picks up cached
134
+ `local_control_username` from cloud params; `--pop` used as the SRP6a password
135
+ - QR code `username` / `password` fields now honoured by `provision`
136
+ - `Security2.serialize`/`deserialize` for local-control session resume parity with Security 1
137
+
125
138
  ## [1.14.0] - 24-Apr-2026
126
139
  ### Added
127
140
  - New `group sharing` subcommand for sharing device groups / Matter fabrics between users:
@@ -285,6 +285,10 @@ def main():
285
285
  choices=[0, 1, 2],
286
286
  default=None,
287
287
  help='Security version for local control (default: 1)')
288
+ getnodeconfig_parser.add_argument('--sec2_username',
289
+ type=str,
290
+ default=None,
291
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
288
292
  getnodeconfig_parser.add_argument('--local-raw',
289
293
  action='store_true',
290
294
  help='Use local control via raw endpoints (get_config with fragmentation) instead of esp_local_ctrl')
@@ -355,6 +359,10 @@ def main():
355
359
  choices=[0, 1, 2],
356
360
  default=None,
357
361
  help='Security version for local control (default: 1)')
362
+ setparams_parser.add_argument('--sec2_username',
363
+ type=str,
364
+ default=None,
365
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
358
366
  setparams_parser.add_argument('--local-raw',
359
367
  action='store_true',
360
368
  help='Use local control via raw endpoints (get_params/set_params) instead of esp_local_ctrl')
@@ -398,6 +406,10 @@ def main():
398
406
  choices=[0, 1, 2],
399
407
  default=None,
400
408
  help='Security version for local control (default: 1)')
409
+ getparams_parser.add_argument('--sec2_username',
410
+ type=str,
411
+ default=None,
412
+ help='SRP6a username for Security 2 local control. Defaults to the username cached from cloud params, else \'wifiprov\'. --pop is used as the password.')
401
413
  getparams_parser.add_argument('--local-raw',
402
414
  action='store_true',
403
415
  help='Use local control via raw endpoints (get_params/set_params) instead of esp_local_ctrl')
@@ -5,4 +5,4 @@
5
5
  # SPDX-License-Identifier: Apache-2.0
6
6
 
7
7
  # This file contains the version information for the ESP RainMaker CLI
8
- VERSION = "1.14.0"
8
+ VERSION = "1.14.1"
@@ -78,6 +78,9 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
78
78
  sec_ver = raw_sec_ver if raw_sec_ver is not None else 1
79
79
  nodeid = vars_dict.get('nodeid', '')
80
80
 
81
+ user_sec2_username = vars_dict.get('sec2_username') # None => not supplied
82
+ sec2_username = user_sec2_username
83
+
81
84
  if node_cache and not pop:
82
85
  capability = node_cache.get_local_control_capability(nodeid)
83
86
  if capability:
@@ -99,6 +102,9 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
99
102
  log.debug(f"Using cached POP for node {nodeid}")
100
103
  if lc_info.get('sec_ver') is not None and not explicit_sec_ver:
101
104
  sec_ver = lc_info['sec_ver']
105
+ if sec2_username is None and lc_info.get('username'):
106
+ sec2_username = lc_info['username']
107
+ log.debug(f"Using cached sec2 username for node {nodeid}")
102
108
 
103
109
  if not pop and not explicit_sec_ver:
104
110
  try:
@@ -114,15 +120,23 @@ def _build_local_options_with_cache(vars_dict, node_cache=None, session_store=No
114
120
  pop = lc_info.get('pop', '')
115
121
  if lc_info.get('sec_ver') is not None:
116
122
  sec_ver = lc_info['sec_ver']
123
+ if sec2_username is None and lc_info.get('username'):
124
+ sec2_username = lc_info['username']
117
125
  log.debug(f"Resolved POP from cloud for node {nodeid}")
118
126
  except Exception as e:
119
127
  log.debug(f"Failed to auto-resolve POP from cloud: {e}")
120
128
 
129
+ if not sec2_username:
130
+ sec2_username = 'wifiprov'
131
+ if sec_ver == 2:
132
+ print(f"Using default Security 2 username '{sec2_username}'")
133
+
121
134
  local_options = {
122
135
  'pop': pop,
123
136
  'transport': vars_dict.get('transport', 'http'),
124
137
  'port': vars_dict.get('port', 8080),
125
138
  'sec_ver': sec_ver,
139
+ 'sec2_username': sec2_username,
126
140
  'local_raw': vars_dict.get('local_raw', False),
127
141
  'device_name': vars_dict.get('device_name', None),
128
142
  'node_cache': node_cache,
@@ -1259,6 +1273,7 @@ def get_node_config(vars=None):
1259
1273
  'transport': vars.get('transport', 'ble'),
1260
1274
  'port': vars.get('port', 8080),
1261
1275
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1276
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1262
1277
  'device_name': vars.get('device_name', None),
1263
1278
  'timestamp': timestamp
1264
1279
  }
@@ -1312,7 +1327,8 @@ def get_node_config(vars=None):
1312
1327
  'pop': vars.get('pop', ''),
1313
1328
  'transport': vars.get('transport', 'http'),
1314
1329
  'port': vars.get('port', 8080),
1315
- 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1
1330
+ 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1331
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1316
1332
  }
1317
1333
 
1318
1334
  node_config = run_local_control_operation(
@@ -1326,7 +1342,8 @@ def get_node_config(vars=None):
1326
1342
  'pop': vars.get('pop', ''),
1327
1343
  'transport': vars.get('transport', 'http'),
1328
1344
  'port': vars.get('port', 8080),
1329
- 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0
1345
+ 'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1346
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1330
1347
  }
1331
1348
 
1332
1349
  log.info("Using simplified local control (security level 0)")
@@ -1567,6 +1584,7 @@ def set_params(vars=None):
1567
1584
  'transport': vars.get('transport', 'http'),
1568
1585
  'port': vars.get('port', 8080),
1569
1586
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1587
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1570
1588
  'local_raw': vars.get('local_raw', False),
1571
1589
  'device_name': vars.get('device_name', None)
1572
1590
  }
@@ -1753,6 +1771,7 @@ def get_params(vars=None):
1753
1771
  'transport': vars.get('transport', 'http'),
1754
1772
  'port': vars.get('port', 8080),
1755
1773
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 1,
1774
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1756
1775
  'local_raw': vars.get('local_raw', False),
1757
1776
  'device_name': vars.get('device_name', None),
1758
1777
  'timestamp': timestamp if not (proxy_report and vars.get('local_raw', False) and not timestamp_explicitly_provided) else timestamp,
@@ -1779,6 +1798,7 @@ def get_params(vars=None):
1779
1798
  'transport': vars.get('transport', 'http'),
1780
1799
  'port': vars.get('port', 8080),
1781
1800
  'sec_ver': vars.get('sec_ver') if vars.get('sec_ver') is not None else 0,
1801
+ 'sec2_username': vars.get('sec2_username', 'wifiprov') or 'wifiprov',
1782
1802
  'local_raw': vars.get('local_raw', False),
1783
1803
  'device_name': vars.get('device_name', None),
1784
1804
  'timestamp': timestamp if not (proxy_report and vars.get('local_raw', False) and not timestamp_explicitly_provided) else timestamp,
@@ -117,8 +117,15 @@ def provision(vars=None):
117
117
  raise ValueError("Proof of possession (pop) may be required depending on security scheme. "
118
118
  "Use --pop, provide via --qrcode, or specify --sec_ver to skip pop requirement "
119
119
  "(Security 0 and 2 don't require pop).")
120
- sec2_username = vars.get('sec2_username', '')
121
- sec2_password = vars.get('sec2_password', '')
120
+
121
+ # sec2 credentials: explicit options override QR code values.
122
+ # QR code fields: 'username' and 'password' (sec2 SRP6a credentials).
123
+ sec2_username = (vars.get('sec2_username')
124
+ or qrcode_data.get('username')
125
+ or '')
126
+ sec2_password = (vars.get('sec2_password')
127
+ or qrcode_data.get('password')
128
+ or '')
122
129
  ssid = vars.get('ssid')
123
130
  passphrase = vars.get('passphrase')
124
131
  no_wifi = vars.get('no_wifi', False)
@@ -56,7 +56,7 @@ def _get_cache_base_dir(profile_config=None):
56
56
 
57
57
  def extract_local_control_info(params_or_details):
58
58
  """
59
- Extract local control info (POP, sec_ver, transport, port) from
59
+ Extract local control info (POP, sec_ver, username, transport, port) from
60
60
  node params or node details response data.
61
61
 
62
62
  Scans for a service/device named "Local Control" or similar that
@@ -74,6 +74,9 @@ def extract_local_control_info(params_or_details):
74
74
  lower_key = key.lower().replace(' ', '_').replace('-', '_')
75
75
  if lower_key in ('pop', 'proof_of_possession'):
76
76
  info['pop'] = str(value)
77
+ elif lower_key in ('username', 'user_name', 'local_control_username'):
78
+ if value:
79
+ info['username'] = str(value)
77
80
  elif lower_key == 'sec_ver':
78
81
  try:
79
82
  info['sec_ver'] = int(value)
@@ -2,6 +2,8 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  # APIs for interpreting and creating protobuf packets for
4
4
  # protocomm endpoint with security type protocomm_security2
5
+ import base64
6
+ import hashlib
5
7
  import struct
6
8
  import sys
7
9
  import os
@@ -75,6 +77,7 @@ class Security2(Security):
75
77
 
76
78
  self.client_pop_key = None
77
79
  self.nonce = bytearray()
80
+ self.session_key = None
78
81
 
79
82
  Security.__init__(self, self.security2_session)
80
83
 
@@ -170,6 +173,7 @@ class Security2(Security):
170
173
 
171
174
  # Using the first 256 bits of a 512 bit key
172
175
  session_key = shared_secret[:AES_KEY_LEN]
176
+ self.session_key = session_key
173
177
  self._print_verbose(f'Session Key:\t0x{session_key.hex()}')
174
178
 
175
179
  # 96-bit nonce
@@ -203,3 +207,65 @@ class Security2(Security):
203
207
  plaintext = self.cipher.decrypt(self.nonce, data, None)
204
208
  self._increment_nonce()
205
209
  return plaintext
210
+
211
+ @staticmethod
212
+ def _creds_hash(username: str, password: str) -> str:
213
+ h = hashlib.sha256()
214
+ h.update(str_to_bytes(username or ''))
215
+ h.update(b'\x00')
216
+ h.update(str_to_bytes(password or ''))
217
+ return h.hexdigest()
218
+
219
+ def serialize(self) -> Any:
220
+ """
221
+ Serialize session crypto state for disk persistence.
222
+ Returns a dict suitable for JSON serialization.
223
+ """
224
+ if self.session_key is None or not self.nonce:
225
+ return None
226
+ return {
227
+ 'sec_ver': 2,
228
+ 'sec_patch_ver': self.sec_patch_ver,
229
+ 'session_key': base64.b64encode(self.session_key).decode('ascii'),
230
+ 'nonce': base64.b64encode(bytes(self.nonce)).decode('ascii'),
231
+ 'creds_hash': Security2._creds_hash(self.username, self.password),
232
+ }
233
+
234
+ @classmethod
235
+ def deserialize(cls, data: dict, username: str = '', password: str = '',
236
+ verbose: bool = False) -> Any:
237
+ """
238
+ Restore a Security2 object from serialized session data.
239
+ Recreates the AES-GCM cipher and restores the nonce counter.
240
+
241
+ :param data: dict from serialize() / session.json
242
+ :param username: SRP6a username (used for creds_hash validation)
243
+ :param password: SRP6a password (used for creds_hash validation)
244
+ :param verbose: verbose flag
245
+ :return: Security2 instance, or None on hash mismatch / malformed data
246
+ """
247
+ try:
248
+ session_key = base64.b64decode(data['session_key'])
249
+ nonce = bytearray(base64.b64decode(data['nonce']))
250
+ sec_patch_ver = int(data.get('sec_patch_ver', 0))
251
+ except (KeyError, ValueError, Exception):
252
+ return None
253
+
254
+ saved_hash = data.get('creds_hash', '')
255
+ if saved_hash:
256
+ if cls._creds_hash(username, password) != saved_hash:
257
+ return None
258
+
259
+ obj = cls.__new__(cls)
260
+ obj.session_state = security_state.FINISHED
261
+ obj.sec_patch_ver = sec_patch_ver
262
+ obj.username = username
263
+ obj.password = password
264
+ obj.verbose = verbose
265
+ obj.client_pop_key = None
266
+ obj.nonce = nonce
267
+ obj.session_key = session_key
268
+ obj.cipher = AESGCM(session_key)
269
+
270
+ Security.__init__(obj, obj.security2_session)
271
+ return obj
@@ -31,10 +31,14 @@ async def _probe_session(transport_obj, security_obj):
31
31
  return False
32
32
 
33
33
 
34
- async def _try_resume_session(nodeid, pop, transport_type, session_store):
34
+ async def _try_resume_session(nodeid, pop, transport_type, session_store,
35
+ sec2_username='wifiprov'):
35
36
  """
36
37
  Attempt to resume a saved session from disk.
37
38
  Returns (transport_obj, security_obj) on success, (None, None) on failure.
39
+
40
+ For sec_ver=2 sessions, sec2_username is used with pop (as the SRP6a password)
41
+ to validate the saved creds_hash.
38
42
  """
39
43
  if session_store is None:
40
44
  return None, None
@@ -102,12 +106,20 @@ async def _try_resume_session(nodeid, pop, transport_type, session_store):
102
106
  return None, None
103
107
 
104
108
  try:
105
- from ..common.security.security1 import Security1
106
109
  from ..common.transport.transport_http import Transport_HTTP
107
110
 
108
- security_obj = Security1.deserialize(session_data, pop=pop, verbose=False)
111
+ saved_sec_ver = session_data.get('sec_ver')
112
+ if saved_sec_ver == 2:
113
+ from ..common.security.security2 import Security2
114
+ security_obj = Security2.deserialize(
115
+ session_data, username=sec2_username, password=pop, verbose=False
116
+ )
117
+ else:
118
+ from ..common.security.security1 import Security1
119
+ security_obj = Security1.deserialize(session_data, pop=pop, verbose=False)
120
+
109
121
  if security_obj is None:
110
- log.debug("Failed to deserialize security object (POP mismatch?)")
122
+ log.debug("Failed to deserialize security object (creds mismatch?)")
111
123
  session_store.invalidate_session(nodeid)
112
124
  return None, None
113
125
 
@@ -182,13 +194,54 @@ def _save_session_state(nodeid, transport_obj, security_obj, transport_type, sec
182
194
 
183
195
 
184
196
  def _update_session_offset(nodeid, security_obj, session_store):
185
- """Update the CTR offset in the saved session after an operation."""
197
+ """
198
+ Update the crypto state (CTR offset for sec1, nonce for sec2) in the saved
199
+ session after an operation, so a future resume picks up where we left off.
200
+ """
186
201
  if session_store is None:
187
202
  return
188
203
  try:
189
- session_store.update_session_offset(nodeid, security_obj.ctr_offset)
204
+ if hasattr(security_obj, 'ctr_offset') and not hasattr(security_obj, 'nonce'):
205
+ # Security1 fast-path — cheap partial update.
206
+ session_store.update_session_offset(nodeid, security_obj.ctr_offset)
207
+ return
208
+
209
+ # Security2 (or any future scheme): rewrite the serialized blob so the
210
+ # current nonce/counter is persisted.
211
+ if not hasattr(security_obj, 'serialize'):
212
+ return
213
+ data = session_store.load_session(nodeid)
214
+ if data is None:
215
+ return
216
+ sec_data = security_obj.serialize()
217
+ if sec_data is None:
218
+ return
219
+ data.update(sec_data)
220
+ session_store.save_session(nodeid, data)
221
+ except Exception as e:
222
+ log.debug(f"Failed to update session state: {e}")
223
+
224
+
225
+ def _probe_sec_patch_ver(transport_obj):
226
+ """
227
+ Query esp_local_ctrl/version to discover the device's sec_patch_ver.
228
+ Returns the advertised integer, or 0 if the endpoint is missing/parse fails
229
+ (legacy firmware).
230
+
231
+ protocomm_httpd rejects POSTs with content_len <= 0, so we send "---"
232
+ as a dummy body — the version handler ignores it.
233
+ """
234
+ try:
235
+ response = transport_obj.send_data('esp_local_ctrl/version', '---')
236
+ if isinstance(response, bytes):
237
+ response = response.decode('utf-8', errors='replace')
238
+ info = json.loads(response)
239
+ local_ctrl = info.get('local_ctrl') if isinstance(info, dict) else None
240
+ if isinstance(local_ctrl, dict) and 'sec_patch_ver' in local_ctrl:
241
+ return int(local_ctrl['sec_patch_ver'])
190
242
  except Exception as e:
191
- log.debug(f"Failed to update session offset: {e}")
243
+ log.debug(f"sec_patch_ver probe failed, defaulting to 0: {e}")
244
+ return 0
192
245
 
193
246
 
194
247
  async def _execute_operation(transport_obj, security_obj, operation, data):
@@ -204,11 +257,17 @@ async def _execute_operation(transport_obj, security_obj, operation, data):
204
257
  return None
205
258
 
206
259
 
207
- async def _fresh_establish(nodeid, pop, transport_type, sec_ver, port):
260
+ async def _fresh_establish(nodeid, pop, transport_type, sec_ver, port,
261
+ sec2_username='wifiprov', sec_patch_ver=None):
208
262
  """
209
263
  Establish a fresh transport + security session.
210
264
  Returns (transport_obj, security_obj, error_reason).
211
265
  error_reason is None on success, ERR_TRANSPORT or ERR_SECURITY on failure.
266
+
267
+ For sec_ver=2, `pop` is used as the SRP6a password. If sec_patch_ver is
268
+ None, the firmware is probed via esp_local_ctrl/version — newer firmware
269
+ advertises sec_patch_ver=1 (counter-in-IV), older firmware returns 0
270
+ (legacy static IV).
212
271
  """
213
272
  if ':' not in nodeid:
214
273
  if nodeid.endswith('.local'):
@@ -225,7 +284,14 @@ async def _fresh_establish(nodeid, pop, transport_type, sec_ver, port):
225
284
  log.error("Failed to establish transport")
226
285
  return None, None, ERR_TRANSPORT
227
286
 
228
- security_obj = get_security(sec_ver, 0, '', '', pop, False)
287
+ if sec_ver == 2 and sec_patch_ver is None:
288
+ sec_patch_ver = _probe_sec_patch_ver(transport_obj)
289
+ log.debug(f"Using sec_patch_ver={sec_patch_ver} for node {nodeid}")
290
+ if sec_patch_ver is None:
291
+ sec_patch_ver = 0
292
+
293
+ security_obj = get_security(sec_ver, sec_patch_ver,
294
+ sec2_username, pop, pop, False)
229
295
  if security_obj is None:
230
296
  log.error("Failed to setup security")
231
297
  return None, None, ERR_SECURITY
@@ -327,6 +393,8 @@ async def run_local_control_operation(nodeid, operation, data=None, **kwargs):
327
393
  node_cache = kwargs.get('node_cache', None)
328
394
  session_store = kwargs.get('session_store', None)
329
395
  explicit_sec_ver = kwargs.get('explicit_sec_ver', False)
396
+ sec2_username = kwargs.get('sec2_username', 'wifiprov') or 'wifiprov'
397
+ sec_patch_ver = None # discovered lazily in _fresh_establish when needed
330
398
 
331
399
  if node_cache:
332
400
  capability = node_cache.get_local_control_capability(nodeid)
@@ -341,12 +409,14 @@ async def run_local_control_operation(nodeid, operation, data=None, **kwargs):
341
409
  transport_type = capability.get('transport', transport_type)
342
410
  if sec_ver == 0:
343
411
  pop = ''
412
+ if capability and capability.get('sec_patch_ver') is not None:
413
+ sec_patch_ver = capability.get('sec_patch_ver')
344
414
 
345
415
  last_err = None
346
416
 
347
417
  try:
348
418
  transport_obj, security_obj = await _try_resume_session(
349
- nodeid, pop, transport_type, session_store
419
+ nodeid, pop, transport_type, session_store, sec2_username
350
420
  )
351
421
 
352
422
  resumed = transport_obj is not None
@@ -363,11 +433,13 @@ async def run_local_control_operation(nodeid, operation, data=None, **kwargs):
363
433
  return None
364
434
  else:
365
435
  transport_obj, security_obj, last_err = await _fresh_establish(
366
- nodeid, pop, transport_type, sec_ver, port
436
+ nodeid, pop, transport_type, sec_ver, port,
437
+ sec2_username, sec_patch_ver
367
438
  )
368
439
  else:
369
440
  transport_obj, security_obj, last_err = await _fresh_establish(
370
- nodeid, pop, transport_type, sec_ver, port
441
+ nodeid, pop, transport_type, sec_ver, port,
442
+ sec2_username, sec_patch_ver
371
443
  )
372
444
 
373
445
  if transport_obj is None or security_obj is None:
@@ -381,14 +453,25 @@ async def run_local_control_operation(nodeid, operation, data=None, **kwargs):
381
453
 
382
454
  if result is not None:
383
455
  _update_session_offset(nodeid, security_obj, session_store)
384
- if node_cache and not node_cache.get_local_control_capability(nodeid):
385
- node_cache.set_local_control_capability(nodeid, {
386
- 'supported': True,
387
- 'sec_ver': sec_ver,
388
- 'pop_required': bool(pop),
389
- 'transport': transport_type,
390
- 'port': port,
391
- })
456
+ if node_cache:
457
+ existing = node_cache.get_local_control_capability(nodeid)
458
+ obj_patch_ver = getattr(security_obj, 'sec_patch_ver', None)
459
+ if not existing:
460
+ cap = {
461
+ 'supported': True,
462
+ 'sec_ver': sec_ver,
463
+ 'pop_required': bool(pop),
464
+ 'transport': transport_type,
465
+ 'port': port,
466
+ }
467
+ if sec_ver == 2 and obj_patch_ver is not None:
468
+ cap['sec_patch_ver'] = obj_patch_ver
469
+ node_cache.set_local_control_capability(nodeid, cap)
470
+ elif (sec_ver == 2 and obj_patch_ver is not None
471
+ and existing.get('sec_patch_ver') != obj_patch_ver):
472
+ # Persist freshly-probed patch_ver so future connects skip the probe.
473
+ existing['sec_patch_ver'] = obj_patch_ver
474
+ node_cache.set_local_control_capability(nodeid, existing)
392
475
 
393
476
  if result is None and resumed:
394
477
  log.debug("Operation failed on resumed session, retrying with fresh session")
@@ -396,7 +479,8 @@ async def run_local_control_operation(nodeid, operation, data=None, **kwargs):
396
479
  session_store.invalidate_session(nodeid)
397
480
 
398
481
  transport_obj, security_obj, last_err = await _fresh_establish(
399
- nodeid, pop, transport_type, sec_ver, port
482
+ nodeid, pop, transport_type, sec_ver, port,
483
+ sec2_username, sec_patch_ver
400
484
  )
401
485
  if transport_obj is None or security_obj is None:
402
486
  _set_error(last_err or ERR_SECURITY)
@@ -524,6 +524,7 @@ async def run_raw_data_operation(nodeid, data_type, **kwargs):
524
524
  port = kwargs.get('port', None)
525
525
  device_name = kwargs.get('device_name', None)
526
526
  timestamp = kwargs.get('timestamp', None)
527
+ sec2_username = kwargs.get('sec2_username', 'wifiprov') or 'wifiprov'
527
528
  endpoint_name = 'get_params' if data_type == 0 else 'get_config'
528
529
  data_name = 'params' if data_type == 0 else 'config'
529
530
 
@@ -575,8 +576,9 @@ async def run_raw_data_operation(nodeid, data_type, **kwargs):
575
576
  if actual_sec_ver is None:
576
577
  actual_sec_ver = 1
577
578
 
578
- # Setup security
579
- security_obj = get_security(actual_sec_ver, sec_patch_ver, '', '', pop, False)
579
+ # Setup security. For sec_ver=2, pop is used as the SRP6a password.
580
+ security_obj = get_security(actual_sec_ver, sec_patch_ver,
581
+ sec2_username, pop, pop, False)
580
582
  if security_obj is None:
581
583
  print("Failed to setup security")
582
584
  return None
@@ -356,6 +356,7 @@ async def run_raw_params_operation(nodeid, operation, data=None, **kwargs):
356
356
  sec_ver = kwargs.get('sec_ver', 0) # Default to 0 for raw endpoints
357
357
  port = kwargs.get('port', None)
358
358
  device_name = kwargs.get('device_name', None)
359
+ sec2_username = kwargs.get('sec2_username', 'wifiprov') or 'wifiprov'
359
360
 
360
361
  # Build service name
361
362
  # For BLE transport, prefer device_name if provided, otherwise fallback to nodeid
@@ -414,8 +415,10 @@ async def run_raw_params_operation(nodeid, operation, data=None, **kwargs):
414
415
  if actual_sec_ver is None:
415
416
  actual_sec_ver = 1 # Default to Security 1
416
417
 
417
- # Setup security using detected/configured version
418
- security_obj = get_security(actual_sec_ver, sec_patch_ver, '', '', pop, False)
418
+ # Setup security using detected/configured version.
419
+ # For sec_ver=2, pop is used as the SRP6a password.
420
+ security_obj = get_security(actual_sec_ver, sec_patch_ver,
421
+ sec2_username, pop, pop, False)
419
422
  if security_obj is None:
420
423
  print("Failed to setup security")
421
424
  return None
@@ -240,32 +240,40 @@ def provision_device(transport_mode, pop, userid, secretkey,
240
240
  print("Establishing connection to node - Failed")
241
241
  return None
242
242
 
243
- # Auto-detect security version if not specified
244
- if security_version is None:
245
- # First check if capabilities are supported or not
246
- if not esp_prov.has_capability(obj_transport):
247
- print('Security capabilities could not be determined, defaulting to Security 1')
248
- security_version = 1
249
- else:
250
- # When no_sec is present, use security 0, else security 1
251
- security_version = int(not esp_prov.has_capability(obj_transport, 'no_sec'))
252
- print(f'==== Auto-detected Security Scheme: {security_version} ====')
253
-
254
- # Fetch and print device capabilities before checking pop requirements
255
- # This helps users understand why pop might be required
256
- # Store the response to reuse later for challenge-response check
243
+ # Fetch device capabilities up-front so auto-detect can read prov.sec_ver
244
+ # directly (no_sec / no_pop caps alone don't distinguish sec1 from sec2).
245
+ # The response is reused later for challenge-response checks.
257
246
  version_response = None
247
+ prov_info = {}
258
248
  try:
259
249
  print("Checking device capabilities...")
260
250
  version_response = esp_prov.get_version(obj_transport)
261
251
  if version_response:
262
252
  print(f"Device capabilities response: {version_response}")
253
+ try:
254
+ prov_info = json.loads(version_response).get('prov', {}) or {}
255
+ except (ValueError, AttributeError):
256
+ prov_info = {}
263
257
  else:
264
258
  print("Device capabilities response: (empty or not available)")
265
259
  except Exception as e:
266
260
  # If we can't get capabilities, continue anyway
267
261
  print(f"Could not retrieve device capabilities: {e}")
268
262
 
263
+ # Auto-detect security version if not specified
264
+ if security_version is None:
265
+ advertised = prov_info.get('sec_ver')
266
+ if isinstance(advertised, int):
267
+ security_version = advertised
268
+ elif 'no_sec' in prov_info.get('cap', []):
269
+ security_version = 0
270
+ elif prov_info:
271
+ security_version = 1
272
+ else:
273
+ print('Security capabilities could not be determined, defaulting to Security 1')
274
+ security_version = 1
275
+ print(f'==== Auto-detected Security Scheme: {security_version} ====')
276
+
269
277
  # Handle Security 1 PoP requirements
270
278
  if security_version == 1:
271
279
  if not esp_prov.has_capability(obj_transport, 'no_pop'):
@@ -280,9 +288,20 @@ def provision_device(transport_mode, pop, userid, secretkey,
280
288
  # Handle Security 2 credentials
281
289
  sec_patch_ver = 0
282
290
  if security_version == 2:
283
- sec_patch_ver = esp_prov.get_sec_patch_ver(obj_transport)
291
+ # Prefer the sec_patch_ver already present in the cached prov_info, else probe.
292
+ advertised_patch = prov_info.get('sec_patch_ver') if prov_info else None
293
+ sec_patch_ver = int(advertised_patch) if isinstance(advertised_patch, int) \
294
+ else esp_prov.get_sec_patch_ver(obj_transport)
295
+
296
+ # Default username to 'wifiprov' (firmware default for the RainMaker
297
+ # provisioning endpoint) and treat --pop / QR pop as the SRP6a password
298
+ # when no explicit --sec2_password is given. Only prompt when neither
299
+ # pop nor sec2_password is available.
284
300
  if len(sec2_username) == 0:
285
- sec2_username = input('Security Scheme 2 - SRP6a Username required: ')
301
+ sec2_username = 'wifiprov'
302
+ print(f"Using default Security 2 username '{sec2_username}'")
303
+ if len(sec2_password) == 0 and len(pop) > 0:
304
+ sec2_password = pop
286
305
  if len(sec2_password) == 0:
287
306
  sec2_password = getpass('Security Scheme 2 - SRP6a Password required: ')
288
307