From a6ff011ad0f11ed29430fea8b4253aeeecdf0196 Mon Sep 17 00:00:00 2001 From: Luca Olivetti Date: Thu, 8 Sep 2022 08:55:09 +0200 Subject: [PATCH 1/2] command line parameters, filter and sort reports by timestamp, select keyfiles by prefix Since apple's servers seem to send all the reports regardless of startDate and endDate, I'm only showing the results after the timestamp specified on the commandline. Using the prefix option (to get all reports from the same tag that's using more than one key) it will add a "key" fields to each report with the part of the filename after the prefix. Another minor modification is the key "isodatetime" (easier to read for an human) and "goog" with an url to see the location on google maps. --- request_reports.py | 83 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/request_reports.py b/request_reports.py index d5d294c..ce9b325 100755 --- a/request_reports.py +++ b/request_reports.py @@ -9,7 +9,8 @@ import objc from Foundation import NSBundle, NSClassFromString, NSData, NSPropertyListSerialization from p224 import scalar_mult,curve - +from datetime import datetime +import argparse def bytes_to_int(b): return int(codecs.encode(b, 'hex'), 16) @@ -33,7 +34,8 @@ def decode_tag(data): latitude = struct.unpack(">i", data[0:4])[0] / 10000000.0 longitude = struct.unpack(">i", data[4:8])[0] / 10000000.0 confidence = bytes_to_int(data[8:9]) - return {'lat': latitude, 'lon': longitude, 'conf': confidence} + status = bytes_to_int(data[9:10]) + return {'lat': latitude, 'lon': longitude, 'conf': confidence, 'status':status} def getAppleDSIDandSearchPartyToken(iCloudKey): # copied from https://github.com/Hsn723/MMeTokenDecrypt @@ -62,8 +64,16 @@ def getCurrentTimes(): if __name__ == "__main__": - from getpass import getpass - iCloud_decryptionkey = getpass("Enter your iCloud decryption key ($ security find-generic-password -ws 'iCloud'):") + parser = argparse.ArgumentParser() + parser.add_argument('-H', '--hours', help='only show reports not older than these hours', type=int, default=24) + parser.add_argument('-p', '--prefix', help='only use keyfiles starting with this prefix', default='') + parser.add_argument('-k', '--key', help='iCloud decryption key') + args=parser.parse_args() + if args.key is None: + from getpass import getpass + iCloud_decryptionkey = getpass("Enter your iCloud decryption key ($ security find-generic-password -ws 'iCloud'):") + else: + iCloud_decryptionkey = args.key AppleDSID,searchPartyToken = getAppleDSIDandSearchPartyToken(iCloud_decryptionkey) machineID, oneTimePassword = getOTPHeaders() UTCTime, Timezone, unixEpoch = getCurrentTimes() @@ -81,11 +91,14 @@ def getCurrentTimes(): } ids = {} - for keyfile in glob.glob('./*keys'): + names = {} + found = {} + for keyfile in glob.glob(args.prefix+'*.keys'): # read key files generated with generate_keys.py with open(keyfile) as f: hashed_adv = '' priv = '' + name = keyfile[:-5] for line in f: key = line.rstrip('\n').split(': ') if key[0] == 'Private key': @@ -95,10 +108,17 @@ def getCurrentTimes(): if priv and hashed_adv: ids[hashed_adv] = priv + names[hashed_adv] = name + subkey=name[len(args.prefix):] + found[subkey] = False else: print "Couldn't find key pair in " + keyfile - data = '{"search": [{"endDate": %d, "startDate": %d, "ids": %s}]}' % (unixEpoch *1000, (unixEpoch *1000) -(86400000 *7), ids.keys()) + print "keys ", len(ids) + enddate = unixEpoch + startdate = enddate - 60 * 60 * args.hours + coco = 978307200 + data = '{"search": [{"endDate": %d, "startDate": %d, "ids": %s}]}' % ((enddate-coco) *1000000, (startdate-coco)*1000000, ids.keys()) # send out the whole thing import httplib, urllib @@ -109,21 +129,48 @@ def getCurrentTimes(): res = json.loads(response.read())['results'] print '%d reports received.' % len(res) + ordered = [] for report in res: priv = bytes_to_int(base64.b64decode(ids[report['id']])) data = base64.b64decode(report['payload']) # the following is all copied from https://github.com/hatomist/openhaystack-python, thanks @hatomist! timestamp = bytes_to_int(data[0:4]) - eph_key = (bytes_to_int(data[6:34]), bytes_to_int(data[34:62])) - shared_key = scalar_mult(priv, eph_key) - symmetric_key = sha256(int_to_bytes(shared_key[0], 28) + int_to_bytes(1, 4) + data[5:62]) - decryption_key = symmetric_key[:16] - iv = symmetric_key[16:] - enc_data = data[62:72] - tag = data[72:] - - decrypted = decrypt_AES(enc_data, decryption_key, modes.GCM(iv, tag)) - res = decode_tag(decrypted) - res['timestamp'] = timestamp + 978307200 - print report['id'] + ': ' + str(res) + if timestamp + 978307200 >= startdate: + eph_key = (bytes_to_int(data[6:34]), bytes_to_int(data[34:62])) + shared_key = scalar_mult(priv, eph_key) + symmetric_key = sha256(int_to_bytes(shared_key[0], 28) + int_to_bytes(1, 4) + data[5:62]) + decryption_key = symmetric_key[:16] + iv = symmetric_key[16:] + enc_data = data[62:72] + tag = data[72:] + + decrypted = decrypt_AES(enc_data, decryption_key, modes.GCM(iv, tag)) + res = decode_tag(decrypted) + res['timestamp'] = timestamp + 978307200 + res['isodatetime'] = datetime.fromtimestamp(res['timestamp']).isoformat() + name=names[report['id']] + if args.prefix: + res['key'] = name[len(args.prefix)] + found[res['key']]=True + res['goog'] = 'https://maps.google.com/maps?q='+str(res['lat'])+','+str(res['lon']) + #print str(res) + ordered.append(res) + print '%d reports used.' % len(ordered) + ordered.sort(key=lambda item: item.get('timestamp')) + for x in ordered: + print x + +if args.prefix: + f = [] + m = [] + for x in found: + if found[x]: + f.append(x) + else: + m.append(x) + + f.sort(key=lambda x: (len(x),x)) + m.sort(key=lambda x: (len(x),x)) + print "missing ", len(m),': ', ', '.join(m) + print "found ", len(f),': ',', '.join(f) From 682269a39f78020c8daacdfdf8fa1f2ba40b3e4b Mon Sep 17 00:00:00 2001 From: Luca Olivetti Date: Thu, 8 Sep 2022 09:19:06 +0200 Subject: [PATCH 2/2] Added several options to generate_keys.py - command line argument for the number of keys - name of the yaml file containing all generated keys - prefix to use for the filenames of the generated keys --- generate_keys.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/generate_keys.py b/generate_keys.py index 1a0003b..9129961 100755 --- a/generate_keys.py +++ b/generate_keys.py @@ -1,6 +1,7 @@ #!/usr/bin/env python2 import sys,base64,hashlib,random from p224 import scalar_mult,curve +import argparse def int_to_bytes(n, length, endianess='big'): h = '%x' % n @@ -13,11 +14,20 @@ def sha256(data): return digest.digest() -nkeys = 1 -if len(sys.argv) == 2 and sys.argv[1].isdigit(): - nkeys = int(sys.argv[1]) -for i in range(nkeys): +parser = argparse.ArgumentParser() +parser.add_argument('-k','--keys', help='number of keys to generate', type=int, default=1) +parser.add_argument('-n','--name', help='name (prefix) of the keyfiles') +parser.add_argument('-y','--yaml', help='yaml file where to write the list of generated keys') +parser.add_argument('-v','--verbose', help='print keys as they are generated', action="store_true") +args=parser.parse_args() + +if args.yaml is not None: + yaml=open(args.yaml+'.yaml','w') + yaml.write(' keys:\n') + +i=1 +while i<=args.keys: priv = random.getrandbits(224) adv,_ = scalar_mult(priv, curve.g) @@ -28,15 +38,22 @@ def sha256(data): adv_b64 = base64.b64encode(adv_bytes).decode("ascii") s256_b64 = base64.b64encode(sha256(adv_bytes)).decode("ascii") - print('%d)' % (i+1)) - print('Private key: %s' % priv_b64) - print('Advertisement key: %s' % adv_b64) - print('Hashed adv key: %s' % s256_b64) + if args.verbose: + print('%d)' % (i)) + print('Private key: %s' % priv_b64) + print('Advertisement key: %s' % adv_b64) + print('Hashed adv key: %s' % s256_b64) if '/' in s256_b64[:7]: print('no key file written, there was a / in the b64 of the hashed pubkey :(') else: - with open('%s.keys' % s256_b64[:7], 'w') as f: + if args.name: + fname = '%s_%d.keys' % (args.name, i) + else: + fname = '%s.keys' % s256_b64[:7] + with open(fname, 'w') as f: f.write('Private key: %s\n' % priv_b64) f.write('Advertisement key: %s\n' % adv_b64) f.write('Hashed adv key: %s\n' % s256_b64) - + i = i +1 + if args.yaml is not None: + yaml.write(' - "%s"\n' % adv_b64)