malmeloo / FindMy.py

🍏 + 🎯 + 🐍 = Everything you need to work with Apple's FindMy network!
http://docs.mikealmel.ooo/FindMy.py/
MIT License
59 stars 7 forks source link

Use with device already in the network? #4

Closed YeapGuy closed 2 months ago

YeapGuy commented 5 months ago

Hi, How technically feasible is it to use this project to work with official AirTags or other Find My devices? Already working AirTag clones are being sold for $2-4 a piece on Aliexpress, so I don't see a point in spending a lot of time messing with flashing, firmwares and all of that OpenHaystack stuff, when I can just buy a working "AirTag" for so cheap.

malmeloo commented 5 months ago

The issue with using original (or compatible) accessories is that each accessory has a private master key that is required in order to decrypt its location reports. This key is generated during the pairing session between the accessory and an Apple device when it is first set up.

The nicest solution would be to perform the pairing sequence ourselves in order to obtain the private key. We have pretty good (official) documentation for the bluetooth protocol between the device and accessory, so that won't be a problem. The real issue is that Apple's servers are also involved in the pairing process. As far as I know, nobody has successfully reverse engineered this part of the protocol yet, so we would first need to find out what data is exchanged between the device and Apple. Well-behaving accessories will reject pairing attempts that are not signed by Apple servers.

Another way could be to have an Apple device do the pairing, and figure out where the master key is stored. As long as we have enough details to generate the rotating private keys derived from this master key, we can fetch location reports for the accessory. I don't own any Apple devices however, so I'm not really able to help in this regard.

That said, this is something I'd be interested in to include in this library, so I'll leave this issue open as a feature request.

YeapGuy commented 5 months ago

I successfully retrieved the keys from the macOS Find My application. Screenshot 2024-01-29 at 08 38 41 What's next?

malmeloo commented 5 months ago

Oh, neat! I'm expecting the privateKey value to be the master key from which we can derive the accessory's keys and retrieve its location reports. I don't have much time to replicate the exact algorithm right now, but I'll look into it by the end of the week. Feel free to poke me if I forget :)

Just for future reference, can you share where you found those values? This is information I'd like to include in the documentation if possible.

malmeloo commented 5 months ago

Just as a quick update, I'm currently working on implementing the key derivation algorithm needed for this. It's just taking a little longer because to my knowledge this algorithm isn't standardized anywhere, so I have to roll my own crypto... fingers crossed, haha.

At this point, judging by your screenshot I'm fairly certain that we're going to need the secondarySharedSecret in combination with either the privateKey or the publicKey (they appear to be the same in your pic? the first few bytes at least.) The sharedSecret is probably the one used to derive the "primary key," which is only used in lost mode on the first day until 4 am. I think the "secondary key" is more interesting, but both use the same algorithm, so it's not difficult to implement both.

In the meantime, could you please try to dump a copy of your tag in lost mode using this script? There is a keyroll mechanism involved, so to test the algorithm we're gonna sequentially generate a bunch of keys and compare them against one of the public keys that the tag broadcasts; if it's in the sequence, we know the algorithm works. If you have a rough indication of when you first paired your tag, that will help narrow down the search sequence. The keyroll works in periods of 15 minutes.

YeapGuy commented 5 months ago

Just for future reference, can you share where you found those values? This is information I'd like to include in the documentation if possible.

Sure :) There are .record files in /Library/com.apple.icloud.searchpartyd/OwnedBeacons/ β€’ one for each "owned beacon". These files are actually plists. The data in them is β€’ of course β€’ encrypted. It's using AES GCM. The first value of the plist is the nonce, the second value is the tag and the third value is the data. The key used to encrypt these .record files can be obtained by running security find-generic-password -l 'FindMyAccessories' -g β€’ the "gena" attribute value is the key in hex format. Here's the script I used to decrypt the .record file. It's very rough and you have to substitute the key and the .record file path, but it works.

in combination with either the privateKey or the publicKey (they appear to be the same in your pic? the first few bytes at least.)

The privateKey includes the publicKey at the beginning, but they're not the same β€’ the privateKey is longer.

If you have a rough indication of when you first paired your tag, that will help narrow down the search sequence. The keyroll works in periods of 15 minutes.

Sure, I'll re-pair the tag when I'm at home, so that I know the exact time when it was paired, and then I'll give it a shot.

YeapGuy commented 5 months ago

Okay, I re-paired the tag at 13:28, put it into lost mode at 13:30 (why is this needed btw?) and turned off my experimental iPhone right after that. Then I ran the scanner.

