chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2018-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import struct
|
||||
import sys
|
||||
import textwrap
|
||||
from getpass import getpass
|
||||
|
||||
import proto_lc
|
||||
|
||||
try:
|
||||
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 + '/examples/protocols/esp_local_ctrl/scripts')
|
||||
|
||||
import security
|
||||
import transport
|
||||
|
||||
# Set this to true to allow exceptions to be thrown
|
||||
config_throw_except = False
|
||||
|
||||
|
||||
# Property types enum
|
||||
PROP_TYPE_TIMESTAMP = 0
|
||||
PROP_TYPE_INT32 = 1
|
||||
PROP_TYPE_BOOLEAN = 2
|
||||
PROP_TYPE_STRING = 3
|
||||
|
||||
|
||||
# Property flags enum
|
||||
PROP_FLAG_READONLY = 1 << 0
|
||||
|
||||
|
||||
def prop_typestr(prop):
|
||||
if prop['type'] == PROP_TYPE_TIMESTAMP:
|
||||
return 'TIME(us)'
|
||||
elif prop['type'] == PROP_TYPE_INT32:
|
||||
return 'INT32'
|
||||
elif prop['type'] == PROP_TYPE_BOOLEAN:
|
||||
return 'BOOLEAN'
|
||||
elif prop['type'] == PROP_TYPE_STRING:
|
||||
return 'STRING'
|
||||
return 'UNKNOWN'
|
||||
|
||||
|
||||
def encode_prop_value(prop, value):
|
||||
try:
|
||||
if prop['type'] == PROP_TYPE_TIMESTAMP:
|
||||
return struct.pack('q', value)
|
||||
elif prop['type'] == PROP_TYPE_INT32:
|
||||
return struct.pack('i', value)
|
||||
elif prop['type'] == PROP_TYPE_BOOLEAN:
|
||||
return struct.pack('?', value)
|
||||
elif prop['type'] == PROP_TYPE_STRING:
|
||||
return bytes(value, encoding='latin-1')
|
||||
return value
|
||||
except struct.error as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
def decode_prop_value(prop, value):
|
||||
try:
|
||||
if prop['type'] == PROP_TYPE_TIMESTAMP:
|
||||
return struct.unpack('q', value)[0]
|
||||
elif prop['type'] == PROP_TYPE_INT32:
|
||||
return struct.unpack('i', value)[0]
|
||||
elif prop['type'] == PROP_TYPE_BOOLEAN:
|
||||
return struct.unpack('?', value)[0]
|
||||
elif prop['type'] == PROP_TYPE_STRING:
|
||||
return value.decode('latin-1')
|
||||
return value
|
||||
except struct.error as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
def str_to_prop_value(prop, strval):
|
||||
try:
|
||||
if prop['type'] == PROP_TYPE_TIMESTAMP:
|
||||
return int(strval)
|
||||
elif prop['type'] == PROP_TYPE_INT32:
|
||||
return int(strval)
|
||||
elif prop['type'] == PROP_TYPE_BOOLEAN:
|
||||
return bool(strval)
|
||||
elif prop['type'] == PROP_TYPE_STRING:
|
||||
return strval
|
||||
return strval
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
def prop_is_readonly(prop):
|
||||
return (prop['flags'] & PROP_FLAG_READONLY) != 0
|
||||
|
||||
|
||||
def on_except(err):
|
||||
if config_throw_except:
|
||||
raise RuntimeError(err)
|
||||
else:
|
||||
print(err)
|
||||
|
||||
|
||||
def get_security(secver, sec_patch_ver, username, password, pop='', verbose=False):
|
||||
if secver == 2:
|
||||
return security.Security2(sec_patch_ver, username, password, verbose)
|
||||
if secver == 1:
|
||||
return security.Security1(pop, verbose)
|
||||
if secver == 0:
|
||||
return security.Security0(verbose)
|
||||
return None
|
||||
|
||||
|
||||
async def get_transport(sel_transport, service_name, check_hostname):
|
||||
try:
|
||||
tp = None
|
||||
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 = 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',
|
||||
},
|
||||
)
|
||||
await tp.connect(devname=service_name)
|
||||
return tp
|
||||
except RuntimeError as e:
|
||||
on_except(e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_sec_patch_ver(tp, verbose=False):
|
||||
try:
|
||||
response = await tp.send_data('esp_local_ctrl/version', '---')
|
||||
|
||||
if verbose:
|
||||
print('esp_local_ctrl/version response : ', response)
|
||||
|
||||
try:
|
||||
# Interpret this as JSON structure containing
|
||||
# information with security version information
|
||||
info = json.loads(response)
|
||||
try:
|
||||
sec_patch_ver = info['local_ctrl']['sec_patch_ver']
|
||||
except KeyError:
|
||||
sec_patch_ver = 0
|
||||
return sec_patch_ver
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, we assume default patch level
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
on_except(e)
|
||||
return None
|
||||
|
||||
|
||||
async def version_match(tp, protover, verbose=False):
|
||||
try:
|
||||
response = await tp.send_data('esp_local_ctrl/version', protover)
|
||||
|
||||
if verbose:
|
||||
print('esp_local_ctrl/version response : ', response)
|
||||
|
||||
# First assume this to be a simple version string
|
||||
if response.lower() == protover.lower():
|
||||
return True
|
||||
|
||||
try:
|
||||
# Else interpret this as JSON structure containing
|
||||
# information with versions and capabilities of both
|
||||
# provisioning service and application
|
||||
info = json.loads(response)
|
||||
if info['local_ctrl']['ver'].lower() == protover.lower():
|
||||
return True
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, it means that capabilities
|
||||
# are not supported
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
on_except(e)
|
||||
return None
|
||||
|
||||
|
||||
async def has_capability(tp, capability='none', verbose=False):
|
||||
# Note : default value of `capability` argument cannot be empty string
|
||||
# because protocomm_httpd expects non zero content lengths
|
||||
try:
|
||||
response = await tp.send_data('esp_local_ctrl/version', capability)
|
||||
|
||||
if verbose:
|
||||
print('esp_local_ctrl/version response : ', response)
|
||||
|
||||
try:
|
||||
# Interpret this as JSON structure containing
|
||||
# information with versions and capabilities of both
|
||||
# provisioning service and application
|
||||
info = json.loads(response)
|
||||
try:
|
||||
supported_capabilities = info['local_ctrl']['cap']
|
||||
if capability.lower() == 'none':
|
||||
# No specific capability to check, but capabilities
|
||||
# feature is present so return True
|
||||
return True
|
||||
elif capability in supported_capabilities:
|
||||
return True
|
||||
return False
|
||||
except KeyError:
|
||||
# If capabilities field is not present, it means
|
||||
# that capabilities are not supported
|
||||
return False
|
||||
|
||||
except ValueError:
|
||||
# If decoding as JSON fails, it means that capabilities
|
||||
# are not supported
|
||||
return False
|
||||
|
||||
except RuntimeError as e:
|
||||
on_except(e)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def establish_session(tp, sec):
|
||||
try:
|
||||
response = None
|
||||
while True:
|
||||
request = sec.security_session(response)
|
||||
if request is None:
|
||||
break
|
||||
response = await tp.send_data('esp_local_ctrl/session', request)
|
||||
if response is None:
|
||||
return False
|
||||
return True
|
||||
except RuntimeError as e:
|
||||
on_except(e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_all_property_values(tp, security_ctx):
|
||||
try:
|
||||
props = []
|
||||
message = proto_lc.get_prop_count_request(security_ctx)
|
||||
response = await tp.send_data('esp_local_ctrl/control', message)
|
||||
count = proto_lc.get_prop_count_response(security_ctx, response)
|
||||
if count == 0:
|
||||
raise RuntimeError('No properties found!')
|
||||
indices = [i for i in range(count)]
|
||||
message = proto_lc.get_prop_vals_request(security_ctx, indices)
|
||||
response = await tp.send_data('esp_local_ctrl/control', message)
|
||||
props = proto_lc.get_prop_vals_response(security_ctx, response)
|
||||
if len(props) != count:
|
||||
raise RuntimeError('Incorrect count of properties!', len(props), count)
|
||||
for p in props:
|
||||
p['value'] = decode_prop_value(p, p['value'])
|
||||
return props
|
||||
except RuntimeError as e:
|
||||
on_except(e)
|
||||
return []
|
||||
|
||||
|
||||
async def set_property_values(tp, security_ctx, props, indices, values, check_readonly=False):
|
||||
try:
|
||||
if check_readonly:
|
||||
for index in indices:
|
||||
if prop_is_readonly(props[index]):
|
||||
raise RuntimeError('Cannot set value of Read-Only property')
|
||||
message = proto_lc.set_prop_vals_request(security_ctx, indices, values)
|
||||
response = await tp.send_data('esp_local_ctrl/control', message)
|
||||
return proto_lc.set_prop_vals_response(security_ctx, response)
|
||||
except RuntimeError as e:
|
||||
on_except(e)
|
||||
return False
|
||||
|
||||
|
||||
def desc_format(*args):
|
||||
desc = ''
|
||||
for arg in args:
|
||||
desc += textwrap.fill(replace_whitespace=False, text=arg) + '\n'
|
||||
return desc
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
|
||||
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('--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(
|
||||
'--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(
|
||||
'--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_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(
|
||||
'--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')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.secver == 2 and args.sec2_gen_cred:
|
||||
if not args.sec2_usr or not args.sec2_pwd:
|
||||
raise ValueError('Username/password cannot be empty for security scheme 2 (SRP6a)')
|
||||
|
||||
print('==== Salt-verifier for security scheme 2 (SRP6a) ====')
|
||||
security.sec2_gen_salt_verifier(args.sec2_usr, args.sec2_pwd, args.sec2_salt_len)
|
||||
sys.exit()
|
||||
|
||||
if args.version != '':
|
||||
print(f'==== Esp_Ctrl Version: {args.version} ====')
|
||||
|
||||
if args.service_name == '':
|
||||
args.service_name = 'my_esp_ctrl_device'
|
||||
if args.transport == 'http' or args.transport == 'https':
|
||||
args.service_name += '.local'
|
||||
|
||||
obj_transport = await get_transport(args.transport, args.service_name, not args.dont_check_hostname)
|
||||
if obj_transport is None:
|
||||
raise RuntimeError('Failed to establish connection')
|
||||
|
||||
sec_patch_ver = 0
|
||||
# If security version not specified check in capabilities
|
||||
if args.secver is None:
|
||||
# First check if capabilities are supported or not
|
||||
if not await has_capability(obj_transport):
|
||||
print('Security capabilities could not be determined, please specify "--sec_ver" explicitly')
|
||||
raise ValueError('Invalid Security Version')
|
||||
|
||||
# When no_sec is present, use security 0, else security 1
|
||||
args.secver = int(not await has_capability(obj_transport, 'no_sec'))
|
||||
print(f'==== Security Scheme: {args.secver} ====')
|
||||
|
||||
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 ----')
|
||||
exit(2)
|
||||
elif len(args.pop) != 0:
|
||||
print('---- Proof of Possession will be ignored ----')
|
||||
args.pop = ''
|
||||
|
||||
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: ')
|
||||
if len(args.sec2_pwd) == 0:
|
||||
prompt_str = 'Security Scheme 2 - SRP6a Password required: '
|
||||
args.sec2_pwd = getpass(prompt_str)
|
||||
|
||||
obj_security = get_security(args.secver, sec_patch_ver, args.sec2_usr, args.sec2_pwd, args.pop, args.verbose)
|
||||
if obj_security is None:
|
||||
raise ValueError('Invalid Security Version')
|
||||
|
||||
if args.version != '':
|
||||
print('\n==== Verifying protocol version ====')
|
||||
if not await version_match(obj_transport, args.version, args.verbose):
|
||||
raise RuntimeError('Error in protocol version matching')
|
||||
print('==== Verified protocol version successfully ====')
|
||||
|
||||
print('\n==== Starting Session ====')
|
||||
if not await establish_session(obj_transport, obj_security):
|
||||
print('Failed to establish session. Ensure that security scheme and proof of possession are correct')
|
||||
raise RuntimeError('Error in establishing session')
|
||||
print('==== Session Established ====')
|
||||
|
||||
while True:
|
||||
properties = await get_all_property_values(obj_transport, obj_security)
|
||||
if len(properties) == 0:
|
||||
raise RuntimeError('Error in reading property value')
|
||||
|
||||
print('\n==== Available Properties ====')
|
||||
print('{: >4} {: <16} {: <10} {: <16} {: <16}'.format('S.N.', 'Name', 'Type', 'Flags', 'Value'))
|
||||
for i in range(len(properties)):
|
||||
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) : ")
|
||||
if inval.lower() == 'q':
|
||||
print('Quitting...')
|
||||
exit(0)
|
||||
invals = inval.split(',')
|
||||
selections = [int(val) for val in invals]
|
||||
if min(selections) < 0 or max(selections) > len(properties):
|
||||
raise ValueError('Invalid input')
|
||||
break
|
||||
except ValueError as e:
|
||||
print(str(e) + '! Retry...')
|
||||
|
||||
if len(selections) == 1 and selections[0] == 0:
|
||||
continue
|
||||
|
||||
set_values = []
|
||||
set_indices = []
|
||||
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))
|
||||
if value is None:
|
||||
print('Invalid input! Retry...')
|
||||
continue
|
||||
break
|
||||
set_values += [value]
|
||||
set_indices += [select - 1]
|
||||
|
||||
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())
|
||||
@@ -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')
|
||||
@@ -0,0 +1,95 @@
|
||||
# SPDX-FileCopyrightText: 2018-2023 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']
|
||||
constants_pb2 = _load_source('constants_pb2', idf_path + '/components/protocomm/python/constants_pb2.py')
|
||||
local_ctrl_pb2 = _load_source('esp_local_ctrl_pb2', idf_path + '/components/esp_local_ctrl/python/esp_local_ctrl_pb2.py')
|
||||
|
||||
|
||||
def to_bytes(s: str) -> bytes:
|
||||
return bytes(s, encoding='latin-1')
|
||||
|
||||
|
||||
def get_prop_count_request(security_ctx):
|
||||
req = local_ctrl_pb2.LocalCtrlMessage()
|
||||
req.msg = local_ctrl_pb2.TypeCmdGetPropertyCount
|
||||
payload = local_ctrl_pb2.CmdGetPropertyCount()
|
||||
req.cmd_get_prop_count.MergeFrom(payload)
|
||||
enc_cmd = security_ctx.encrypt_data(req.SerializeToString())
|
||||
return enc_cmd.decode('latin-1')
|
||||
|
||||
|
||||
def get_prop_count_response(security_ctx, response_data):
|
||||
decrypt = security_ctx.decrypt_data(to_bytes(response_data))
|
||||
resp = local_ctrl_pb2.LocalCtrlMessage()
|
||||
resp.ParseFromString(decrypt)
|
||||
if (resp.resp_get_prop_count.status == 0):
|
||||
return resp.resp_get_prop_count.count
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def get_prop_vals_request(security_ctx, indices):
|
||||
req = local_ctrl_pb2.LocalCtrlMessage()
|
||||
req.msg = local_ctrl_pb2.TypeCmdGetPropertyValues
|
||||
payload = local_ctrl_pb2.CmdGetPropertyValues()
|
||||
payload.indices.extend(indices)
|
||||
req.cmd_get_prop_vals.MergeFrom(payload)
|
||||
enc_cmd = security_ctx.encrypt_data(req.SerializeToString())
|
||||
return enc_cmd.decode('latin-1')
|
||||
|
||||
|
||||
def get_prop_vals_response(security_ctx, response_data):
|
||||
decrypt = security_ctx.decrypt_data(to_bytes(response_data))
|
||||
resp = local_ctrl_pb2.LocalCtrlMessage()
|
||||
resp.ParseFromString(decrypt)
|
||||
results = []
|
||||
if (resp.resp_get_prop_vals.status == 0):
|
||||
for prop in resp.resp_get_prop_vals.props:
|
||||
results += [{
|
||||
'name': prop.name,
|
||||
'type': prop.type,
|
||||
'flags': prop.flags,
|
||||
'value': prop.value
|
||||
}]
|
||||
return results
|
||||
|
||||
|
||||
def set_prop_vals_request(security_ctx, indices, values):
|
||||
req = local_ctrl_pb2.LocalCtrlMessage()
|
||||
req.msg = local_ctrl_pb2.TypeCmdSetPropertyValues
|
||||
payload = local_ctrl_pb2.CmdSetPropertyValues()
|
||||
for i, v in zip(indices, values):
|
||||
prop = payload.props.add()
|
||||
prop.index = i
|
||||
prop.value = v
|
||||
req.cmd_set_prop_vals.MergeFrom(payload)
|
||||
enc_cmd = security_ctx.encrypt_data(req.SerializeToString())
|
||||
return enc_cmd.decode('latin-1')
|
||||
|
||||
|
||||
def set_prop_vals_response(security_ctx, response_data):
|
||||
decrypt = security_ctx.decrypt_data(to_bytes(response_data))
|
||||
resp = local_ctrl_pb2.LocalCtrlMessage()
|
||||
resp.ParseFromString(decrypt)
|
||||
return (resp.resp_set_prop_vals.status == 0)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from .convenience import * # noqa: F403, F401
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user