Просмотр исходного кода

Improve scpair.py to allow cloning (#119)

Florian Bach 3 месяцев назад
Родитель
Сommit
f86a3733e7
2 измененных файлов с 130 добавлено и 5 удалено
  1. 21 0
      ReversePuck/PUCK_CLONING.md
  2. 109 5
      ReversePuck/scpair.py

+ 21 - 0
ReversePuck/PUCK_CLONING.md

@@ -0,0 +1,21 @@
+# Puck cloning
+
+The Steam Controller has two different pairing slots that each contain an 8-byte key and the serial number of the paired puck. The Steam Puck has four pairing slots that each contain an 8-byte key and the serial number of the paired controller. 
+
+However, the serial numbers both on the Puck and on the Controller seem to only be used to display them to the user. The only thing that handles the pairing is the 8-byte key. 
+
+Usually, when pairing, Steam generates that key randomly, then writes it to the Puck and to the Controller. 
+
+With the script `scpair.py`, it is possible to read these pairing records both from the controller and from a Puck (Steam Puck or OpenPuck). Then, with the key known, you can pair one single controller slot to multiple Pucks, so they seamlessly switch to whichever Puck they can reach. 
+
+`scpair.py list` lists all pairings on the attached pucks, including the controller serial and the 8-byte pairing key. 
+
+Use `scpair.py write-controller --slot <0|1> --serial <puck-serial> --key <pairing-key>` to write the key to a controller.
+
+Use `scpair.py write-puck --slot <0|1|2|3> --serial <controller-serial> --key <pairing-key>` to write the key to a Puck. 
+
+For example, if you want to pair one controller to multiple different pucks, then just pair the controller to one Puck normally, then run `scpair.py list`. This will dump the pairing record from the controller pair. Note the controller serial and the key. 
+
+Then plug in another puck and run `scpair.py write-puck --serial <controller-serial> --key <pairing-key>`. Repeat that process with as many pucks as you like. 
+
+Now your controller will always choose any of the available pucks to connect to.

+ 109 - 5
ReversePuck/scpair.py

@@ -185,7 +185,7 @@ def read_slots(puck):
     return slots
 
 
-def pair(slot=None):
+def pair(slot=None, pkey=None):
     devs = enumerate_devices()
     pucks = sorted(devs["puck"], key=lambda d: d.node)
     ctrls = sorted(devs["ctrl"], key=lambda d: d.node)
@@ -203,7 +203,11 @@ def pair(slot=None):
     if slot >= len(pucks):
         raise SystemExit("slot %d has no puck control interface" % slot)
 
-    r = os.urandom(8)
+    if pkey is None: 
+        r = os.urandom(8)
+    else: 
+        r = pkey
+
     key = b"esb/bond\x00" if slot == 0 else b"esb/bond_2\x00"
     print("pairing puck %s slot %d <-> controller %s  key=%s" %
           (puck_serial, slot, ctrl_serial, r.hex()))
@@ -218,6 +222,74 @@ def pair(slot=None):
     ctrl.set_feature(1, 0x95, bytes([0x52, 0xAF, 0x27, 0xA4]))
     print("paired. controller rebooting into wireless — move the puck to your host.")
 
+def write_puck_slot(slot=None, ctrl_serial=None, pkey=None):
+    devs = enumerate_devices()
+    pucks = sorted(devs["puck"], key=lambda d: d.node)
+    if not pucks:
+        raise SystemExit("no puck (28DE:1304) control interface found")
+
+    if slot is None:
+        slots = read_slots(pucks)
+        slot = next((s["idx"] for s in slots if not s["used"]), 0)
+    if slot >= len(pucks):
+        raise SystemExit("slot %d has no puck control interface" % slot)
+
+    
+    if ctrl_serial is None and pkey is None: 
+        print("wiping puck slot %d" % (slot))
+        pucks[slot].set_feature(2, 0xA2, b"\x00" * 24)
+    else:
+        if pkey is None: 
+            raise SystemExit("No key provided!")
+        
+        print("writing to puck slot %d: controller %s key=%s" %
+          (slot, ctrl_serial, pkey.hex()))
+        # puck slot: 0xA2 [r1 r2][ctrl serial]
+        pucks[slot].set_feature(2, 0xA2, pkey + _ser16(ctrl_serial))
+
+    print("done")
+
+def write_controller_slot(slot=0, puck_serial=None, pkey=None):
+    devs = enumerate_devices()
+    ctrls = sorted(devs["ctrl"], key=lambda d: d.node)
+    if not ctrls:
+        raise SystemExit("no controller (28DE:1302) control interface found — plug the ReversePuck controller in")
+    
+    if slot not in [0, 1]:
+        raise SystemExit("invalid controller slot %d" % (slot))
+
+
+    ctrl = ctrls[0]
+
+    if pkey is None: 
+        r = os.urandom(8)
+    else: 
+        r = pkey
+
+    key = b"esb/bond\x00" if slot == 0 else b"esb/bond_2\x00"
+
+    if puck_serial is None and pkey is None: 
+        print("Wiping controller slot %d" % (slot))
+        try:
+            ctrl.set_feature(1, 0xEE, key + b"\x00" * 24)
+            ctrl.set_feature(1, 0xEF, key)
+            print("Slot wiped.")
+        except Exception:
+            print("Slot wipe failed.")
+        
+    else: 
+
+        print("Writing controller slot %d: puck serial %s key=%s" %
+          (slot, puck_serial, r.hex()))
+
+        # controller esb/bond: 0xEE [key\0][r1 r2][puck serial] + 0xEF [key\0]
+        ctrl.set_feature(1, 0xEE, key + r + _ser16(puck_serial))
+        ctrl.set_feature(1, 0xEF, key)
+        time.sleep(0.1)
+        # reboot controller into wireless mode (magic 0xA427AF52)
+        ctrl.set_feature(1, 0x95, bytes([0x52, 0xAF, 0x27, 0xA4]))
+        print("paired. controller rebooting into wireless — move the puck to your host.")
+
 
 def unpair(slot):
     devs = enumerate_devices()
@@ -232,10 +304,11 @@ def unpair(slot):
             ctrl.set_feature(1, 0xEE, key + b"\x00" * 24)
             ctrl.set_feature(1, 0xEF, key)
         except Exception:
-            pass
+            print("Slot wipe on controller failed.")
     print("cleared slot %d" % slot)
 
 
+
 def cmd_list():
     devs = enumerate_devices()
     pucks = sorted(devs["puck"], key=lambda d: d.node)
@@ -244,26 +317,57 @@ def cmd_list():
     if pucks:
         print("puck:", pucks[0].read_serial(2))
         for s in read_slots(pucks):
-            print("  slot %d: %s" % (s["idx"], (s["serial"] + " (bonded)") if s["used"] else "empty"))
+            if s["used"]:
+                print("  slot %d: %s (key %s)" % (s["idx"], s["serial"], s["rec"][:8].hex()))
+            else: 
+                print("  slot %d: Empty" % (s["idx"]))
     else:
         print("puck: (none)")
 
 
+def steam_key_check(key):
+    try: 
+        key = bytes.fromhex(key)
+        if len(key) == 8: 
+            return key
+    except: 
+        raise argparse.ArgumentTypeError("Invalid pairing key!")
+
+
 def main():
     ap = argparse.ArgumentParser(description="Linux hidraw steamless pairing")
     sub = ap.add_subparsers(dest="cmd", required=True)
     sub.add_parser("list")
     p = sub.add_parser("pair")
     p.add_argument("--slot", type=int, default=None)
+    p.add_argument("--key", type=steam_key_check, default=None)
     u = sub.add_parser("unpair")
     u.add_argument("--slot", type=int, required=True)
+
+    s = sub.add_parser("write-puck")
+    s.add_argument("--slot", type=int, default=None)
+    s.add_argument("--serial", type=str, default=None)
+    s.add_argument("--key", type=steam_key_check, default=None)
+
+    c = sub.add_parser("write-controller")
+    c.add_argument("--slot", type=int, default=None)
+    c.add_argument("--serial", type=str, default=None)
+    c.add_argument("--key", type=steam_key_check, default=None)
+
+
+    
     args = ap.parse_args()
     if args.cmd == "list":
         cmd_list()
     elif args.cmd == "pair":
-        pair(args.slot)
+        pair(args.slot, args.key)
     elif args.cmd == "unpair":
         unpair(args.slot)
+    elif args.cmd == "write-puck":
+        write_puck_slot(args.slot, args.serial, args.key)
+    elif args.cmd == "write-controller":
+        write_controller_slot(args.slot, args.serial, args.key)
+
 
 
 if __name__ == "__main__":