Here's the result from 13:32 (4 minutes after pairing) ``` Device - C8:EC:6B:63:BA:BC Public key: COxrY7q8bBtoDHzbAgL282zjCvC516HZGV0jgg== Lookup key: 0WN0SQ78QJ5F/DVz5Wy0OJgstsebL07Av8iIPLpUDu0= Status byte: 20 Hint byte: c7 Extra data: Adapter : /org/bluez/hci0 Address : C8:EC:6B:63:BA:BC AddressType : random Alias : C8-EC-6B-63-BA-BC Blocked : False Connected : False LegacyPairing : False ManufacturerData : {76: bytearray(b'\x12\x19 l\x1bh\x0c|\xdb\x02\x02\xf6\xf3l\xe3\n\xf0\xb9\xd7\xa1\xd9\x19]#\x82\x00\xc7')} Paired : False RSSI : -49 ServicesResolved : False Trusted : False UUIDs : [] ```
And here's the result from 13:55 (27 minutes after pairing) ``` Device - C8:EC:6B:63:BA:BC Public key: COxrY7q8bBtoDHzbAgL282zjCvC516HZGV0jgg== Lookup key: 0WN0SQ78QJ5F/DVz5Wy0OJgstsebL07Av8iIPLpUDu0= Status byte: 20 Hint byte: 36 Extra data: Adapter : /org/bluez/hci0 Address : C8:EC:6B:63:BA:BC AddressType : random Alias : C8-EC-6B-63-BA-BC Blocked : False Connected : False LegacyPairing : False ManufacturerData : {76: bytearray(b'\x12\x19 l\x1bh\x0c|\xdb\x02\x02\xf6\xf3l\xe3\n\xf0\xb9\xd7\xa1\xd9\x19]#\x82\x006')} Paired : False RSSI : -49 ServicesResolved : False Trusted : False UUIDs : [] ```

Do you need anything else? I'm willing to send you over the stuff I dumped from the macOS Find My app if that helps. πŸ˜„

malmeloo commented 5 months ago

Thanks! With lost mode I actually meant the "separated" state, i.e. the state in which the tag is actually broadcasting its keys and can be found using Find My. Those scans are looking good though! It keeps broadcasting the same key while the hint byte changes, which is what I was expecting.

Yesterday I implemented the actual algorithm, so it should be able to generate the keys now. I did have to change it a bit to generate private keys instead of public ones (because we want to decrypt reports instead of only generate them), so I hope it works... my group theory is a bit rusty. πŸ˜… If I find the time, I'll try to integrate it with the library today and see if I can set up a test script.

If you're comfortable with sharing that data, it would be greatly appreciated! Feel free to send them to git@mikealmel.ooo.

airy10 commented 5 months ago

The key used to encrypt these .record files can be obtained by running security find-generic-password -l 'FindMyAccessories' -g β€’ the "gena" attribute value is the key in hex format.

Which OS are you running ? I don't have that record in my Keychain But I'm running 14.4 beta and Apple did some changes about Localisation data on that system (for example, plists in Library/Caches/com.apple.findmy.fmipcore used to contain plain data but is now encrypted data).

YeapGuy commented 5 months ago

I'm running 13.6.1 on a Hackintosh system

malmeloo commented 5 months ago

Alright, it should be ready! I have added a new example which you can check out if you want; it currently does not actually fetch location reports, just tries to find the private key belonging to the broadcasted public key. That key can be plugged into the fetch_reports.py example though.

Note that the master / secret keys are in binary format, so you might have to convert them first. I think the script should be pretty self-explanatory, but if you get stuck let me know.

YeapGuy commented 5 months ago

The key used to encrypt these .record files can be obtained by running security find-generic-password -l 'FindMyAccessories' -g β€’ the "gena" attribute value is the key in hex format.

Which OS are you running ? I don't have that record in my Keychain But I'm running 14.4 beta and Apple did some changes about Localisation data on that system (for example, plists in Library/Caches/com.apple.findmy.fmipcore used to contain plain data but is now encrypted data).

Heh, funny, the command stopped working for me as well. The keychain item is now called BeaconStore, so the command to obtain the key is now security find-generic-password -l 'BeaconStore' -g. I didn't do any software update β€’ Apple is somehow making changes on the fly. Funny, but annoying. Hopefully they don't do more significant changes.

airy10 commented 5 months ago

Ah right. The 'gena' attribute is null for me but the password works for me

