Files

85 lines
4.5 KiB
Python
Raw Permalink Normal View History

"""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()