Skip to content

Commit dbce08a

Browse files
committed
Creating new repo with fmd_api package name
0 parents  commit dbce08a

20 files changed

+2806
-0
lines changed

.gitignore

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
7+
# C extensions
8+
*.so
9+
10+
# Distribution / packaging
11+
.Python
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
pip-wheel-metadata/
25+
share/python-wheels/
26+
*.egg-info/
27+
.installed.cfg
28+
*.egg
29+
MANIFEST
30+
31+
# Virtual environment
32+
venv/
33+
env/
34+
.venv/
35+
36+
# IDE / Editor folders
37+
.vscode/
38+
.idea/
39+
40+
# FMD Server and android app files
41+
fmd-server/
42+
fmd-android/

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.

0 commit comments

Comments
 (0)