func cmd(_ cmd: String, _ args: String...) -> String {
    let task = Process()
    task.launchPath = cmd
    task.arguments = args
    let pipe = Pipe()
    task.standardOutput = pipe
    task.launch()
    task.waitUntilExit()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    guard let output: String = String(data: data, encoding: .utf8) else { return "" }; 
    return output
}

let hexKey = cmd("/usr/bin/security", "find-generic-password",  "-l",  "BeaconStore",  "-w")
airy10 commented 5 months ago

I've updated your script to automatically get the decode key and decode all of the files from ~/Library/com.apple.icloud.searchpartyd https://gist.github.com/airy10/5205dc851fbd0715fcd7a5cdde25e7c8

malmeloo commented 5 months ago

It works! I was successfully able to retrieve the private key belonging to the scan @YeapGuy posted here before πŸŽ‰ The one you emailed me earlier today does not work however; I'm not entirely sure why, but I probably messed something up for the secondary key generation. After the first 4:00 am it switches from primary to secondary. If you could post the full results of a scan again that'd be great.

Right now this means that using the values that you extracted from the beaconstore, we're able to generate all keys that the accessory is using and use them to fetch and decrypt their location reports. There are still a few issues though:

  1. Secondary key generation appears to be broken, so reports can only be fetched until 4 am of when the accessory was "lost" (i.e. disconnected)
  2. I don't know how to find the number of iterations to do in order to get the correct key for a certain time period. The key you posted was found at period 2, which is a bit weird given that it starts at 1 and the periods should take 15 minutes. I also don't know what happens when an accessory is "dead" for a few days; I assume the period continues where it left off, which means we cannot just do a rough (time in minutes / 15) to get the correct time. This probably just requires some experimentation.
  3. I'm wondering whether it's possible to fetch the required accessory data straight from apple. I'll ask around a bit for this.

The library and example script have been updated; make sure to use the public key (so not the "lookup key") to make it work. It can now also read all the necessary details from the .plist file, so you don't need to fill out the keys manually anymore.

malmeloo commented 5 months ago

Nevermind, secondary key generation was actually working perfectly; I just made a silly mistake where it compared against the primary key twice. 🫠 Should be fixed now.

YeapGuy commented 5 months ago

Awesome :) It works for me with the first broadcasted key. But with the current public key, I get no match found.

Results of a scan now ``` Device - FC:1D:C1:E1:2A:25 Public key: /B3B4SolC42UdG4o73EbycccsWSWY+9+Ty7zJQ== Lookup key: P1FJz8xpLGo9gusrbZSt93kyaGMAywRzyfymseGd+iE= Status byte: 20 Hint byte: f6 Extra data: Adapter : /org/bluez/hci0 Address : FC:1D:C1:E1:2A:25 AddressType : random Alias : FC-1D-C1-E1-2A-25 Blocked : False Connected : False LegacyPairing : False ManufacturerData : {76: bytearray(b'\x12\x19 \x0b\x8d\x94tn(\xefq\x1b\xc9\xc7\x1c\xb1d\x96c\xef~O.\xf3%\x03\xf6')} Paired : False RSSI : -60 ServicesResolved : False Trusted : False UUIDs : [] ```
malmeloo commented 5 months ago

It works fine for me, but the key was found at index 192 / 200. That's 8 * 15 min = 2 hours ago, which also just happens to be exactly 2 days + 2 hours after you first paired the tag. The reason for this is that the library currently just generates the secondary key at number of time slots / 96 + 1, but this is wrong; the first update is at 4:00 AM, which is most likely less than 96 time slots (= 1 day) after it was paired, therefore the calculations don't line up.

I have just pushed an update that specifically generates keys for a certain timestamp. I'm uncertain whether the 4 AM rule is in UTC or the local timezone, but it should now be much more reliable.

YeapGuy commented 5 months ago

Yeah, it works with yesterday's key now. I'm not sure why it wasn't found before, yet it worked for you later... I reread your reply and I get it now. :smile:

Today's key worked immediately.

