mirror of
https://github.com/espressif/esp-idf.git
synced 2025-11-11 16:20:50 +00:00
component: Remove wifi_provisioning component and esp_prov tool
This commit is contained in:
@@ -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())
|
||||
|
||||
31
examples/protocols/esp_local_ctrl/scripts/proto/__init__.py
Normal file
31
examples/protocols/esp_local_ctrl/scripts/proto/__init__.py
Normal 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')
|
||||
@@ -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
|
||||
145
examples/protocols/esp_local_ctrl/scripts/security/security1.py
Normal file
145
examples/protocols/esp_local_ctrl/scripts/security/security1.py
Normal 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)
|
||||
181
examples/protocols/esp_local_ctrl/scripts/security/security2.py
Normal file
181
examples/protocols/esp_local_ctrl/scripts/security/security2.py
Normal 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
|
||||
317
examples/protocols/esp_local_ctrl/scripts/security/srp6a.py
Normal file
317
examples/protocols/esp_local_ctrl/scripts/security/srp6a.py
Normal 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
|
||||
@@ -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
|
||||
221
examples/protocols/esp_local_ctrl/scripts/transport/ble_cli.py
Normal file
221
examples/protocols/esp_local_ctrl/scripts/transport/ble_cli.py
Normal 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()
|
||||
@@ -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