component: Remove wifi_provisioning component and esp_prov tool

This commit is contained in:
WanqQixiang
2025-09-11 18:47:03 +08:00
parent 56949379e0
commit 6b503d0db5
109 changed files with 381 additions and 12118 deletions

View File

@@ -16,15 +16,15 @@ from getpass import getpass
import proto_lc
try:
import esp_prov
import security
import transport
except ImportError:
idf_path = os.environ['IDF_PATH']
sys.path.insert(0, idf_path + '/components/protocomm/python')
sys.path.insert(1, idf_path + '/tools/esp_prov')
sys.path.insert(1, idf_path + '/examples/protocols/esp_local_ctrl/scripts')
import esp_prov
import security
import transport
# Set this to true to allow exceptions to be thrown
config_throw_except = False
@@ -32,13 +32,13 @@ config_throw_except = False
# Property types enum
PROP_TYPE_TIMESTAMP = 0
PROP_TYPE_INT32 = 1
PROP_TYPE_BOOLEAN = 2
PROP_TYPE_STRING = 3
PROP_TYPE_INT32 = 1
PROP_TYPE_BOOLEAN = 2
PROP_TYPE_STRING = 3
# Property flags enum
PROP_FLAG_READONLY = (1 << 0)
PROP_FLAG_READONLY = 1 << 0
def prop_typestr(prop):
@@ -125,20 +125,22 @@ def get_security(secver, sec_patch_ver, username, password, pop='', verbose=Fals
async def get_transport(sel_transport, service_name, check_hostname):
try:
tp = None
if (sel_transport == 'http'):
tp = esp_prov.transport.Transport_HTTP(service_name, None)
elif (sel_transport == 'https'):
if sel_transport == 'http':
tp = transport.Transport_HTTP(service_name, None)
elif sel_transport == 'https':
example_path = os.environ['IDF_PATH'] + '/examples/protocols/esp_local_ctrl'
cert_path = example_path + '/main/certs/rootCA.pem'
ssl_ctx = ssl.create_default_context(cafile=cert_path)
ssl_ctx.check_hostname = check_hostname
tp = esp_prov.transport.Transport_HTTP(service_name, ssl_ctx)
elif (sel_transport == 'ble'):
tp = esp_prov.transport.Transport_BLE(
tp = transport.Transport_HTTP(service_name, ssl_ctx)
elif sel_transport == 'ble':
tp = transport.Transport_BLE(
service_uuid='3d981e4a-31eb-42b4-8a68-75bd8d3bd521',
nu_lookup={'esp_local_ctrl/version': '0001',
'esp_local_ctrl/session': '0002',
'esp_local_ctrl/control': '0003'}
nu_lookup={
'esp_local_ctrl/version': '0001',
'esp_local_ctrl/session': '0002',
'esp_local_ctrl/control': '0003',
},
)
await tp.connect(devname=service_name)
return tp
@@ -249,7 +251,7 @@ async def establish_session(tp, sec):
if request is None:
break
response = await tp.send_data('esp_local_ctrl/session', request)
if (response is None):
if response is None:
return False
return True
except RuntimeError as e:
@@ -305,47 +307,67 @@ async def main():
parser = argparse.ArgumentParser(description='Control an ESP32 running esp_local_ctrl service')
parser.add_argument('--version', dest='version', type=str,
help='Protocol version', default='')
parser.add_argument('--version', dest='version', type=str, help='Protocol version', default='')
parser.add_argument('--transport', dest='transport', type=str,
help='transport i.e http/https/ble', default='https')
parser.add_argument('--transport', dest='transport', type=str, help='transport i.e http/https/ble', default='https')
parser.add_argument('--name', dest='service_name', type=str,
help='BLE Device Name / HTTP Server hostname or IP', default='')
parser.add_argument(
'--name', dest='service_name', type=str, help='BLE Device Name / HTTP Server hostname or IP', default=''
)
parser.add_argument('--sec_ver', dest='secver', type=int, default=None,
help=desc_format(
'Protocomm security scheme used for secure '
'session establishment. Accepted values are :',
'\t- 0 : No security',
'\t- 1 : X25519 key exchange + AES-CTR encryption',
'\t- 2 : SRP6a + AES-GCM encryption',
'\t + Authentication using Proof of Possession (PoP)'))
parser.add_argument(
'--sec_ver',
dest='secver',
type=int,
default=None,
help=desc_format(
'Protocomm security scheme used for secure session establishment. Accepted values are :',
'\t- 0 : No security',
'\t- 1 : X25519 key exchange + AES-CTR encryption',
'\t- 2 : SRP6a + AES-GCM encryption',
'\t + Authentication using Proof of Possession (PoP)',
),
)
parser.add_argument('--pop', dest='pop', type=str, default='',
help=desc_format(
'This specifies the Proof of possession (PoP) when security scheme 1 '
'is used'))
parser.add_argument(
'--pop',
dest='pop',
type=str,
default='',
help=desc_format('This specifies the Proof of possession (PoP) when security scheme 1 is used'),
)
parser.add_argument('--sec2_username', dest='sec2_usr', type=str, default='',
help=desc_format(
'Username for security scheme 2 (SRP6a)'))
parser.add_argument(
'--sec2_username',
dest='sec2_usr',
type=str,
default='',
help=desc_format('Username for security scheme 2 (SRP6a)'),
)
parser.add_argument('--sec2_pwd', dest='sec2_pwd', type=str, default='',
help=desc_format(
'Password for security scheme 2 (SRP6a)'))
parser.add_argument(
'--sec2_pwd', dest='sec2_pwd', type=str, default='', help=desc_format('Password for security scheme 2 (SRP6a)')
)
parser.add_argument('--sec2_gen_cred', help='Generate salt and verifier for security scheme 2 (SRP6a)', action='store_true')
parser.add_argument(
'--sec2_gen_cred', help='Generate salt and verifier for security scheme 2 (SRP6a)', action='store_true'
)
parser.add_argument('--sec2_salt_len', dest='sec2_salt_len', type=int, default=16,
help=desc_format(
'Salt length for security scheme 2 (SRP6a)'))
parser.add_argument(
'--sec2_salt_len',
dest='sec2_salt_len',
type=int,
default=16,
help=desc_format('Salt length for security scheme 2 (SRP6a)'),
)
parser.add_argument('--dont-check-hostname', action='store_true',
# If enabled, the certificate won't be rejected for hostname mismatch.
# This option is hidden because it should be used only for testing purposes.
help=argparse.SUPPRESS)
parser.add_argument(
'--dont-check-hostname',
action='store_true',
# If enabled, the certificate won't be rejected for hostname mismatch.
# This option is hidden because it should be used only for testing purposes.
help=argparse.SUPPRESS,
)
parser.add_argument('-v', '--verbose', dest='verbose', help='increase output verbosity', action='store_true')
@@ -383,7 +405,7 @@ async def main():
args.secver = int(not await has_capability(obj_transport, 'no_sec'))
print(f'==== Security Scheme: {args.secver} ====')
if (args.secver == 1):
if args.secver == 1:
if not await has_capability(obj_transport, 'no_pop'):
if len(args.pop) == 0:
print('---- Proof of Possession argument not provided ----')
@@ -392,7 +414,7 @@ async def main():
print('---- Proof of Possession will be ignored ----')
args.pop = ''
if (args.secver == 2):
if args.secver == 2:
sec_patch_ver = await get_sec_patch_ver(obj_transport, args.verbose)
if len(args.sec2_usr) == 0:
args.sec2_usr = input('Security Scheme 2 - SRP6a Username required: ')
@@ -422,18 +444,22 @@ async def main():
raise RuntimeError('Error in reading property value')
print('\n==== Available Properties ====')
print('{0: >4} {1: <16} {2: <10} {3: <16} {4: <16}'.format(
'S.N.', 'Name', 'Type', 'Flags', 'Value'))
print('{: >4} {: <16} {: <10} {: <16} {: <16}'.format('S.N.', 'Name', 'Type', 'Flags', 'Value'))
for i in range(len(properties)):
print('[{0: >2}] {1: <16} {2: <10} {3: <16} {4: <16}'.format(
i + 1, properties[i]['name'], prop_typestr(properties[i]),
['','Read-Only'][prop_is_readonly(properties[i])],
str(properties[i]['value'])))
print(
'[{: >2}] {: <16} {: <10} {: <16} {: <16}'.format(
i + 1,
properties[i]['name'],
prop_typestr(properties[i]),
['', 'Read-Only'][prop_is_readonly(properties[i])],
str(properties[i]['value']),
)
)
select = 0
while True:
try:
inval = input('\nSelect properties to set (0 to re-read, \'q\' to quit) : ')
inval = input("\nSelect properties to set (0 to re-read, 'q' to quit) : ")
if inval.lower() == 'q':
print('Quitting...')
exit(0)
@@ -453,8 +479,7 @@ async def main():
for select in selections:
while True:
inval = input('Enter value to set for property (' + properties[select - 1]['name'] + ') : ')
value = encode_prop_value(properties[select - 1],
str_to_prop_value(properties[select - 1], inval))
value = encode_prop_value(properties[select - 1], str_to_prop_value(properties[select - 1], inval))
if value is None:
print('Invalid input! Retry...')
continue
@@ -465,5 +490,6 @@ async def main():
if not await set_property_values(obj_transport, obj_security, properties, set_indices, set_values):
print('Failed to set values!')
if __name__ == '__main__':
asyncio.run(main())

View File

@@ -0,0 +1,31 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import importlib.util
import os
import sys
from importlib.abc import Loader
from typing import Any
def _load_source(name: str, path: str) -> Any:
spec = importlib.util.spec_from_file_location(name, path)
if not spec:
return None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
assert isinstance(spec.loader, Loader)
spec.loader.exec_module(module)
return module
idf_path = os.environ['IDF_PATH']
# protocomm component related python files generated from .proto files
constants_pb2 = _load_source('constants_pb2', idf_path + '/components/protocomm/python/constants_pb2.py')
sec0_pb2 = _load_source('sec0_pb2', idf_path + '/components/protocomm/python/sec0_pb2.py')
sec1_pb2 = _load_source('sec1_pb2', idf_path + '/components/protocomm/python/sec1_pb2.py')
sec2_pb2 = _load_source('sec2_pb2', idf_path + '/components/protocomm/python/sec2_pb2.py')
session_pb2 = _load_source('session_pb2', idf_path + '/components/protocomm/python/session_pb2.py')

View File

@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from .security0 import * # noqa: F403, F401
from .security1 import * # noqa: F403, F401
from .security2 import * # noqa: F403, F401

View File

@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# Base class for protocomm security
class Security:
def __init__(self, security_session):
self.security_session = security_session

View File

@@ -0,0 +1,53 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# APIs for interpreting and creating protobuf packets for
# protocomm endpoint with security type protocomm_security0
import proto
from utils import str_to_bytes
from .security import Security
class Security0(Security):
def __init__(self, verbose):
# Initialize state of the security1 FSM
self.session_state = 0
self.verbose = verbose
Security.__init__(self, self.security0_session)
def security0_session(self, response_data):
# protocomm security0 FSM which interprets/forms
# protobuf packets according to present state of session
if self.session_state == 0:
self.session_state = 1
return self.setup0_request()
if self.session_state == 1:
self.setup0_response(response_data)
return None
def setup0_request(self):
# Form protocomm security0 request packet
setup_req = proto.session_pb2.SessionData()
setup_req.sec_ver = 0
session_cmd = proto.sec0_pb2.S0SessionCmd()
setup_req.sec0.sc.MergeFrom(session_cmd)
return setup_req.SerializeToString().decode('latin-1')
def setup0_response(self, response_data):
# Interpret protocomm security0 response packet
setup_resp = proto.session_pb2.SessionData()
setup_resp.ParseFromString(str_to_bytes(response_data))
# Check if security scheme matches
if setup_resp.sec_ver != proto.session_pb2.SecScheme0:
raise RuntimeError('Incorrect security scheme')
def encrypt_data(self, data):
# Passive. No encryption when security0 used
return data
def decrypt_data(self, data):
# Passive. No encryption when security0 used
return data

View File

@@ -0,0 +1,145 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# APIs for interpreting and creating protobuf packets for
# protocomm endpoint with security type protocomm_security1
import proto
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from utils import long_to_bytes
from utils import str_to_bytes
from .security import Security
def a_xor_b(a: bytes, b: bytes) -> bytes:
return b''.join(long_to_bytes(a[i] ^ b[i]) for i in range(0, len(b)))
# Enum for state of protocomm_security1 FSM
class security_state:
REQUEST1 = 0
RESPONSE1_REQUEST2 = 1
RESPONSE2 = 2
FINISHED = 3
class Security1(Security):
def __init__(self, pop, verbose):
# Initialize state of the security1 FSM
self.session_state = security_state.REQUEST1
self.pop = str_to_bytes(pop)
self.verbose = verbose
Security.__init__(self, self.security1_session)
def security1_session(self, response_data):
# protocomm security1 FSM which interprets/forms
# protobuf packets according to present state of session
if self.session_state == security_state.REQUEST1:
self.session_state = security_state.RESPONSE1_REQUEST2
return self.setup0_request()
elif self.session_state == security_state.RESPONSE1_REQUEST2:
self.session_state = security_state.RESPONSE2
self.setup0_response(response_data)
return self.setup1_request()
elif self.session_state == security_state.RESPONSE2:
self.session_state = security_state.FINISHED
self.setup1_response(response_data)
return None
print('Unexpected state')
return None
def __generate_key(self):
# Generate private and public key pair for client
self.client_private_key = X25519PrivateKey.generate()
self.client_public_key = self.client_private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw
)
def _print_verbose(self, data):
if self.verbose:
print(f'\x1b[32;20m++++ {data} ++++\x1b[0m')
def setup0_request(self):
# Form SessionCmd0 request packet using client public key
setup_req = proto.session_pb2.SessionData()
setup_req.sec_ver = proto.session_pb2.SecScheme1
self.__generate_key()
setup_req.sec1.sc0.client_pubkey = self.client_public_key
self._print_verbose(f'Client Public Key:\t0x{self.client_public_key.hex()}')
return setup_req.SerializeToString().decode('latin-1')
def setup0_response(self, response_data):
# Interpret SessionResp0 response packet
setup_resp = proto.session_pb2.SessionData()
setup_resp.ParseFromString(str_to_bytes(response_data))
self._print_verbose('Security version:\t' + str(setup_resp.sec_ver))
if setup_resp.sec_ver != proto.session_pb2.SecScheme1:
raise RuntimeError('Incorrect security scheme')
self.device_public_key = setup_resp.sec1.sr0.device_pubkey
# Device random is the initialization vector
device_random = setup_resp.sec1.sr0.device_random
self._print_verbose(f'Device Public Key:\t0x{self.device_public_key.hex()}')
self._print_verbose(f'Device Random:\t0x{device_random.hex()}')
# Calculate Curve25519 shared key using Client private key and Device public key
sharedK = self.client_private_key.exchange(X25519PublicKey.from_public_bytes(self.device_public_key))
self._print_verbose(f'Shared Key:\t0x{sharedK.hex()}')
# If PoP is provided, XOR SHA256 of PoP with the previously
# calculated Shared Key to form the actual Shared Key
if len(self.pop) > 0:
# Calculate SHA256 of PoP
h = hashes.Hash(hashes.SHA256(), backend=default_backend())
h.update(self.pop)
digest = h.finalize()
# XOR with and update Shared Key
sharedK = a_xor_b(sharedK, digest)
self._print_verbose(f'Updated Shared Key (Shared key XORed with PoP):\t0x{sharedK.hex()}')
# Initialize the encryption engine with Shared Key and initialization vector
cipher = Cipher(algorithms.AES(sharedK), modes.CTR(device_random), backend=default_backend())
self.cipher = cipher.encryptor()
def setup1_request(self):
# Form SessionCmd1 request packet using encrypted device public key
setup_req = proto.session_pb2.SessionData()
setup_req.sec_ver = proto.session_pb2.SecScheme1
setup_req.sec1.msg = proto.sec1_pb2.Session_Command1
# Encrypt device public key and attach to the request packet
client_verify = self.cipher.update(self.device_public_key)
self._print_verbose(f'Client Proof:\t0x{client_verify.hex()}')
setup_req.sec1.sc1.client_verify_data = client_verify
return setup_req.SerializeToString().decode('latin-1')
def setup1_response(self, response_data):
# Interpret SessionResp1 response packet
setup_resp = proto.session_pb2.SessionData()
setup_resp.ParseFromString(str_to_bytes(response_data))
# Ensure security scheme matches
if setup_resp.sec_ver == proto.session_pb2.SecScheme1:
# Read encrypyed device verify string
device_verify = setup_resp.sec1.sr1.device_verify_data
self._print_verbose(f'Device Proof:\t0x{device_verify.hex()}')
# Decrypt the device verify string
enc_client_pubkey = self.cipher.update(setup_resp.sec1.sr1.device_verify_data)
# Match decrypted string with client public key
if enc_client_pubkey != self.client_public_key:
raise RuntimeError('Failed to verify device!')
else:
raise RuntimeError('Unsupported security protocol')
def encrypt_data(self, data):
return self.cipher.update(data)
def decrypt_data(self, data):
return self.cipher.update(data)

View File

@@ -0,0 +1,181 @@
# SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
# APIs for interpreting and creating protobuf packets for
# protocomm endpoint with security type protocomm_security2
import struct
from typing import Any
import proto
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from utils import long_to_bytes
from utils import str_to_bytes
from .security import Security
from .srp6a import Srp6a
from .srp6a import generate_salt_and_verifier
AES_KEY_LEN = 256 // 8
# Enum for state of protocomm_security1 FSM
class security_state:
REQUEST1 = 0
RESPONSE1_REQUEST2 = 1
RESPONSE2 = 2
FINISHED = 3
def sec2_gen_salt_verifier(username: str, password: str, salt_len: int) -> Any:
salt, verifier = generate_salt_and_verifier(username, password, len_s=salt_len)
salt_str = ', '.join([format(b, '#04x') for b in salt])
salt_c_arr = '\n '.join(salt_str[i : i + 96] for i in range(0, len(salt_str), 96))
print(f'static const char sec2_salt[] = {{\n {salt_c_arr}\n}};\n') # noqa E702
verifier_str = ', '.join([format(b, '#04x') for b in verifier])
verifier_c_arr = '\n '.join(verifier_str[i : i + 96] for i in range(0, len(verifier_str), 96))
print(f'static const char sec2_verifier[] = {{\n {verifier_c_arr}\n}};\n') # noqa E702
class Security2(Security):
def __init__(self, sec_patch_ver: int, username: str, password: str, verbose: bool) -> None:
# Initialize state of the security2 FSM
self.session_state = security_state.REQUEST1
self.sec_patch_ver = sec_patch_ver
self.username = username
self.password = password
self.verbose = verbose
self.srp6a_ctx: type[Srp6a]
self.cipher: type[AESGCM]
self.client_pop_key = None
self.nonce = bytearray()
Security.__init__(self, self.security2_session)
def security2_session(self, response_data: bytes) -> Any:
# protocomm security2 FSM which interprets/forms
# protobuf packets according to present state of session
if self.session_state == security_state.REQUEST1:
self.session_state = security_state.RESPONSE1_REQUEST2
return self.setup0_request()
if self.session_state == security_state.RESPONSE1_REQUEST2:
self.session_state = security_state.RESPONSE2
self.setup0_response(response_data)
return self.setup1_request()
if self.session_state == security_state.RESPONSE2:
self.session_state = security_state.FINISHED
self.setup1_response(response_data)
return None
print('---- Unexpected state! ----')
return None
def _print_verbose(self, data: str) -> None:
if self.verbose:
print(f'\x1b[32;20m++++ {data} ++++\x1b[0m') # noqa E702
def setup0_request(self) -> Any:
# Form SessionCmd0 request packet using client public key
setup_req = proto.session_pb2.SessionData()
setup_req.sec_ver = proto.session_pb2.SecScheme2
setup_req.sec2.msg = proto.sec2_pb2.S2Session_Command0
setup_req.sec2.sc0.client_username = str_to_bytes(self.username)
self.srp6a_ctx = Srp6a(self.username, self.password)
if self.srp6a_ctx is None:
raise RuntimeError('Failed to initialize SRP6a instance!')
client_pubkey = long_to_bytes(self.srp6a_ctx.A)
setup_req.sec2.sc0.client_pubkey = client_pubkey
self._print_verbose(f'Client Public Key:\t0x{client_pubkey.hex()}')
return setup_req.SerializeToString().decode('latin-1')
def setup0_response(self, response_data: bytes) -> None:
# Interpret SessionResp0 response packet
setup_resp = proto.session_pb2.SessionData()
setup_resp.ParseFromString(str_to_bytes(response_data))
self._print_verbose(f'Security version:\t{str(setup_resp.sec_ver)}')
if setup_resp.sec_ver != proto.session_pb2.SecScheme2:
raise RuntimeError('Incorrect security scheme')
# Device public key, random salt and password verifier
device_pubkey = setup_resp.sec2.sr0.device_pubkey
device_salt = setup_resp.sec2.sr0.device_salt
self._print_verbose(f'Device Public Key:\t0x{device_pubkey.hex()}')
self.client_pop_key = self.srp6a_ctx.process_challenge(device_salt, device_pubkey)
def setup1_request(self) -> Any:
# Form SessionCmd1 request packet using encrypted device public key
setup_req = proto.session_pb2.SessionData()
setup_req.sec_ver = proto.session_pb2.SecScheme2
setup_req.sec2.msg = proto.sec2_pb2.S2Session_Command1
# Encrypt device public key and attach to the request packet
if self.client_pop_key is None:
raise RuntimeError('Failed to generate client proof!')
self._print_verbose(f'Client Proof:\t0x{self.client_pop_key.hex()}')
setup_req.sec2.sc1.client_proof = self.client_pop_key
return setup_req.SerializeToString().decode('latin-1')
def setup1_response(self, response_data: bytes) -> Any:
# Interpret SessionResp1 response packet
setup_resp = proto.session_pb2.SessionData()
setup_resp.ParseFromString(str_to_bytes(response_data))
# Ensure security scheme matches
if setup_resp.sec_ver == proto.session_pb2.SecScheme2:
# Read encrypyed device proof string
device_proof = setup_resp.sec2.sr1.device_proof
self._print_verbose(f'Device Proof:\t0x{device_proof.hex()}')
self.srp6a_ctx.verify_session(device_proof)
if not self.srp6a_ctx.authenticated():
raise RuntimeError('Failed to verify device proof')
else:
raise RuntimeError('Unsupported security protocol')
# Getting the shared secret
shared_secret = self.srp6a_ctx.get_session_key()
self._print_verbose(f'Shared Secret:\t0x{shared_secret.hex()}')
# Using the first 256 bits of a 512 bit key
session_key = shared_secret[:AES_KEY_LEN]
self._print_verbose(f'Session Key:\t0x{session_key.hex()}')
# 96-bit nonce
self.nonce = bytearray(setup_resp.sec2.sr1.device_nonce)
if self.nonce is None:
raise RuntimeError('Received invalid nonce from device!')
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
# Initialize the encryption engine with Shared Key and initialization vector
self.cipher = AESGCM(session_key)
if self.cipher is None:
raise RuntimeError('Failed to initialize AES-GCM cryptographic engine!')
def _increment_nonce(self) -> None:
"""Increment the last 4 bytes of nonce (big-endian counter)."""
if self.sec_patch_ver == 1:
counter = struct.unpack('>I', self.nonce[8:])[0] # Read last 4 bytes as big-endian integer
counter += 1 # Increment counter
if counter > 0xFFFFFFFF: # Check for overflow
raise RuntimeError('Nonce counter overflow')
self.nonce[8:] = struct.pack('>I', counter) # Store back as big-endian
def encrypt_data(self, data: bytes) -> Any:
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
ciphertext = self.cipher.encrypt(self.nonce, data, None)
self._increment_nonce()
return ciphertext
def decrypt_data(self, data: bytes) -> Any:
self._print_verbose(f'Nonce:\t0x{self.nonce.hex()}')
plaintext = self.cipher.decrypt(self.nonce, data, None)
self._increment_nonce()
return plaintext

View File

@@ -0,0 +1,317 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# N A large safe prime (N = 2q+1, where q is prime) [All arithmetic is done modulo N]
# g A generator modulo N
# k Multiplier parameter (k = H(N, g) in SRP-6a, k = 3 for legacy SRP-6)
# s User's salt
# Iu Username
# p Cleartext Password
# H() One-way hash function
# ^ (Modular) Exponentiation
# u Random scrambling parameter
# a, b Secret ephemeral values
# A, B Public ephemeral values
# x Private key (derived from p and s)
# v Password verifier
import hashlib
import os
from collections.abc import Callable
from typing import Any
from utils import bytes_to_long
from utils import long_to_bytes
SHA1 = 0
SHA224 = 1
SHA256 = 2
SHA384 = 3
SHA512 = 4
NG_1024 = 0
NG_2048 = 1
NG_3072 = 2
NG_4096 = 3
NG_8192 = 4
_hash_map = {
SHA1: hashlib.sha1,
SHA224: hashlib.sha224,
SHA256: hashlib.sha256,
SHA384: hashlib.sha384,
SHA512: hashlib.sha512,
}
_ng_const = (
# 1024-bit
(
"""\
EEAF0AB9ADB38DD69C33F80AFA8FC5E86072618775FF3C0B9EA2314C9C256576D674DF7496\
EA81D3383B4813D692C6E0E0D5D8E250B98BE48E495C1D6089DAD15DC7D7B46154D6B6CE8E\
F4AD69B15D4982559B297BCF1885C529F566660E57EC68EDBC3C05726CC02FD4CBF4976EAA\
9AFD5138FE8376435B9FC61D2FC0EB06E3""",
'2',
),
# 2048
(
"""\
AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4\
A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF60\
95179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF\
747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B907\
8717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB37861\
60279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DB\
FBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73""",
'2',
),
# 3072
(
"""\
FFFFFFFFFFFFFFFFC90FDAA22168C2\
34C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E\
3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B5\
76625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE\
9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D3\
9A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED5290770\
96966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E77\
2C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558171839\
95497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A\
33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6\
E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA\
06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C77\
0988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2\
CAFFFFFFFFFFFFFFFF""",
'5',
),
# 4096
(
"""\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199\
FFFFFFFFFFFFFFFF""",
'5',
),
# 8192
(
"""\
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08\
8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B\
302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9\
A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6\
49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8\
FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D\
670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C\
180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718\
3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D\
04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D\
B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226\
1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C\
BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC\
E0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B26\
99C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB\
04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2\
233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127\
D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934028492\
36C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406\
AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918\
DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B33205151\
2BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03\
F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97F\
BEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AA\
CC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58B\
B7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632\
387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E\
6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA\
3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C\
5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD9\
22222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC886\
2F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A6\
6D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC5\
0846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268\
359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6\
FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E71\
60C980DD98EDD3DFFFFFFFFFFFFFFFFF""",
'0x13',
),
)
def get_ng(ng_type: int) -> tuple[int, int]:
n_hex, g_hex = _ng_const[ng_type]
return int(n_hex, 16), int(g_hex, 16)
def get_random(nbytes: int) -> Any:
return bytes_to_long(os.urandom(nbytes))
def get_random_of_length(nbytes: int) -> Any:
offset = (nbytes * 8) - 1
return get_random(nbytes) | (1 << offset)
def H(hash_class: Callable, *args: Any, **kwargs: Any) -> int:
width = kwargs.get('width', None)
h = hash_class()
for s in args:
if s is not None:
data = long_to_bytes(s) if isinstance(s, int) else s
if width is not None:
h.update(bytes(width - len(data)))
h.update(data)
return int(h.hexdigest(), 16)
def H_N_xor_g(hash_class: Callable, N: int, g: int) -> bytes:
bin_N = long_to_bytes(N)
bin_g = long_to_bytes(g)
padding = len(bin_N) - len(bin_g)
hN = hash_class(bin_N).digest()
hg = hash_class(b''.join([b'\0' * padding, bin_g])).digest()
return b''.join(long_to_bytes(hN[i] ^ hg[i]) for i in range(0, len(hN)))
def calculate_x(hash_class: Callable, s: Any, Iu: str, p: str) -> int:
_Iu = Iu.encode()
_p = p.encode()
return H(hash_class, s, H(hash_class, _Iu + b':' + _p))
def generate_salt_and_verifier(
Iu: str, p: str, len_s: int, hash_alg: int = SHA512, ng_type: int = NG_3072
) -> tuple[bytes, bytes]:
hash_class = _hash_map[hash_alg]
N, g = get_ng(ng_type)
_s = long_to_bytes(get_random(len_s))
_v = long_to_bytes(pow(g, calculate_x(hash_class, _s, Iu, p), N))
return _s, _v
def calculate_M(hash_class: Callable, N: int, g: int, Iu: str, s: int, A: int, B: int, K: bytes) -> Any:
_Iu = Iu.encode()
h = hash_class()
h.update(H_N_xor_g(hash_class, N, g))
h.update(hash_class(_Iu).digest())
h.update(long_to_bytes(s))
h.update(long_to_bytes(A))
h.update(long_to_bytes(B))
h.update(K)
return h.digest()
def calculate_H_AMK(hash_class: Callable, A: int, M: bytes, K: bytes) -> Any:
h = hash_class()
h.update(long_to_bytes(A))
h.update(M)
h.update(K)
return h.digest()
class Srp6a:
def __init__(self, username: str, password: str, hash_alg: int = SHA512, ng_type: int = NG_3072):
hash_class = _hash_map[hash_alg]
N, g = get_ng(ng_type)
k = H(hash_class, N, g, width=len(long_to_bytes(N)))
self.Iu = username
self.p = password
self.a = get_random_of_length(32)
self.A = pow(g, self.a, N)
self.v: int | None = None
self.K: bytes | None = None
self.H_AMK = None
self._authenticated = False
self.hash_class = hash_class
self.N = N
self.g = g
self.k = k
def authenticated(self) -> bool:
return self._authenticated
def get_username(self) -> str:
return self.Iu
def get_ephemeral_secret(self) -> Any:
return long_to_bytes(self.a)
def get_session_key(self) -> Any:
return self.K if self._authenticated else None
def start_authentication(self) -> tuple[str, bytes]:
return (self.Iu, long_to_bytes(self.A))
# Returns M or None if SRP-6a safety check is violated
def process_challenge(self, bytes_s: bytes, bytes_B: bytes) -> Any:
s = bytes_to_long(bytes_s)
B = bytes_to_long(bytes_B)
N = self.N
g = self.g
k = self.k
hash_class = self.hash_class
# SRP-6a safety check
if (B % N) == 0:
return None
u = H(hash_class, self.A, B, width=len(long_to_bytes(N)))
if u == 0: # SRP-6a safety check
return None
x = calculate_x(hash_class, s, self.Iu, self.p)
v = pow(g, x, N)
S = pow((B - k * v), (self.a + u * x), N)
self.K = hash_class(long_to_bytes(S)).digest()
M = calculate_M(hash_class, N, g, self.Iu, s, self.A, B, self.K)
if not M:
return None
self.H_AMK = calculate_H_AMK(hash_class, self.A, M, self.K)
return M
def verify_session(self, host_HAMK: bytes) -> None:
if self.H_AMK == host_HAMK:
self._authenticated = True
class AuthenticationFailed(Exception):
pass

View File

@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from .transport_ble import * # noqa: F403, F401
from .transport_console import * # noqa: F403, F401
from .transport_http import * # noqa: F403, F401

View File

@@ -0,0 +1,221 @@
# SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import platform
from utils import hex_str_to_bytes
from utils import str_to_bytes
fallback = True
# Check if required packages are installed
# else fallback to console mode
try:
import bleak
fallback = False
except ImportError:
pass
# --------------------------------------------------------------------
def device_sort(device):
return device[0].address
class BLE_Bleak_Client:
def __init__(self):
self.adapter = None
self.adapter_props = None
self.characteristics = dict()
self.chrc_names = None
self.device = None
self.devname = None
self.iface = None
self.nu_lookup = None
self.services = None
self.srv_uuid_adv = None
self.srv_uuid_fallback = None
async def connect(self, devname, iface, chrc_names, fallback_srv_uuid):
self.devname = devname
self.srv_uuid_fallback = fallback_srv_uuid
self.chrc_names = [name.lower() for name in chrc_names]
self.iface = iface
print('Discovering...')
try:
discovery = await bleak.BleakScanner.discover(return_adv=True)
devices = list(discovery.values())
except bleak.exc.BleakDBusError as e:
if str(e) == '[org.bluez.Error.NotReady] Resource Not Ready':
raise RuntimeError('Bluetooth is not ready. Maybe try `bluetoothctl power on`?')
raise
found_device = None
if self.devname is None:
if len(devices) == 0:
print('No devices found!')
exit(1)
while True:
devices.sort(key=device_sort)
print('==== BLE Discovery results ====')
print('{: >4} {: <33} {: <12}'.format('S.N.', 'Name', 'Address'))
for i, _ in enumerate(devices):
print(
'[{: >2}] {: <33} {: <12}'.format(i + 1, devices[i][0].name or 'Unknown', devices[i][0].address)
)
while True:
try:
select = int(input('Select device by number (0 to rescan) : '))
if select < 0 or select > len(devices):
raise ValueError
break
except ValueError:
print('Invalid input! Retry')
if select != 0:
break
discovery = await bleak.BleakScanner.discover(return_adv=True)
devices = list(discovery.values())
self.devname = devices[select - 1][0].name
found_device = devices[select - 1]
else:
for d in devices:
if d[0].name == self.devname:
found_device = d
if not found_device:
raise RuntimeError('Device not found')
uuids = found_device[1].service_uuids
# There should be 1 service UUID in advertising data
# If bluez had cached an old version of the advertisement data
# the list of uuids may be incorrect, in which case connection
# or service discovery may fail the first time. If that happens
# the cache will be refreshed before next retry
if len(uuids) == 1:
self.srv_uuid_adv = uuids[0]
print('Connecting...')
self.device = bleak.BleakClient(found_device[0].address)
await self.device.connect()
# must be paired on Windows to access characteristics;
# cannot be paired on Mac
if platform.system() == 'Windows':
await self.device.pair()
print('Getting Services...')
services = self.device.services
service = services[self.srv_uuid_adv] or services[self.srv_uuid_fallback]
if not service:
await self.device.disconnect()
self.device = None
raise RuntimeError('Provisioning service not found')
nu_lookup = dict()
for characteristic in service.characteristics:
for descriptor in characteristic.descriptors:
if descriptor.uuid[4:8] != '2901':
continue
readval = await self.device.read_gatt_descriptor(descriptor.handle)
found_name = ''.join(chr(b) for b in readval).lower()
nu_lookup[found_name] = characteristic.uuid
self.characteristics[characteristic.uuid] = characteristic
match_found = True
for name in self.chrc_names:
if name not in nu_lookup:
# Endpoint name not present
match_found = False
break
# Create lookup table only if all endpoint names found
self.nu_lookup = [None, nu_lookup][match_found]
return True
def get_nu_lookup(self):
return self.nu_lookup
def has_characteristic(self, uuid):
print('checking for characteristic ' + uuid)
if uuid in self.characteristics:
return True
return False
async def disconnect(self):
if self.device:
print('Disconnecting...')
if platform.system() == 'Windows':
await self.device.unpair()
await self.device.disconnect()
self.device = None
self.nu_lookup = None
self.characteristics = dict()
async def send_data(self, characteristic_uuid, data):
await self.device.write_gatt_char(characteristic_uuid, bytearray(data.encode('latin-1')), True)
readval = await self.device.read_gatt_char(characteristic_uuid)
return ''.join(chr(b) for b in readval)
# --------------------------------------------------------------------
# Console based BLE client for Cross Platform support
class BLE_Console_Client:
async def connect(self, devname, iface, chrc_names, fallback_srv_uuid):
print('BLE client is running in console mode')
print('\tThis could be due to your platform not being supported or dependencies not being met')
print('\tPlease ensure all pre-requisites are met to run the full fledged client')
print('BLECLI >> Please connect to BLE device `' + devname + '` manually using your tool of choice')
resp = input('BLECLI >> Was the device connected successfully? [y/n] ')
if resp != 'Y' and resp != 'y':
return False
print('BLECLI >> List available attributes of the connected device')
resp = input(
"BLECLI >> Is the service UUID '" + fallback_srv_uuid + "' listed among available attributes? [y/n] "
)
if resp != 'Y' and resp != 'y':
return False
return True
def get_nu_lookup(self):
return None
def has_characteristic(self, uuid):
resp = input("BLECLI >> Is the characteristic UUID '" + uuid + "' listed among available attributes? [y/n] ")
if resp != 'Y' and resp != 'y':
return False
return True
async def disconnect(self):
pass
async def send_data(self, characteristic_uuid, data):
print("BLECLI >> Write following data to characteristic with UUID '" + characteristic_uuid + "' :")
print('\t>> ' + str_to_bytes(data).hex())
print('BLECLI >> Enter data read from characteristic (in hex) :')
resp = input('\t<< ')
return hex_str_to_bytes(resp)
# --------------------------------------------------------------------
# Function to get client instance depending upon platform
def get_client():
if fallback:
return BLE_Console_Client()
return BLE_Bleak_Client()

View File

@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# Base class for protocomm transport
import abc
class Transport:
@abc.abstractmethod
def send_session_data(self, data):
pass
@abc.abstractmethod
def send_config_data(self, data):
pass
async def disconnect(self):
pass

View File

@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from . import ble_cli
from .transport import Transport
class Transport_BLE(Transport):
def __init__(self, service_uuid, nu_lookup):
self.nu_lookup = nu_lookup
self.service_uuid = service_uuid
self.name_uuid_lookup = None
# Expect service UUID like '0000ffff-0000-1000-8000-00805f9b34fb'
for name in nu_lookup.keys():
# Calculate characteristic UUID for each endpoint
nu_lookup[name] = (
service_uuid[:4] + f'{int(nu_lookup[name], 16) & int(service_uuid[4:8], 16):02x}' + service_uuid[8:]
)
# Get BLE client module
self.cli = ble_cli.get_client()
async def connect(self, devname):
# Use client to connect to BLE device and bind to service
if not await self.cli.connect(
devname=devname, iface='hci0', chrc_names=self.nu_lookup.keys(), fallback_srv_uuid=self.service_uuid
):
raise RuntimeError('Failed to initialize transport')
# Irrespective of provided parameters, let the client
# generate a lookup table by reading advertisement data
# and characteristic user descriptors
self.name_uuid_lookup = self.cli.get_nu_lookup()
# If that doesn't work, use the lookup table provided as parameter
if self.name_uuid_lookup is None:
self.name_uuid_lookup = self.nu_lookup
# Check if expected characteristics are provided by the service
for name in self.name_uuid_lookup.keys():
if not self.cli.has_characteristic(self.name_uuid_lookup[name]):
raise RuntimeError(f"'{name}' endpoint not found")
async def disconnect(self):
await self.cli.disconnect()
async def send_data(self, ep_name, data):
# Write (and read) data to characteristic corresponding to the endpoint
if ep_name not in self.name_uuid_lookup.keys():
raise RuntimeError(f'Invalid endpoint: {ep_name}')
return await self.cli.send_data(self.name_uuid_lookup[ep_name], data)

View File

@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from utils import str_to_bytes
from .transport import Transport
class Transport_Console(Transport):
async def send_data(self, path, data, session_id=0):
print('Client->Device msg :', path, session_id, str_to_bytes(data).hex())
try:
resp = input('Enter device->client msg : ')
except Exception as err:
print('error:', err)
return None
return bytearray.fromhex(resp).decode('latin-1')

View File

@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: 2018-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import socket
from http.client import HTTPConnection
from http.client import HTTPSConnection
from utils import str_to_bytes
from .transport import Transport
class Transport_HTTP(Transport):
def __init__(self, hostname, ssl_context=None):
try:
socket.getaddrinfo(hostname.split(':')[0], None)
except socket.gaierror:
raise RuntimeError(f'Unable to resolve hostname: {hostname}')
if ssl_context is None:
self.conn = HTTPConnection(hostname, timeout=60)
else:
self.conn = HTTPSConnection(hostname, context=ssl_context, timeout=60)
try:
print(f'++++ Connecting to {hostname}++++')
self.conn.connect()
except Exception as err:
raise RuntimeError('Connection Failure : ' + str(err))
self.headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
def _send_post_request(self, path, data):
data = str_to_bytes(data) if isinstance(data, str) else data
try:
self.conn.request('POST', path, data, self.headers)
response = self.conn.getresponse()
# While establishing a session, the device sends the Set-Cookie header
# with value 'session=cookie_session_id' in its first response of the session to the tool.
# To maintain the same session, successive requests from the tool should include
# an additional 'Cookie' header with the above received value.
for hdr_key, hdr_val in response.getheaders():
if hdr_key == 'Set-Cookie':
self.headers['Cookie'] = hdr_val
if response.status == 200:
return response.read().decode('latin-1')
except Exception as err:
raise RuntimeError('Connection Failure : ' + str(err))
raise RuntimeError('Server responded with error code ' + str(response.status))
async def send_data(self, ep_name, data):
return self._send_post_request('/' + ep_name, data)

View File

@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from .convenience import * # noqa: F403, F401

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# Convenience functions for commonly used data type conversions
def bytes_to_long(s: bytes) -> int:
return int.from_bytes(s, 'big')
def long_to_bytes(n: int) -> bytes:
if n == 0:
return b'\x00'
return n.to_bytes((n.bit_length() + 7) // 8, 'big')
# 'deadbeef' -> b'deadbeef'
def str_to_bytes(s: str) -> bytes:
return bytes(s, encoding='latin-1')
# 'deadbeef' -> b'\xde\xad\xbe\xef'
def hex_str_to_bytes(s: str) -> bytes:
return bytes.fromhex(s)