You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

109 lines
4.5 KiB

#!/usr/bin/env python3
"""
Generate FindMy / OpenHaystack advertising keys for the MeshCore nRF52 FindMy beacon.
Keys are derived deterministically from a random 32-byte seed so the whole set can be
regenerated from that seed alone. Each slot is a NIST P-224 (secp224r1) keypair:
d_i = SHA256(seed || "findmy" || i) reduced into [1, n-1] (private scalar)
P_i = d_i * G (public point)
adv = X coordinate of P_i (28 bytes) -> goes on the device
hash = SHA256(adv) -> server lookup id for Apple's network
The device only ever stores the public `adv` keys and picks the active slot from its clock:
slot = (now_utc / 86400) % count
so the keys rotate daily and cycle every `count` days. count == 1 is a single static key.
Outputs (into --out):
seed.txt the master seed (hex) - keep this safe, it regenerates everything
provision.txt CLI script: `set findmy.clear`, one `set findmy.add <b64>` per slot,
then `set findmy on`. Paste/pipe into the node's serial console.
keys/<i>.keys per-slot OpenHaystack/macless-haystack key file (Private/Advertisement/
Hashed adv key) for the server side.
Requires: cryptography (pip install cryptography)
Examples:
python3 genkeys.py --count 365 --out mytag
python3 genkeys.py --count 1 --out mytag # single static key
python3 genkeys.py --count 30 --seed <hex> --out mytag # reproduce from a seed
"""
import argparse
import base64
import hashlib
import os
import sys
try:
from cryptography.hazmat.primitives.asymmetric import ec
except ImportError:
sys.exit("This script needs the 'cryptography' package: pip install cryptography")
# Order of the secp224r1 / NIST P-224 curve.
P224_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D
def derive_private_scalar(seed: bytes, index: int) -> int:
"""Deterministic private scalar in [1, n-1] from seed and slot index."""
counter = 0
while True:
h = hashlib.sha256(seed + b"findmy" + index.to_bytes(4, "big") + bytes([counter])).digest()
d = int.from_bytes(h, "big") % P224_N
if d != 0:
return d
counter += 1
def slot_keys(seed: bytes, index: int):
"""Return (private_b64, adv_b64, hashed_b64) for a slot."""
d = derive_private_scalar(seed, index)
priv = ec.derive_private_key(d, ec.SECP224R1())
x = priv.public_key().public_numbers().x
adv = x.to_bytes(28, "big") # advertised public key (X coordinate)
private = d.to_bytes(28, "big")
hashed = hashlib.sha256(adv).digest()
b64 = lambda b: base64.b64encode(b).decode()
return b64(private), b64(adv), b64(hashed)
def main():
ap = argparse.ArgumentParser(description="Generate FindMy keys for the MeshCore nRF52 beacon.")
ap.add_argument("--count", type=int, default=365, help="number of daily slots (1..365)")
ap.add_argument("--seed", help="32-byte master seed as hex (random if omitted)")
ap.add_argument("--out", default="findmy_keys", help="output directory")
args = ap.parse_args()
if not 1 <= args.count <= 365:
sys.exit("--count must be between 1 and 365")
seed = bytes.fromhex(args.seed) if args.seed else os.urandom(32)
os.makedirs(os.path.join(args.out, "keys"), exist_ok=True)
with open(os.path.join(args.out, "seed.txt"), "w") as f:
f.write(seed.hex() + "\n")
with open(os.path.join(args.out, "provision.txt"), "w") as prov:
prov.write("set findmy.clear\n")
for i in range(args.count):
private_b64, adv_b64, hashed_b64 = slot_keys(seed, i)
prov.write(f"set findmy.add {adv_b64}\n")
with open(os.path.join(args.out, "keys", f"{i:03d}.keys"), "w") as kf:
kf.write(f"Private key: {private_b64}\n")
kf.write(f"Advertisement key: {adv_b64}\n")
kf.write(f"Hashed adv key: {hashed_b64}\n")
prov.write("set findmy on\n")
print(f"Generated {args.count} key(s) in '{args.out}/'")
print(f" seed: {args.out}/seed.txt (keep safe - regenerates all keys)")
print(f" device: {args.out}/provision.txt (pipe/paste into the node serial console)")
print(f" server keys: {args.out}/keys/*.keys (import into macless-haystack)")
print()
print("Provision a node over USB, e.g.:")
print(f" while read line; do echo \"$line\"; sleep 0.2; done < {args.out}/provision.txt > /dev/ttyACM0")
print("then `reboot` the node.")
if __name__ == "__main__":
main()