component: Remove wifi_provisioning component and esp_prov tool

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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