esp_prov: Added provision for SRP6a-based security scheme

This commit is contained in:
Laukik Hase 2022-05-31 10:49:23 +05:30 committed by BOT
parent 5169e22277
commit 3235206624
8 changed files with 568 additions and 64 deletions

View File

@ -2258,7 +2258,6 @@ tools/esp_prov/__init__.py
tools/esp_prov/prov/__init__.py
tools/esp_prov/prov/wifi_prov.py
tools/esp_prov/prov/wifi_scan.py
tools/esp_prov/security/__init__.py
tools/esp_prov/security/security.py
tools/esp_prov/security/security0.py
tools/esp_prov/security/security1.py
@ -2269,7 +2268,6 @@ tools/esp_prov/transport/transport_ble.py
tools/esp_prov/transport/transport_console.py
tools/esp_prov/transport/transport_http.py
tools/esp_prov/utils/__init__.py
tools/esp_prov/utils/convenience.py
tools/find_apps.py
tools/find_build_apps/__init__.py
tools/find_build_apps/cmake.py

View File

@ -1,4 +1,4 @@
# ESP Provisioning Tool
****# ESP Provisioning Tool
# NAME
`esp_prov` - A python based utility for testing the provisioning examples over a host
@ -31,31 +31,54 @@ Usage of `esp-prov` assumes that the provisioning app has specific protocomm end
Sets the verbosity level of output log
* `--transport <mode>`
Three options are available:
* `softap`
For SoftAP + HTTPD based provisioning. This assumes that the device is running in Wi-Fi SoftAP mode and hosts an HTTP server supporting specific endpoint URIs. Also client needs to connect to the device softAP network before running `esp_prov`
* `ble`
For BLE based provisioning (Linux support only. In Windows/macOS it redirects to console). This assumes that the provisioning endpoints are active on the device with specific BLE service UUIDs
* `console`
For debugging via console based provisioning. The client->device commands are printed to STDOUT and device->client messages are accepted via STDIN. This is to be used when device is accepting provisioning commands on UART console.
* `--ssid <AP SSID>` (Optional)
For specifying the SSID of the Wi-Fi AP to which the device is to connect after provisioning. If not provided, scanning is initiated and scan results, as seen by the device, are displayed, of which an SSID can be picked and the corresponding password specified.
* `--passphrase <AP Password>` (Optional)
For specifying the password of the Wi-Fi AP to which the device is to connect after provisioning. Only used when corresponding SSID is provided using `--ssid`
* `--sec_ver <Security version number>`
For specifying version of protocomm endpoint security to use. For now two versions are supported:
* `0` for `protocomm_security0`
* `1` for `protocomm_security1`
* `--pop <Proof of possession string>` (Optional)
For specifying optional Proof of Possession string to use for protocomm endpoint security version 1. This option is ignored when security version 0 is in use
- Three options are available:
* `softap` - for SoftAP + HTTPD based provisioning
* Requires the device to be running in Wi-Fi SoftAP mode and hosting an HTTP server supporting specific endpoint URIs
* The client needs to be connected to the device softAP network before running the `esp_prov` tool.
* `ble` - for BLE based provisioning
* Supports Linux only; on Windows/macOS, it is redirected to console
* Assumes that the provisioning endpoints are active on the device with specific BLE service UUIDs
* `console` - for debugging via console-based provisioning
* The client->device commands are printed to STDOUT and device->client messages are accepted via STDIN.
* This is to be used when the device is accepting provisioning commands on UART console.
* `--service_name <name>` (Optional)
When transport mode is ble, this specifies the BLE device name to which connection is to be established for provisioned.
When transport mode is softap, this specifies the HTTP server hostname / IP which is running the provisioning service, on the SoftAP network of the device which is to be provisioned. This defaults to `192.168.4.1:80` if not specified
- When transport mode is `ble`, this specifies the BLE device name to which connection is to be established for provisioned.
- When transport mode is `softap`, this specifies the HTTP server hostname / IP which is running the provisioning service, on the SoftAP network of the device which is to be provisioned. This defaults to `192.168.4.1:80` if not specified
* `--ssid <AP SSID>` (Optional)
- For specifying the SSID of the Wi-Fi AP to which the device is to connect after provisioning.
- If not provided, scanning is initiated and scan results, as seen by the device, are displayed, of which an SSID can be picked and the corresponding password specified.
* `--passphrase <AP Password>` (Optional)
- For specifying the password of the Wi-Fi AP to which the device is to connect after provisioning.
- Only used when corresponding SSID is provided using the `--ssid` option
* `--sec_ver <Security version number>`
- For specifying the version of protocomm endpoint security to use. Following 3 versions are supported:
* `0` for `protocomm_security0` - No security
* `1` for `protocomm_security1` - X25519 key exchange + Authentication using Proof of Possession (PoP) + AES-CTR encryption
* `2` for `protocomm_security2` - Secure Remote Password protocol (SRP6a) + AES-GCM encryption
* `--pop <Proof of possession string>` (Optional)
- For specifying optional Proof of Possession string to use for protocomm endpoint security version 1
- Ignored when other security versions are used
* `--sec2_username <SRP6a Username>` (Optional)
- For specifying optional username to use for protocomm endpoint security version 2
- Ignored when other security versions are used
* `--sec2_pwd <SRP6a Password>` (Optional)
- For specifying optional password to use for protocomm endpoint security version 2
- Ignored when other security versions are used
* `--sec2_gen_cred` (Optional)
- For generating the `SRP6a` credentials (salt and verifier) from the provided username and password for protocomm endpoint security version 2
- Ignored when other security versions are used
* `--sec2_salt_len <SRP6a Salt Length>` (Optional)
- For specifying the optional `SRP6a` salt length to be used for generating protocomm endpoint security version 2 credentials
- Ignored when other security versions are used and the ``--sec2_gen_cred` option is not set
* `--custom_data <some string>` (Optional)
An information string can be sent to the `custom-data` endpoint during provisioning using this argument.
@ -65,7 +88,7 @@ Usage of `esp-prov` assumes that the provisioning app has specific protocomm end
`esp_prov` is intended as a cross-platform tool, but currently BLE communication functionality is only available on Linux (via BlueZ and DBus)
For android, a provisioning tool along with source code is available [here](https://github.com/espressif/esp-idf-provisioning-android)
For Android, a provisioning tool along with source code is available [here](https://github.com/espressif/esp-idf-provisioning-android)
On macOS and Windows, running with `--transport ble` option falls back to console mode, ie. write data and target UUID are printed to STDOUT and read data is input through STDIN. Users are free to use their app of choice to connect to the BLE device, send the write data to the target characteristic and read from it.
@ -91,6 +114,6 @@ Run `pip install -r $IDF_PATH/tools/esp_prov/requirements_linux_extra.txt`
# EXAMPLE USAGE
Please refer to the README.md file with the `wifi_prov_mgr` example present under `$IDF_PATH/examples/provisioning/`.
Please refer to the `README.md` file with the `wifi_prov_mgr` example present under `$IDF_PATH/examples/provisioning/`.
This example uses specific options of the `esp_prov` tool and gives an overview of simple as well as advanced usage scenarios.

View File

@ -40,8 +40,10 @@ def on_except(err):
print(err)
def get_security(secver, pop='', verbose=False):
if secver == 1:
def get_security(secver, username, password, pop='', verbose=False):
if secver == 2:
return security.Security2(username, password, verbose)
elif secver == 1:
return security.Security1(pop, verbose)
elif secver == 0:
return security.Security0(verbose)
@ -331,15 +333,30 @@ if __name__ == '__main__':
'\t- 0 : No security',
'\t- 1 : X25519 key exchange + AES-CTR encryption',
'\t + Authentication using Proof of Possession (PoP)',
'\t- 2 : SRP6a + AES-GCM encryption',
'In case device side application uses IDF\'s provisioning manager, '
'the compatible security version is automatically determined from '
'capabilities retrieved via the version endpoint'))
parser.add_argument('--pop', dest='pop', type=str, default='',
parser.add_argument('--pop', dest='sec1_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('--ssid', dest='ssid', type=str, default='',
help=desc_format(
'This configures the device to use SSID of the Wi-Fi network to which '
@ -363,6 +380,14 @@ if __name__ == '__main__':
args = parser.parse_args()
if args.secver == 2 and args.sec2_gen_cred:
if not args.sec2_usr or not args.sec2_pwd:
print('---- Username/password cannot be empty for security scheme 2 (SRP6a) ----')
exit(1)
print('==== Salt-verifier for security scheme 2 (SRP6a) ====')
security.sec2_gen_salt_verifier(args.sec2_usr, args.sec2_pwd, args.sec2_salt_len)
exit(0)
obj_transport = get_transport(args.mode.lower(), args.name)
if obj_transport is None:
print('---- Failed to establish connection ----')
@ -381,14 +406,14 @@ if __name__ == '__main__':
print('Security scheme determined to be :', args.secver)
if (args.secver != 0) and not has_capability(obj_transport, 'no_pop'):
if len(args.pop) == 0:
if len(args.sec1_pop) == 0:
print('---- Proof of Possession argument not provided ----')
exit(2)
elif len(args.pop) != 0:
elif len(args.sec1_pop) != 0:
print('---- Proof of Possession will be ignored ----')
args.pop = ''
args.sec1_pop = ''
obj_security = get_security(args.secver, args.pop, args.verbose)
obj_security = get_security(args.secver, args.sec2_usr, args.sec2_pwd, args.sec1_pop, args.verbose)
if obj_security is None:
print('---- Invalid Security Version ----')
exit(2)

View File

@ -9,8 +9,11 @@ from importlib.abc import Loader
from typing import Any
def _load_source(name, path): # type: (str, str) -> 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)
@ -24,6 +27,7 @@ idf_path = os.environ['IDF_PATH']
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')
# wifi_provisioning component related python files generated from .proto files

View File

@ -1,17 +1,7 @@
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
from .security0 import * # noqa: F403, F401
from .security1 import * # noqa: F403, F401
from .security2 import * # noqa: F403, F401

View File

@ -0,0 +1,167 @@
# 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_security2
from typing import Any, Type
import proto
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from future.utils import tobytes
from .security import Security
from .srp6a import Srp6a, bytes_to_long, generate_salt_and_verifier, long_to_bytes
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')
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')
class Security2(Security):
def __init__(self, username: str, password: str, verbose: bool) -> None:
# Initialize state of the security2 FSM
self.session_state = security_state.REQUEST1
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 = None
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')
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 = tobytes(self.username)
self.srp6a_ctx = Srp6a(self.username, self.password)
if self.srp6a_ctx is None:
print('Failed to initialize SRP6a instance!')
exit(1)
client_pubkey = long_to_bytes(self.srp6a_ctx.A)
setup_req.sec2.sc0.client_pubkey = client_pubkey
self._print_verbose('Client Public Key:\t' + hex(bytes_to_long(client_pubkey)))
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(tobytes(response_data))
self._print_verbose('Security version:\t' + str(setup_resp.sec_ver))
if setup_resp.sec_ver != proto.session_pb2.SecScheme2:
print('Incorrect sec scheme')
exit(1)
# 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('Device Public Key:\t' + hex(bytes_to_long(device_pubkey)))
self._print_verbose('Device Salt:\t' + hex(bytes_to_long(device_salt)))
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
self._print_verbose('Client Proof:\t' + hex(bytes_to_long(self.client_pop_key)))
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(tobytes(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('Device Proof:\t' + hex(bytes_to_long(device_proof)))
self.srp6a_ctx.verify_session(device_proof)
if not self.srp6a_ctx.authenticated():
print('Failed to verify device proof')
exit(1)
else:
print('Unsupported security protocol')
exit(1)
# Getting the shared secret
shared_secret = self.srp6a_ctx.get_session_key()
self._print_verbose('Shared Secret:\t' + hex(bytes_to_long(shared_secret)))
# Using the first 256 bits of a 512 bit key
session_key = shared_secret[:AES_KEY_LEN]
self._print_verbose('Session Key:\t' + hex(bytes_to_long(session_key)))
# 96-bit nonce
self.nonce = setup_resp.sec2.sr1.device_nonce
self._print_verbose('Nonce:\t' + hex(bytes_to_long(self.nonce)))
# Initialize the encryption engine with Shared Key and initialization vector
self.cipher = AESGCM(session_key)
if self.cipher is None:
print('Failed to initialize AES-GCM cryptographic engine!')
exit(1)
def encrypt_data(self, data: bytes) -> Any:
return self.cipher.encrypt(self.nonce, data, None)
def decrypt_data(self, data: bytes) -> Any:
return self.cipher.decrypt(self.nonce, data, None)

View File

@ -0,0 +1,308 @@
# 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 typing import Any, Callable, Optional, Tuple
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 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')
def get_random(nbytes: int) -> int:
return bytes_to_long(os.urandom(nbytes))
def get_random_of_length(nbytes: int) -> int:
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 (object):
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: Optional[int] = None
self.K: Optional[bytes] = 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) -> bytes:
return long_to_bytes(self.a)
def get_session_key(self) -> Any:
return self.K if self._authenticated else None
def start_authentication(self) -> Tuple[str, bytes]:
return (self.Iu, long_to_bytes(self.A))
# Returns M or None if SRP-6a safety check is violated
def process_challenge(self, bytes_s: bytes, bytes_B: bytes) -> Any:
s = bytes_to_long(bytes_s)
B = bytes_to_long(bytes_B)
N = self.N
g = self.g
k = self.k
hash_class = self.hash_class
# SRP-6a safety check
if (B % N) == 0:
return None
u = H(hash_class, self.A, B, width=len(long_to_bytes(N)))
if u == 0: # SRP-6a safety check
return None
x = calculate_x(hash_class, s, self.Iu, self.p)
v = pow(g, x, N)
S = pow((B - k * v), (self.a + u * x), N)
self.K = hash_class(long_to_bytes(S)).digest()
M = calculate_M(hash_class, N, g, self.Iu, s, self.A, B, self.K)
if not M:
return None
self.H_AMK = calculate_H_AMK(hash_class, self.A, M, self.K)
return M
def verify_session(self, host_HAMK: bytes) -> None:
if self.H_AMK == host_HAMK:
self._authenticated = True
class AuthenticationFailed (Exception):
pass

View File

@ -1,16 +1,5 @@
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
# Convenience functions for commonly used data type conversions
@ -22,7 +11,7 @@ from future.utils import tobytes
def str_to_hexstr(string):
# Form hexstr by appending ASCII codes (in hex) corresponding to
# each character in the input string
return binascii.hexlify(tobytes(string)).decode()
return binascii.hexlify(tobytes(string)).decode('latin-1')
def hexstr_to_str(hexstr):
@ -31,4 +20,4 @@ def hexstr_to_str(hexstr):
hexstr = '0' + hexstr
# Interpret consecutive pairs of hex characters as 8 bit ASCII codes
# and append characters corresponding to each code to form the string
return binascii.unhexlify(tobytes(hexstr)).decode()
return binascii.unhexlify(tobytes(hexstr)).decode('latin-1')