feat: expand companion platform, device controls, and memoir workflows
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.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,84 @@
"""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()