Skip to content

Commit 7566e28

Browse files
committed
issue #24: implement better testing for establishing the fallback strategy when we fail to extract the key
1 parent 46dcf1e commit 7566e28

4 files changed

Lines changed: 232 additions & 62 deletions

File tree

python/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pyinstaller \
106106

107107
Zip up result (MacOS):
108108
```shell
109-
APP_VERSION=1.0.3
109+
APP_VERSION=1.0.4
110110
cd ./dist
111111
zip -r OpenTagViewer-ExportWizardMacOS-$APP_VERSION.zip OpenTagViewer.app/ OpenTagViewer
112112
```

python/main/airtag_decryptor.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from abc import ABC, abstractmethod
12
import os
23
import re
34
import shlex
@@ -37,25 +38,42 @@ class KeyStoreKeyNotFoundException(Exception):
3738
pass
3839

3940

40-
def get_key(label: str) -> bytearray:
41+
class AbstractSubprocessRunner(ABC):
42+
"""
43+
Wrapper class to enable easier unit testing
44+
"""
45+
46+
@abstractmethod
47+
def run(self, command: list[str]) -> str:
48+
...
49+
50+
51+
class SubprocessRunner(AbstractSubprocessRunner):
52+
"""
53+
Runs generic `subprocess` implementation
54+
"""
55+
56+
def run(self, command: list[str]) -> str:
57+
return subprocess.run(command, capture_output=True, text=True).stdout
58+
59+
60+
def get_key(label: str, runner: AbstractSubprocessRunner = SubprocessRunner()) -> bytearray:
4161
"""
4262
Tries to extract the key via the command `security find-generic-password -l <label> -w`.
4363
4464
:throws KeyStoreKeyNotFoundException: when not found or unable to extract
4565
"""
4666
try:
47-
result = subprocess.run(
48-
[
49-
"security",
50-
"find-generic-password",
51-
"-l",
52-
label,
53-
"-w"
54-
],
55-
capture_output=True,
56-
text=True
57-
)
58-
key_in_hex_format: str = result.stdout
67+
key_in_hex_format: str = runner.run([
68+
"security",
69+
"find-generic-password",
70+
"-l",
71+
label,
72+
"-w"
73+
])
74+
75+
if len(key_in_hex_format.strip()) == 0:
76+
raise KeyStoreKeyNotFoundException("security command output with flag -w returned empty")
5977

6078
return extract_key(key_in_hex_format)
6179

@@ -72,7 +90,7 @@ def extract_key(key_in_hex_format: str) -> bytearray:
7290
return key
7391

7492

75-
def get_key_from_full_output(label: str):
93+
def get_key_from_full_output(label: str, runner: AbstractSubprocessRunner = SubprocessRunner()):
7694
"""
7795
Tries to extract the key via the command `security find-generic-password -l <label>`.
7896
@@ -81,17 +99,16 @@ def get_key_from_full_output(label: str):
8199
:throws KeyStoreKeyNotFoundException: when not found in output or unable to extract
82100
"""
83101
try:
84-
result = subprocess.run(
85-
[
86-
"security",
87-
"find-generic-password",
88-
"-l",
89-
label
90-
],
91-
capture_output=True,
92-
text=True
93-
)
94-
full_key_info: str = result.stdout
102+
full_key_info: str = runner.run([
103+
"security",
104+
"find-generic-password",
105+
"-l",
106+
label
107+
])
108+
109+
if len(full_key_info.strip()) == 0:
110+
raise KeyStoreKeyNotFoundException("security command full output returned empty")
111+
95112
return extract_gena_key(full_key_info)
96113
except Exception as e:
97114
raise KeyStoreKeyNotFoundException("Failed to retrieve keystore value") from e
@@ -137,6 +154,14 @@ def extract_gena_key(output: str):
137154
return key
138155

139156

157+
def get_key_fallback(label: str, runner: AbstractSubprocessRunner = SubprocessRunner()) -> bytearray:
158+
try:
159+
return get_key(label, runner)
160+
except KeyStoreKeyNotFoundException:
161+
# Fallback to alternate solution (see issue #13)
162+
return get_key_from_full_output(label, runner)
163+
164+
140165
def decrypt_plist(in_file_path: str, key: bytearray) -> dict:
141166
"""
142167
Given an encrypted plist file at path `in_file_path`, decrypt it using `key` and AES-GMC and return

python/main/wizard.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616
KEYCHAIN_LABEL,
1717
INPUT_PATH,
1818
WHITELISTED_DIRS,
19-
KeyStoreKeyNotFoundException,
20-
get_key,
2119
decrypt_plist,
22-
get_key_from_full_output,
20+
get_key_fallback,
2321
make_output_path,
2422
dump_plist
2523
)
2624
from main.utils import MACOS_VER
2725

2826
# Wrapper around the main decryptor implementation that allows to filter which beacon files get exported/zipped
2927

30-
VERSION = "1.0.3"
28+
VERSION = "1.0.4"
3129

3230
APP_TITLE = f"OpenTagViewer AirTag Exporter {VERSION}"
3331

@@ -131,13 +129,6 @@ def _retrieve_beacon_data(self) -> dict[str, BeaconData]:
131129
self.quit()
132130
raise Exception("User does not want to give password access to keystore!")
133131

134-
def _retrieve_key(self):
135-
try:
136-
return get_key(KEYCHAIN_LABEL)
137-
except KeyStoreKeyNotFoundException:
138-
# Fallback to alternate solution (see issue #13)
139-
return get_key_from_full_output(KEYCHAIN_LABEL)
140-
141132
def _read_all_plists(self, beacon_store_key: bytearray) -> tuple[list[PListFileInfo], list[PListFileInfo]]:
142133
owned_beacons: list[PListFileInfo]
143134
beacon_naming_records: list[PListFileInfo]
@@ -162,7 +153,7 @@ def _read_all_plists(self, beacon_store_key: bytearray) -> tuple[list[PListFileI
162153

163154
def _create_beacon_data_map(self) -> dict[str, BeaconData]:
164155
# get key: prompts password entry
165-
beacon_store_key: bytearray = self._retrieve_key()
156+
beacon_store_key: bytearray = get_key_fallback(KEYCHAIN_LABEL)
166157

167158
if not beacon_store_key:
168159
messagebox.showerror(

0 commit comments

Comments
 (0)