The scan from today ``` Device - E1:0F:4D:EC:F9:70 Public key: oQ9N7Plwe+SIKnt5+k8GApo53nJzUcm8wAL+pA== Lookup key: LG1q5ytNC9TmxBWHHX3mEfYm6LMLEjSxLZSQcxTJpb4= Status byte: 20 Hint byte: e Extra data: Adapter : /org/bluez/hci0 Address : E1:0F:4D:EC:F9:70 AddressType : random Alias : E1-0F-4D-EC-F9-70 Blocked : False Connected : False LegacyPairing : False ManufacturerData : {76: bytearray(b'\x12\x19 {\xe4\x88*{y\xfaO\x06\x02\x9a9\xdersQ\xc9\xbc\xc0\x02\xfe\xa4\x02\x0e')} Paired : False RSSI : -46 ServicesResolved : False Trusted : False UUIDs : [] ```
And the result of real_airtag.py ``` KEY FOUND!! KEEP THE BELOW KEY SECRET! IT CAN BE USED TO RETRIEVE THE DEVICE'S LOCATION! - Key: - Approx. Time: 2024-02-08 03:00:00+00:00 - Type: KeyType.SECONDARY ```
hajekj commented 4 months ago

I would like to share some of my work based on this library, which we used in the past week to attempt to recover a stolen MacBook. Unfortunately, it ran out of battery, before we put it all together and understood how it works. So we were unsuccessful, but I think there's quite a big potential with it, and could help others a lot: https://github.com/hajekj/OfflineFindRecovery

I plan to work on automating most of the steps, and making it into an application, so it can be used from phone or any device more easily.

biemster commented 4 months ago

The FindMy networks for AirTags (like this project) and for MacBooks / iPhones are two separate things if I quote the original seemoo research correctly. @hajekj are you querying Apple's servers with this to find your MacBook, or are you searching for the BLE beacon?

hajekj commented 4 months ago

I am doing both. Querying Apple service for historical movement data, and searching for BLE to find the precise location - like specific hotel room. It is the same thing in my opinion, just handles the key generation differently:

biemster commented 4 months ago

I did not know that! I was under the impression that only airtags could be queried with the FindMy projects. It would be very handy to have a small script that dumps the private key from the login.keychain-db, just for safekeeping. Putting this is on disk and not in the Secure Enclave seems like quite an oversight on Apple's side to me.

hajekj commented 4 months ago

Look into the repo there is a script to decrypt the keys of all the beacons you have - AirPods, MacBook, iPhone, AirTag etc.

airy10 commented 4 months ago

https://gist.github.com/airy10/5205dc851fbd0715fcd7a5cdde25e7c8 will decrypt automlcally all the beacon files at once

Then copy the file from the OwnedBeacons directory for the device you want to "decrypted.plist" and you then can use the scripts from @hajekj to generate the keys using "findmy-keygeneration.py", then find the last known locations if you have an anisette server using "findmy-historicallocations.py"

Note the the script "findmy-keygeneration.py" needs some change for the AirTag (at least with my iTag chinese devices...) as the secondary key is from a different field :

secondary = device_data.get("secondarySharedSecret") or device_data.get("secureLocationsSharedSecret")
SKS = secondary["key"]["data"]

And the test if key.key_type == KeyType.PRIMARY should be removed (and you might want to change the script too to generate only keys for the last month if you have some very old device)

With that, I could retrieve the history locations from my iTags

hajekj commented 4 months ago

I tried using that script, but couldn't get it to work @airy10 - I will try again and comment on the Gist, if I manage to isolate the issue, but I guess it could be MacOS version related.

airy10 commented 4 months ago

It might be. It's fine here both from the command line or from a playground Note that the first time you run it, you get some auth window about accessing the keychain to get the beacon password - so you might get an exception if you run it from some headless/ssh session

samtombson commented 3 months ago

I'm not clearly understand which _PUBLICKEY should be used in _realairtag.py, if the only one I have is already contained in decrypted.plist, but the result there is no match found I can't use _devicescanner.py because the tag is far away

malmeloo commented 3 months ago

I'm not clearly understand which _PUBLICKEY should be used in _realairtag.py, if the only one I have is already contained in decrypted.plist, but the result there is no match found I can't use _devicescanner.py because the tag is far away

The example just generates keys until it's found a matching public key to test that the algorithm works, but that's not necessary for your use case. If you want to fetch location reports, you will need all the keys it generates for a specific time frame. That is, each key in the for key in keys: loop is a potential key, and should be checked as demonstrated in fetch_reports.py.

Note that it will probably generate way too many keys, as it generates all previous keys instead of only the ones for the past 7 days (which is what I believe the history limit is for fetching location reports).

I will see if I can update the example soon, it's not very useful in its current form anyway.

wes1993 commented 2 months ago

hello @malmeloo and @YeapGuy, There are some news? I'm trying to track my original AirTag but don't understand how.. could you please give me more info?

Thanks a lot Stefano

malmeloo commented 2 months ago

