ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
85 lines
4.5 KiB
Python
85 lines
4.5 KiB
Python
"""Create a per-device NVS image and setup label; never print device secrets.
|
|
|
|
Use with the *existing* physical_confirm inventory credential, not a newly
|
|
invented server key. Output is private production material, not source code.
|
|
"""
|
|
import argparse
|
|
import csv
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import secrets
|
|
import subprocess
|
|
import sys
|
|
from urllib.parse import urlsplit
|
|
|
|
N = int(''.join([
|
|
'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E',
|
|
'3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE',
|
|
'9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED5290770',
|
|
'96966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF69558171839',
|
|
'95497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6',
|
|
'E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C77',
|
|
'0988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF']), 16)
|
|
|
|
|
|
def security_material(username, password, salt):
|
|
"""Match esp_srp.c: hash raw salt and SHA512 bytes (preserve leading zeros)."""
|
|
inner = hashlib.sha512(f'{username}:{password}'.encode()).digest()
|
|
x = int.from_bytes(hashlib.sha512(salt + inner).digest(), 'big')
|
|
return pow(5, x, N).to_bytes(384, 'big')
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--device-id', required=True)
|
|
p.add_argument('--model', default='ECHOEAR-S3')
|
|
p.add_argument('--origin', required=True)
|
|
p.add_argument('--key-version', type=int, required=True)
|
|
p.add_argument('--device-key-file', type=Path, required=True, help='64 hex chars or controlled migration credential JSON')
|
|
p.add_argument('--output', type=Path, required=True, help='new private directory; refuses overwrite')
|
|
a = p.parse_args()
|
|
origin = urlsplit(a.origin)
|
|
if (not re.fullmatch(r'KP01-[0-9A-F]{12}', a.device_id) or
|
|
not re.fullmatch(r'[A-Za-z0-9_.-]{1,32}', a.model) or
|
|
not 1 <= a.key_version <= 0xffffffff or len(a.origin) > 150 or
|
|
origin.scheme != 'https' or not origin.hostname or origin.username or
|
|
origin.path or origin.query or origin.fragment):
|
|
p.error('invalid identity, model, key version or HTTPS origin')
|
|
key_hex = a.device_key_file.read_text(encoding='ascii').strip()
|
|
if key_hex.startswith('{'):
|
|
identity = json.loads(key_hex)
|
|
if identity.get('device_id') != a.device_id or identity.get('key_version') != a.key_version:
|
|
p.error('credential identity does not match the requested factory image')
|
|
key_hex = identity.get('device_key_hex', '')
|
|
if not re.fullmatch(r'[0-9a-fA-F]{64}', key_hex):
|
|
p.error('device key must be exactly 32 bytes in hexadecimal')
|
|
a.output.mkdir(parents=True, exist_ok=False, mode=0o700)
|
|
password = secrets.token_urlsafe(16)
|
|
salt = secrets.token_bytes(16)
|
|
verifier = security_material(a.device_id, password, salt)
|
|
values = [('identity', 'namespace', '', ''),
|
|
('factory_id', 'data', 'string', a.device_id),
|
|
('model', 'data', 'string', a.model),
|
|
('origin', 'data', 'string', a.origin),
|
|
('key_version', 'data', 'u32', a.key_version),
|
|
('device_key', 'data', 'hex2bin', key_hex),
|
|
('salt', 'data', 'hex2bin', salt.hex()),
|
|
('verifier', 'data', 'hex2bin', verifier.hex())]
|
|
csv_path = a.output / 'factory.csv'
|
|
with csv_path.open('x', encoding='utf-8', newline='') as f:
|
|
writer = csv.writer(f); writer.writerow(('key', 'type', 'encoding', 'value')); writer.writerows(values)
|
|
os.chmod(csv_path, 0o600)
|
|
subprocess.run([sys.executable, '-m', 'esp_idf_nvs_partition_gen', 'generate',
|
|
str(csv_path.resolve()), str((a.output / 'ble_factory.bin').resolve()), '0x6000'], check=True)
|
|
label = a.output / 'setup-label.json'
|
|
label.write_text(json.dumps({'device_id': a.device_id, 'model': a.model, 'setup_password': password}, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
os.chmod(label, 0o600); os.chmod(a.output / 'ble_factory.bin', 0o600)
|
|
print(f'Created private NVS image and label in {a.output.resolve()}. No device key is present in the label.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|