Skip to content

Commit 33b0c5f

Browse files
committed
Reorg
1 parent 845f4d9 commit 33b0c5f

File tree

12 files changed

+318
-0
lines changed

12 files changed

+318
-0
lines changed

README.md

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
# fmd_api: Python client for interacting with FMD (fmd-foss.org)
2+
3+
This directory contains Python scripts for interacting with an FMD (Find My Device) server, including authentication, key retrieval, and location data decryption.
4+
For more information on this open source alternative to Google's Find My Device service, read the Credits section at the bottom of this README.
5+
In this repo you'll find fmd_api.py is the tool supporting fmd_client.py, used in most of the examples.
6+
7+
## Prerequisites
8+
- Python 3.7+
9+
- Install dependencies:
10+
```
11+
pip install requests argon2-cffi cryptography
12+
```
13+
14+
## Scripts Overview
15+
16+
### Main Client
17+
18+
#### `fmd_client.py`
19+
**The primary tool for bulk data export.** Downloads locations and/or pictures, saving them to a directory or ZIP archive.
20+
21+
**Usage:**
22+
```bash
23+
python fmd_client.py --url <server_url> --id <fmd_id> --password <password> --output <path> [--locations [N]] [--pictures [N]]
24+
```
25+
26+
**Options:**
27+
- `--locations [N]`: Export all locations, or specify N for the most recent N locations
28+
- `--pictures [N]`: Export all pictures, or specify N for the most recent N pictures
29+
- `--output`: Output directory or `.zip` file path
30+
- `--session`: Session duration in seconds (default: 3600)
31+
32+
**Examples:**
33+
```bash
34+
# Export all locations to CSV
35+
python fmd_client.py --url https://fmd.example.com --id alice --password secret --output data --locations
36+
37+
# Export last 10 locations and 5 pictures to ZIP
38+
python fmd_client.py --url https://fmd.example.com --id alice --password secret --output export.zip --locations 10 --pictures 5
39+
```
40+
41+
### Debugging Scripts
42+
43+
Located in `debugging/`, these scripts help test individual workflows and troubleshoot issues.
44+
45+
#### `fmd_get_location.py`
46+
**End-to-end test:** Authenticates, retrieves, and decrypts the latest location in one step.
47+
48+
**Usage:**
49+
```bash
50+
cd debugging
51+
python fmd_get_location.py --url <server_url> --id <fmd_id> --password <password>
52+
```
53+
54+
#### `fmd_export_data.py`
55+
**Test native export:** Downloads the server's pre-packaged export ZIP (if available).
56+
57+
**Usage:**
58+
```bash
59+
cd debugging
60+
python fmd_export_data.py --url <server_url> --id <fmd_id> --password <password> --output export.zip
61+
```
62+
63+
#### `request_location_example.py`
64+
**Request new location:** Triggers a device to capture and upload a new location update.
65+
66+
**Usage:**
67+
```bash
68+
cd debugging
69+
python request_location_example.py --url <server_url> --id <fmd_id> --password <password> [--provider all|gps|cell|last] [--wait SECONDS]
70+
```
71+
72+
**Options:**
73+
- `--provider`: Location provider to use (default: all)
74+
- `all`: Use all available providers (GPS, network, fused)
75+
- `gps`: GPS only (most accurate, slower)
76+
- `cell`: Cellular network (faster, less accurate)
77+
- `last`: Don't request new location, just get last known
78+
- `--wait`: Seconds to wait for location update (default: 30)
79+
80+
**Example:**
81+
```bash
82+
# Request GPS location and wait 45 seconds
83+
python request_location_example.py --url https://fmd.example.com --id alice --password secret --provider gps --wait 45
84+
85+
# Quick cellular network location
86+
python request_location_example.py --url https://fmd.example.com --id alice --password secret --provider cell --wait 20
87+
```
88+
89+
#### `diagnose_blob.py`
90+
**Diagnostic tool:** Analyzes encrypted blob structure to troubleshoot decryption issues.
91+
92+
**Usage:**
93+
```bash
94+
cd debugging
95+
python diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>
96+
```
97+
98+
Shows:
99+
- Private key size and type
100+
- Actual blob size vs. expected structure
101+
- Analysis of RSA session key packet layout
102+
- First/last bytes in hex for inspection
103+
104+
## Core Library
105+
106+
### `fmd_api.py`
107+
The foundational API library providing the `FmdApi` class. Handles:
108+
- Authentication (salt retrieval, Argon2id password hashing, token management)
109+
- Encrypted private key retrieval and decryption
110+
- Data blob decryption (RSA-OAEP + AES-GCM)
111+
- Location and picture retrieval
112+
- Command sending (request location updates, ring, lock, camera)
113+
- Commands are cryptographically signed using RSA-PSS to prove authenticity
114+
115+
**For application developers:** See [LOCATION_FIELDS.md](LOCATION_FIELDS.md) for detailed documentation on extracting and using accuracy, altitude, speed, and heading fields.
116+
117+
**Quick example:**
118+
```python
119+
import asyncio
120+
import json
121+
from fmd_api import FmdApi
122+
123+
async def main():
124+
# Authenticate (automatically retrieves and decrypts private key)
125+
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")
126+
127+
# Request a new location update
128+
await api.request_location('gps') # or 'all', 'cell', 'last'
129+
await asyncio.sleep(30) # Wait for device to respond
130+
131+
# Get locations
132+
locations = await api.get_all_locations(num_to_get=10) # Last 10, or -1 for all
133+
134+
# Decrypt a location blob
135+
decrypted_data = api.decrypt_data_blob(locations[0])
136+
location = json.loads(decrypted_data)
137+
138+
# Access fields (use .get() for optional fields)
139+
lat = location['lat']
140+
lon = location['lon']
141+
speed = location.get('speed') # Optional, only when moving
142+
heading = location.get('heading') # Optional, only when moving
143+
144+
# Send commands (see Available Commands section below)
145+
await api.send_command('ring') # Make device ring
146+
await api.send_command('bluetooth on') # Enable Bluetooth
147+
await api.send_command('camera front') # Take picture with front camera
148+
149+
asyncio.run(main())
150+
```
151+
152+
### Available Commands
153+
154+
The FMD Android app supports a comprehensive set of commands. You can send them using `api.send_command(command)` or use the convenience methods and constants:
155+
156+
#### Location Requests
157+
```python
158+
# Using convenience method
159+
await api.request_location('gps') # GPS only
160+
await api.request_location('all') # All providers (default)
161+
await api.request_location('cell') # Cellular network only
162+
163+
# Using send_command directly
164+
await api.send_command('locate gps')
165+
await api.send_command('locate')
166+
await api.send_command('locate cell')
167+
await api.send_command('locate last') # Last known, no new request
168+
169+
# Using constants
170+
from fmd_api import FmdCommands
171+
await api.send_command(FmdCommands.LOCATE_GPS)
172+
```
173+
174+
#### Device Control
175+
```python
176+
# Ring device
177+
await api.send_command('ring')
178+
await api.send_command(FmdCommands.RING)
179+
180+
# Lock device screen
181+
await api.send_command('lock')
182+
await api.send_command(FmdCommands.LOCK)
183+
184+
# ⚠️ Delete/wipe device (DESTRUCTIVE - factory reset!)
185+
await api.send_command('delete')
186+
await api.send_command(FmdCommands.DELETE)
187+
```
188+
189+
#### Camera
190+
```python
191+
# Using convenience method
192+
await api.take_picture('back') # Rear camera (default)
193+
await api.take_picture('front') # Front camera (selfie)
194+
195+
# Using send_command
196+
await api.send_command('camera back')
197+
await api.send_command('camera front')
198+
199+
# Using constants
200+
await api.send_command(FmdCommands.CAMERA_BACK)
201+
await api.send_command(FmdCommands.CAMERA_FRONT)
202+
```
203+
204+
#### Bluetooth
205+
```python
206+
# Using convenience method
207+
await api.toggle_bluetooth(True) # Enable
208+
await api.toggle_bluetooth(False) # Disable
209+
210+
# Using send_command
211+
await api.send_command('bluetooth on')
212+
await api.send_command('bluetooth off')
213+
214+
# Using constants
215+
await api.send_command(FmdCommands.BLUETOOTH_ON)
216+
await api.send_command(FmdCommands.BLUETOOTH_OFF)
217+
```
218+
219+
**Note:** Android 12+ requires BLUETOOTH_CONNECT permission.
220+
221+
#### Do Not Disturb Mode
222+
```python
223+
# Using convenience method
224+
await api.toggle_do_not_disturb(True) # Enable DND
225+
await api.toggle_do_not_disturb(False) # Disable DND
226+
227+
# Using send_command
228+
await api.send_command('nodisturb on')
229+
await api.send_command('nodisturb off')
230+
231+
# Using constants
232+
await api.send_command(FmdCommands.NODISTURB_ON)
233+
await api.send_command(FmdCommands.NODISTURB_OFF)
234+
```
235+
236+
**Note:** Requires Do Not Disturb Access permission.
237+
238+
#### Ringer Mode
239+
```python
240+
# Using convenience method
241+
await api.set_ringer_mode('normal') # Sound + vibrate
242+
await api.set_ringer_mode('vibrate') # Vibrate only
243+
await api.set_ringer_mode('silent') # Silent (also enables DND)
244+
245+
# Using send_command
246+
await api.send_command('ringermode normal')
247+
await api.send_command('ringermode vibrate')
248+
await api.send_command('ringermode silent')
249+
250+
# Using constants
251+
await api.send_command(FmdCommands.RINGERMODE_NORMAL)
252+
await api.send_command(FmdCommands.RINGERMODE_VIBRATE)
253+
await api.send_command(FmdCommands.RINGERMODE_SILENT)
254+
```
255+
256+
**Note:** Setting to "silent" also enables Do Not Disturb (Android behavior). Requires Do Not Disturb Access permission.
257+
258+
#### Device Information
259+
```python
260+
# Get network statistics (IP addresses, WiFi SSID/BSSID)
261+
await api.get_device_stats()
262+
await api.send_command('stats')
263+
await api.send_command(FmdCommands.STATS)
264+
265+
# Get battery and GPS status
266+
await api.send_command('gps')
267+
await api.send_command(FmdCommands.GPS)
268+
```
269+
270+
**Note:** `stats` command requires Location permission to access WiFi information.
271+
272+
#### Command Testing Script
273+
Test any command easily:
274+
```bash
275+
cd debugging
276+
python test_command.py <command> --url <server_url> --id <fmd_id> --password <password>
277+
278+
# Examples
279+
python test_command.py "ring" --url https://fmd.example.com --id alice --password secret
280+
python test_command.py "bluetooth on" --url https://fmd.example.com --id alice --password secret
281+
python test_command.py "ringermode vibrate" --url https://fmd.example.com --id alice --password secret
282+
```
283+
284+
## Troubleshooting
285+
286+
### Empty or Invalid Blobs
287+
If you see warnings like `"Blob too small for decryption"`, the server returned empty/corrupted data. This can happen when:
288+
- No location data was uploaded for that time period
289+
- Data was deleted or corrupted server-side
290+
- The server returns placeholder values for missing data
291+
292+
The client will skip these automatically and report the count at the end.
293+
294+
### Debugging Decryption Issues
295+
Use `debugging/diagnose_blob.py` to analyze blob structure:
296+
```bash
297+
cd debugging
298+
python diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>
299+
```
300+
301+
This shows the actual blob size, expected structure, and helps identify if the RSA key size or encryption format has changed.
302+
303+
## Notes
304+
- All scripts use Argon2id password hashing and AES-GCM/RSA-OAEP encryption, matching the FMD web client
305+
- Blobs must be at least 396 bytes (384 RSA session key + 12 IV + ciphertext) to be valid
306+
- Base64 data from the server may be missing padding - use `_pad_base64()` helper when needed
307+
- **Location data fields**:
308+
- Always present: `time`, `provider`, `bat` (battery %), `lat`, `lon`, `date` (Unix ms)
309+
- Optional (depending on provider): `accuracy` (meters), `altitude` (meters), `speed` (m/s), `heading` (degrees)
310+
- Picture data is double-encoded: encrypted blob → base64 string → actual image bytes
311+
312+
## Credits
313+
314+
This project is a client for the open-source FMD (Find My Device) server. The FMD project provides a decentralized, self-hostable alternative to commercial device tracking services.
315+
316+
- **[fmd-foss.org](https://fmd-foss.org/)**: The official project website, offering general information, documentation, and news.
317+
- **[fmd-foss on GitLab](https://gitlab.com/fmd-foss)**: The official GitLab group hosting the source code for the server, Android client, web UI, and other related projects.
318+
- **[fmd.nulide.de](https://fmd.nulide.de/)**: A generously hosted public instance of the FMD server available for community use.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)