Right now it's a bit convoluted, but essentially:

  1. Get decrypted plist files from a Mac using this program
  2. Get keys for a given time range from a given plist file. You can look at real_airtag.py as an example.
  3. Use the keys to fetch reports. Take a look at fetch_reports.py to see how to do this.

I will pick development in this repository back up soon and come up with a better example program, which would merge steps 2 and 3.

wes1993 commented 2 months ago

Really thanks a lot @malmeloo I'll try this steps now :-D Just one question, i need to do the step 1 just one time is correct? (My object is to run this inside a Ubuntu VM after i have retrieved the decrypted plist)

Regards Stefano

malmeloo commented 2 months ago

Yes, one time should be enough until the tag is re-paired / reset. Good luck! :-)

wes1993 commented 2 months ago

@malmeloo, When i run the script from @airy10 i have many incorrect key size errors... some suggestion?

Best Regards Stefano

malmeloo commented 2 months ago

I haven't actually run it myself, so I'm not sure, sorry. If you scroll up you will also find this version by YeapGuy, maybe that one works for you?

wes1993 commented 2 months ago

I have seen that running this: "security find-generic-password -l 'BeaconStore' -w" won't spool anything.. i think the problem could be here..

malmeloo commented 2 months ago

As someone who is both physically and virtually mac-less, I don't think I'll be able to help... But maybe someone else in this thread might chime in with a solution.

wes1993 commented 2 months ago

Thanks i'll try to investigate further :-D

wes1993 commented 2 months ago

Hello @malmeloo, Finally I have successfully decryped the key using the script from @YeapGuy, i don't know why but the one from @airy10 won't work for me...

Now when i run your script real_airtag.py i don't know what i should put in the public key (I don't have a blutooth enabled device right now)...

If i run without variable i have "No match found! :("

Could i take this key from plist or decrypted record file?

Best regards Stefano

malmeloo commented 2 months ago

The real_airtag.py script is currently still a POC from back when we were still testing its implementation, if you look at the script itself you will see that it is only used to check whether the keygen works correctly. The important aspect is the airtag.keys_at function call, which calculates the private keys for a specific datetime. You'll have to modify the script a bit to simply spit out all keys over the past week.

I'll likely have time to work on this again next week, so I'll update the script soon to do exactly that. What it does right now is not very useful anymore.

airy10 commented 2 months ago

No idea why my script isn't working for you. It's still fine here, on my machine I guess the way the password is stored in the keychain ? (it might depend of your OS version, or the device/version used when the first time Localized was used, or whatever Apple thinks is a good idea...)

Le 16 avr. 2024 Γ  21:31, wes1993 @.***> a Γ©crit :

Hello @malmeloo, Finally I have successfully decryped the key using the script from @YeapGuy, i don't know why but the one from @airy10 won't work for me... Now when i run your script real_airtag.py i don't know what i should put in the public key (I don't have a blutooth enabled device right now)... Could i take this key from plist or decrypted record file? Best regards Stefano

wes1993 commented 2 months ago

Hello @malmeloo and @airy10, Regarding your script @airy10, when i try to run the script i have this error:

swift airtag-decryptor-airy.swift
Error: incorrectKeySize
com.apple.icloud.searchpartyd/SecureLocationInfo/58A33394-9F73-4426-B821-9B0E8FA1E439.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/971035A9-9877-448D-8E02-E6D3AEAAFA59.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/7C86CAC4-3999-4EB0-AF06-C26581924292.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/A113D728-73F8-4E09-9E1D-5EE114557CB3.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/0DC1DC70-E467-47BA-B6A6-7A57D12040D6.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/2023D54B-0F7A-44B7-B582-7E2C1A99EA1F.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/3D948889-1A7E-433B-A771-6255D011B46C.record
Error: incorrectKeySize
com.apple.icloud.searchpartyd/OwnedBeacons/0BC0B39C-5408-4F5F-AD5C-7A82D21BAF6A.record
Error: invalidPlistFormat
com.apple.icloud.searchpartyd/ShareAttemptTracker/shareAttempts.plist
Error: invalidPlistFormat
com.apple.icloud.searchpartyd/BeaconObservationStore/observations.plist
Error: incorrectKeySize
com.apple.icloud.searchpartyd/BeaconNamingRecord/0DC1DC70-E467-47BA-B6A6-7A57D12040D6/BC28DDCA-A250-4E07-8165-641AB9C3213A.record

Details about my OS:

