Closed thisiscam closed 2 months ago
How did you compare the keys? keys_at
returns a set of KeyPair
s, did you compare those public keys against the one you got from the scan?
It's also possible there is some timing inaccuracy, you could try using keys_between
instead and pass two timezones, let's say -24h and +24h of the timestamp of the scan.
How did you compare the keys?
keys_at
returns a set ofKeyPair
s, did you compare those public keys against the one you got from the scan?
Yes, that's what I tried.
It's also possible there is some timing inaccuracy, you could try using
keys_between
instead and pass two timezones, let's say -24h and +24h of the timestamp of the scan.
Thanks! I am running this script:
import datetime, pytz
import findmy
import findmy.scanner
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
acc = findmy.accessory.FindMyAccessory.from_plist(open("mykey.plist", "rb")) # obtained from decoding FindMy
now = datetime.datetime.now(pytz.timezone("EST"))
keys = acc.keys_between(now - datetime.timedelta(hours=24), now + datetime.timedelta(hours=24))
async def scan() -> None:
scanner = await findmy.scanner.OfflineFindingScanner.create()
print("Scanning for FindMy-devices...")
print()
async for device in scanner.scan_for(1000, extend_timeout=True):
print(f"Device - {device.mac_address}")
print(f" Public key: {device.adv_key_b64}")
print(f" Lookup key: {device.hashed_adv_key_b64}")
print(f" Status byte: {device.status:x}")
print(f" Hint byte: {device.hint:x}")
print(" Extra data:")
for k, v in sorted(device.additional_data.items()):
print(f" {k:20}: {v}")
print()
if any((device.adv_key_b64 == k.adv_key_b64) for k in keys):
print("Found!!!!")
break
if __name__ == "__main__":
asyncio.run(scan())
Doesn't seem to hit Found!
after 1000 seconds. I do see two unmatched results, unclear if those are my AirTags or someone else's nearby. Maybe my AirTags are not broadcasting because my other devices are around?
Ohhh yes indeed then that will not work. As you guessed, Airtags do not broadcast when they're near an owner device. Well, they do, but it's a very small payload that is only used to indicate to the owner device that it is still nearby (afaik). I think the scanner currently skips those, because there is no way to retrieve the full identity of the tag from those reports.
It should still be possible to verify whether a given public key is nearby though. If you just scan for nearby bluetooth low energy MAC addresses, the last 4 bytes of your tag should be identical of byte 1 - 4 of the active public key (0-indexed). That's not currently implemented in the scanner, but I'll take it as a feature request π
Ohhh yes indeed then that will not work. As you guessed, Airtags do not broadcast when they're near an owner device. Well, they do, but it's a very small payload that is only used to indicate to the owner device that it is still nearby (afaik). I think the scanner currently skips those, because there is no way to retrieve the full identity of the tag from those reports.
It should still be possible to verify whether a given public key is nearby though. If you just scan for nearby bluetooth low energy MAC addresses, the last 4 bytes of your tag should be identical of byte 1 - 4 of the active public key (0-indexed). That's not currently implemented in the scanner, but I'll take it as a feature request π
Fabulous!
Here's my attempt:
import datetime, pytz
import findmy
import findmy.scanner
import findmy.scanner.scanner
import asyncio
import logging
import time
logging.basicConfig(level=logging.INFO)
acc = findmy.accessory.FindMyAccessory.from_plist(open("myairtag.plist", "rb"))
now = datetime.datetime.now(pytz.timezone("EST"))
keys = acc.keys_between(now - datetime.timedelta(hours=24), now + datetime.timedelta(hours=24))
async def scan() -> None:
scanner = await findmy.scanner.OfflineFindingScanner.create()
print("Scanning for FindMy-devices...")
print()
async for device in scan_for(scanner, 1000, extend_timeout=True):
print(f"Device - {device.mac_address}")
print(f" Public key: {device.adv_key_b64}")
print(f" Lookup key: {device.hashed_adv_key_b64}")
print(f" Status byte: {device.status:x}")
print(f" Hint byte: {device.hint:x}")
print(" Extra data:")
for k, v in sorted(device.additional_data.items()):
print(f" {k:20}: {v}")
print()
if any((device.adv_key_b64 == k.adv_key_b64) for k in keys):
print("Found!!!!")
break
def has_consecutive_four_byte_match(bytes1, bytes2):
length1 = len(bytes1)
length2 = len(bytes2)
if length1 < 4 or length2 < 4:
return False
for i in range(length1 - 3):
four_bytes1 = bytes1[i:i+4]
for j in range(length2 - 3):
four_bytes2 = bytes2[j:j+4]
if four_bytes1 == four_bytes2:
return True
return False
async def _wait_for_device(self, timeout: float):
device, data = await asyncio.wait_for(self._device_fut, timeout=timeout)
mac_bytes = bytes(int(part, 16) for part in device.address.split(':'))
for k in keys:
if has_consecutive_four_byte_match(mac_bytes, k.adv_key_bytes):
print("Found MAC!!!")
print(device)
print(data)
print(mac_bytes)
print(k.adv_key_bytes)
apple_data = data.manufacturer_data.get(self.BLE_COMPANY_APPLE, b"")
if not apple_data:
return None
try:
additional_data = device.details.get("props", {})
except AttributeError:
# Likely Windows host, where details is a '_RawAdvData' object.
# See: https://github.com/malmeloo/FindMy.py/issues/24
additional_data = {}
return findmy.scanner.scanner.OfflineFindingDevice.from_payload(device.address, apple_data, additional_data)
async def scan_for(
self,
timeout: float = 10,
*,
extend_timeout: bool = False,
):
"""
Scan for `OfflineFindingDevice`s for up to `timeout` seconds.
If `extend_timeout` is set, the timer will be extended
by `timeout` seconds every time a new device is discovered.
"""
await self._start_scan()
stop_at = time.time() + timeout
devices_seen = set()
try:
time_left = stop_at - time.time()
while time_left > 0:
device = await _wait_for_device(self, time_left)
if device is not None and device not in devices_seen:
devices_seen.add(device)
if extend_timeout:
stop_at = time.time() + timeout
yield device
time_left = stop_at - time.time()
except (asyncio.CancelledError, asyncio.TimeoutError): # timeout reached
return
finally:
await self._stop_scan()
if __name__ == "__main__":
asyncio.run(scan())
Note that I tried what you described on the indexing but that didn't give me any match. So I used that bruteforce has_consecutive_four_byte_match
function, and I got a match!
Here's the match pretty printed:
MAC: 'e5:29:d7:92:57:65'
adv_key_bytes: 'a5:29:d7:92:57:65:ac:2a:9b:0f:3c:5a:eb:f4:9e:08:2b:78:6f:d7:db:39:f6:08:76:36:a4:f5'
It seems like MAC[-5:] == adv_key_bytes[1:6]
? Five matching bytes?
5 matching bytes indeed, I think I had a brain fart there π
. Actually, the first 6 bytes of the public key are encoded in the MAC address, with the exception of the two most significant bits of the first byte. Those two are always 0b11
due to BLE requirements, so Apple put them in the two MSBs of the 3rd byte of the advertisement payload instead.
I've started implementing 'nearby' status device detection in #50. I'll extend that to allow checking whether a device corresponds to a specific keypair or findmy-accessory.
Hi!
Since #41, I am pretty convinced that close-to-realtime location using the crowdsourcing FindMy approach won't fit my application (tracking my dog). I was thinking of using an AirTag as a BLE tracker just so that I can know in almost real-time that the AirTag is at home.
Is there a way to know if a particular AirTag is present in the scan results? I see that the scanner gives the MAC address and the public key of any AirTag. But, given an AirTag, how do I know either of these two values for that particular AirTag? The public key is rotating, am I right? So maybe there is a way to predict this public key using the seed plist?
I have tried:
But the key it returns doesn't seem to match with any of the scanner results. I also tried passing in my current timezone (EST) to
keys_at
, but still no luck.Any guidance or suggestions would be greatly appreciated!