mirror of
https://github.com/espressif/esp-idf
synced 2025-03-09 17:19:09 -04:00
esp_prov: Added provision for SRP6a-based security scheme
This commit is contained in:
parent
5169e22277
commit
3235206624
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
167
tools/esp_prov/security/security2.py
Normal file
167
tools/esp_prov/security/security2.py
Normal 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)
|
308
tools/esp_prov/security/srp6a.py
Normal file
308
tools/esp_prov/security/srp6a.py
Normal 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
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user