Ventura 13.3.1
Apple Swift version 5.8.1 (swiftlang-5.8.0.124.5 clang-1403.0.22.11.100)

@malmeloo, After trying I finally generated the keys using the script findmy-keygeneration.py and then the location history using findmy-historicallocations.py but i have some questions:

  1. I have seen that only some of the primary keys generated can give me the location is this correct? (898 primary keys generated 122 locations found)
  2. The date near the generated key not match the date of location discovered is normal?
  3. I can retrieve only some of the locations no all the location this could be related to the primary key generated with the other script?
  4. Is there a way to "trigger" the location of the airtag when i run the script instead of accessing only the historical data?

Thanks again a lot Stefano

P.S. @malmeloo if you need some help i could help you, i'm not expert but i can help :-D

malmeloo commented 2 months ago

@wes1993

  1. Yes, this in fact very likely to happen. Each key is simply a "potential" key that the AirTag might have used, and whether it was actually used at that specific timestamp depends on its status (separated or not) and whether it was recently rebooted or not. That's why for each timestamp there are up to 3 different potential keys, although there is quite some overlap here across different timestamps. Also, one key might have been seen by multiple iPhones, so it might generate multiple reports.
  2. This depends on which date you mean: there is the "key derivation date," which is the timestamp belonging to the generated key, and there are the "found at" and "uploaded at" timestamps. If you want accurate measurements you should only use these last two timestamps as they are directly retrieved from Apple; the first one might be inaccurate due to the multiple timestamp problem I mentioned above. If it is off by more than 24 hours though please do let me know.
  3. Can you elaborate on this? For example, are reports before or after a specific date missing or in between as well? Note that Apple only stores location reports for up to 7 days I believe.
  4. What do you mean by "trigger"? There is no way to force the AirTag to report a location if that's what you mean, as the entire system is passive in the sense that other devices need to find your tag. The network itself is built on historical data only, though iDevices can show your tag as "nearby" if they're able to connect to it.

And help is always appreciated! If you have any suggestions or want to contribute feel free to open an issue/PR.

wes1993 commented 2 months ago

@malmeloo Thanks for help me :-D

  1. Based on my experience and test only PrimaryKey work.
  2. I mean differences between "key derivation date" and "found at"/"uploaded at", the biggest interval it's about 10 hours.
  3. Sure, yesterday at 19:50 I have opened the FindMy app on my iPhone and seen that AirTag Location was updated (Under the name location is updated Now) but if i execute the findmy-history python script this location is not finded...
  4. I mean, If for example I open the FindMy app, under Items i can see updating location and after some seconds Location updated now and the actual position of the Tag, my question at this point is: AirTag send the position everytime they find an Apple device or this is triggered only if I open the FindMy app from my own devices?

Best Regards Stefano

malmeloo commented 2 months ago
  1. This is completely normal if your tag hasn't been separated for long enough or if it hasn't been rebooted recently, as it will only use the secondary key under those circumstances.
  2. That's completely normal as well; the same key may in certain circumstances be used for up to 24 hours before it is rotated. For this reason the key derivation timestamp actually depends on the script using this library, as there will be multiple valid timestamps for a certain key.
  3. Could you try again in a few hours and see if it works then? This might be a timezone-related issue.
  4. The AirTag will never share its own location; it is always dependent on other devices to find it and report their own locations. What I suspect Apple is doing there is they're simply showing "Updating location" while the iPhone is downloading the location reports. Either that, or one of your devices is nearby and recognizes the tag.

Could you indicate which timezone you are in? That could explain the issue for point 3, as well as increase the deviation you're seeing for point 2.

Also, I just noticed that the key generator is missing potential "primary latch-on" keys; and while this won't affect your use case, it is something that should probably be fixed.

wes1993 commented 2 months ago
  1. Thanks now is more clear :-D
  2. Today i have seen that i could access the AirTag locations of yesterday but

My Timezone is UTC/GMT +2 hours, I'm in Italy (Rome)

Could you please explain better what do you mean with "primary latch-on" keys and how can i fix?

Thanks a lot Stefano

malmeloo commented 2 months ago

Thanks for the info! I'll need to dive a bit deeper to see if there's actually an error in the library here; it could be that the algorithm is using UTC where it should be using local time or the other way around, which might cause some delays. I'll take a look next week.

