diff --git a/examples/real_airtag.py b/examples/real_airtag.py index 0b7b7e0..9aff41a 100644 --- a/examples/real_airtag.py +++ b/examples/real_airtag.py @@ -33,13 +33,7 @@ logging.basicConfig(level=logging.INFO) def main(plist_path: Path, alignment_plist_path: Path | None) -> int: # Step 0: create an accessory key generator - with plist_path.open("rb") as f: - f2 = alignment_plist_path.open("rb") if alignment_plist_path else None - - airtag = FindMyAccessory.from_plist(f, f2) - - if f2: - f2.close() + airtag = FindMyAccessory.from_plist(plist_path, alignment_plist_path) # Step 1: log into an Apple account print("Logging into account") diff --git a/findmy/accessory.py b/findmy/accessory.py index daf1443..40c1b25 100644 --- a/findmy/accessory.py +++ b/findmy/accessory.py @@ -7,22 +7,21 @@ Accessories could be anything ranging from AirTags to iPhones. from __future__ import annotations import logging -import plistlib from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import IO, TYPE_CHECKING, Literal, TypedDict, overload +from typing import TYPE_CHECKING, Literal, TypedDict, overload from typing_extensions import override from findmy.util.abc import Serializable -from findmy.util.files import read_data_json, save_and_return_json +from findmy.util.files import read_data_json, read_data_plist, save_and_return_json from .keys import KeyGenerator, KeyPair, KeyType from .util import crypto if TYPE_CHECKING: from collections.abc import Generator + from pathlib import Path logger = logging.getLogger(__name__) @@ -224,21 +223,13 @@ class FindMyAccessory(RollingKeyPairSource, Serializable[FindMyAccessoryMapping] @classmethod def from_plist( cls, - plist: str | Path | dict | bytes | IO[bytes], - key_alignment_plist: IO[bytes] | None = None, + plist: str | Path | dict | bytes, + key_alignment_plist: str | Path | dict | bytes | None = None, *, name: str | None = None, ) -> FindMyAccessory: """Create a FindMyAccessory from a .plist file dumped from the FindMy app.""" - if isinstance(plist, bytes): - # plist is a bytes object - device_data = plistlib.loads(plist) - elif isinstance(plist, (str, Path)): - device_data = plistlib.loads(Path(plist).read_bytes()) - elif isinstance(plist, IO): - device_data = plistlib.load(plist) - else: - device_data = plist + device_data = read_data_plist(plist) # PRIVATE master key. 28 (?) bytes. master_key = device_data["privateKey"]["key"]["data"][-28:] @@ -263,11 +254,13 @@ class FindMyAccessory(RollingKeyPairSource, Serializable[FindMyAccessoryMapping] alignment_date = None index = None if key_alignment_plist: - alignment_data = plistlib.load(key_alignment_plist) + alignment_data = read_data_plist(key_alignment_plist) + # last observed date alignment_date = alignment_data["lastIndexObservationDate"].replace( tzinfo=timezone.utc, ) + # primary index value at last observed date index = alignment_data["lastIndexObserved"] return cls( diff --git a/findmy/util/files.py b/findmy/util/files.py index 1686bbf..a366f5a 100644 --- a/findmy/util/files.py +++ b/findmy/util/files.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import plistlib from collections.abc import Mapping from pathlib import Path from typing import TypeVar, cast @@ -32,3 +33,30 @@ def read_data_json(val: str | Path | _T) -> _T: val = cast("_T", json.loads(val.read_text())) return val + + +def save_and_return_plist(data: _T, dst: str | Path | None) -> _T: + """Save and return a Plist file.""" + if dst is None: + return data + + if isinstance(dst, str): + dst = Path(dst) + + dst.write_bytes(plistlib.dumps(data)) + + return data + + +def read_data_plist(val: str | Path | _T | bytes) -> _T: + """Read Plist data from a file if a path is passed, or return the argument itself.""" + if isinstance(val, str): + val = Path(val) + + if isinstance(val, Path): + val = val.read_bytes() + + if isinstance(val, bytes): + val = cast("_T", plistlib.loads(val)) + + return val