With the primary latch-on key I meant the behavior of the tag where it will stop rotating its broadcasted primary key once it is separated from its owner until 4 am. Currently if you tell the library 'hey, generate all potential keys at xx:xx" it won't include all primary keys from the past 24 hours, but technically it is possible for one of those primary keys to have "latched on" until that timestamp. But this is irrelevant in your case since you're simply generating all keys for the past 7 days. Just pretend you didn't see that part of my message, it's a technicality for a very specific use case :-)

wes1993 commented 2 months ago

@malmeloo, Thanks a lot for your reply, i'll do some more tests in the next days and I'll write here what discover :-D

Another thing is that the script generates all the keys from the pairing days until today and next 48 hours so I should have all the keys independently from TZ but won't retrieve the locations..

Bye Stefano

wes1993 commented 2 months ago

@malmeloo, I have seen in the WE that for my case i need to add 2hours to the timestamp reported.

If you need more details ask me :-D

malmeloo commented 2 months ago

Ah sorry, I missed the edit to your previous message. Can you share which script you are using?

As for that second issue, which reported timestamp are you referring to? The ones directly reported by this library (so the found-at and uploaded-at timestamps) are timezone-aware but default to UTC, which would explain the 2-hour difference. I'll fix this to default to the local timezone, but for now it can be fixed by calling .astimezone() on the datetime objects.

wes1993 commented 2 months ago

Here are my scripts:

HistoricalLocations

import asyncio
import json
import logging
from pathlib import Path
import os
import csv
from datetime import datetime, timedelta, timezone
import json

from findmy import KeyPair
from findmy.reports import (
    AsyncAppleAccount,
    LoginState,
    RemoteAnisetteProvider,
    SmsSecondFactorMethod,
    TrustedDeviceSecondFactorMethod
)

# URL to (public or local) anisette server
ANISETTE_SERVER = "http://192.168.0.123:6969"

# Apple account details
ACCOUNT_EMAIL = ""
ACCOUNT_PASS = ""

logging.basicConfig(level=logging.DEBUG)

async def login(account: AsyncAppleAccount) -> None:
    state = await account.login(ACCOUNT_EMAIL, ACCOUNT_PASS)

    if state == LoginState.REQUIRE_2FA:  # Account requires 2FA
        # This only supports SMS methods for now
        methods = await account.get_2fa_methods()

        # Print the (masked) phone numbers
        for i, method in enumerate(methods):
            if isinstance(method, TrustedDeviceSecondFactorMethod):
                print(f"{i} - Trusted Device")
            elif isinstance(method, SmsSecondFactorMethod):
                print(f"{i} - SMS ({method.phone_number})")

        ind = int(input("Method? > "))

        method = methods[ind]
        await method.request()
        code = input("Code? > ")

        # This automatically finishes the post-2FA login flow
        await method.submit(code)

# Define a custom function to serialize datetime objects 
def serialize_datetime(obj): 
    if isinstance(obj, datetime): 
        return obj.isoformat() 
    raise TypeError("Type not serializable") 

async def fetch_reports(keys: list[KeyPair]) -> None:
    anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
    acc = AsyncAppleAccount(anisette)

    try:
        acc_store = Path("account.json")
        try:
            with acc_store.open() as f:
                acc.restore(json.load(f))
        except FileNotFoundError:
            await login(acc)
            with acc_store.open("w+") as f:
                json.dump(acc.export(), f)

        print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")

        # It's that simple!
        #print(keys)
        #print("\n")
        #print("AAAAAAAAAAAAAAA")
        reports = await acc.fetch_last_reports(keys)
        #print(reports)
        dump_list = []
        for keypair in reports:
            report = reports[keypair]
            for r in report:
                print("Pub_AT:", (r.published_at+timedelta(hours=2)), "----- TimeStamp:", (r.timestamp+ timedelta(hours=2)), "-----", r.latitude, r.longitude,"-----", r.key.private_key_b64)
                obj = {
                    "pub_at": (r.published_at+timedelta(hours=2)),
                    "time": (r.timestamp+ timedelta(hours=2)),
                    "lat": r.latitude,
                    "lon": r.longitude,
                    "published_at": r.published_at,
                    "description": r.description,
                    "confidence": r.confidence,
                    "status": r.status,
                    "key": r.key.private_key_b64
                }
                dump_list.append(obj)

                dbstring = str((r.published_at+timedelta(hours=2))) + ";" + str((r.timestamp+ timedelta(hours=2))) + ";" + str(r.latitude) + ";" + str(r.longitude) + ";" + str(r.key.private_key_b64) + "\n"
                with open('historylocations.txt', 'a', encoding="utf-8") as f:
                    f.write(str(dbstring))

        json_object = json.dumps(dump_list, indent=4, default=serialize_datetime)

        with open("location_history.json", "w") as outfile:
            outfile.write(json_object)

    finally:
        await acc.close()

if __name__ == "__main__":
    file = open('discovery-keys.csv', "r")
    csvreader = csv.reader(file, delimiter=";", quotechar='"', quoting=csv.QUOTE_ALL, lineterminator='\n')
    private_keys = []
    for row in csvreader:
        #print(row[0])
        private_keys.append(KeyPair.from_b64(row[2]))

    file.close()
    #print(private_keys)
    #FIX FOR WINDOWS COMMENT ON OTHER PLATFORM
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    #END FIX FOR WINDOWS COMMENT ON OTHER PLATFORM
    asyncio.run(fetch_reports(private_keys))

FindKeys

"""
Example showing how to retrieve the primary key of your own AirTag, or any other FindMy-accessory.

This key can be used to retrieve the device's location for a single day.
"""
import plistlib
from datetime import datetime, timedelta, timezone
from pathlib import Path
#from csv import CSVWriter
import csv
from findmy.keys import KeyType

from findmy import FindMyAccessory

# Path to a .plist dumped from the Find My app.
PLIST_PATH = Path("airtag.plist")

# == The variables below are auto-filled from the plist!! ==

with PLIST_PATH.open("rb") as f:
    device_data = plistlib.load(f)

# PRIVATE master key. 28 (?) bytes.
MASTER_KEY = device_data["privateKey"]["key"]["data"][-28:]

# "Primary" shared secret. 32 bytes.
SKN = device_data["sharedSecret"]["key"]["data"]

# "Secondary" shared secret. 32 bytes.
# This doesn't apply in case of MacBook, but is used for AirTags and other accessories.
secondary = device_data.get("secondarySharedSecret") or device_data.get("secureLocationsSharedSecret")
SKS = secondary["key"]["data"]
#SKS = device_data["secureLocationsSharedSecret"]["key"]["data"]

def main() -> None:
    paired_at = device_data["pairingDate"].replace(tzinfo=timezone.utc)

    airtag = FindMyAccessory(MASTER_KEY, SKN, SKS, paired_at)

    # Generate keys for 2 days ahead
    now = datetime.now(tz=timezone.utc) + timedelta(hours=96)

    print()
    lookup_time = paired_at.replace(
        minute=paired_at.minute // 15 * 15,
        second=0,
        microsecond=0,
    ) + timedelta(minutes=15)

    #mycsv = CSVWriter('discovery-keys.csv')
    #mycsv = csv.writer("discovery-keys.csv")

#    while lookup_time < now:
#        keys = airtag.keys_at(lookup_time)
#        for key in keys:
#            if key.key_type == KeyType.PRIMARY:
#                print(lookup_time, key.adv_key_b64, key.private_key_b64, key.key_type, key.hashed_adv_key_b64)
#                #mycsv.write(lookup_time, key.adv_key_b64, key.private_key_b64, key.key_type, key.hashed_adv_key_b64)
#    if (timedelta((datetime.now(tz=timezone.utc) - timedelta(days=7) - lookup_time )) < 7):
#        print("Meno 7")
#    else:
#        print("Piu 7")

#    print(datetime.now(tz=timezone.utc) - timedelta(days=7))
#    print (lookup_time(tz=timezone.utc))
    #lookup_time = datetime.now(tz=timezone.utc) - timedelta(days=7)
    while lookup_time < now:
        keys = airtag.keys_at(lookup_time)
        for key in keys:
            #print(key.key_type)
            #if (str(key.key_type) == "KeyType.SECONDARY"):
            #if (str(key.key_type) == "KeyType.PRIMARY"):
            if True:
                with open("discovery-keys.csv", 'a') as csvfile:
                    csvwriter = csv.writer(csvfile, delimiter=";", lineterminator='\n')
                    csvwriter.writerow([lookup_time, key.adv_key_b64, key.private_key_b64, key.key_type, key.hashed_adv_key_b64])
            #print(lookup_time, key.adv_key_b64, key.private_key_b64, key.key_type, key.hashed_adv_key_b64)
            #mycsv.write(lookup_time, key.adv_key_b64, key.private_key_b64, key.key_type, key.hashed_adv_key_b64)

        lookup_time += timedelta(minutes=15)

    print("All keys for specified time period generated")
if __name__ == "__main__":
    open('discovery-keys.csv', 'w').close()
    main()

Best Regards Stefano