Skip to content

FMD Server API v2 + Protocol v2 support (3.0.0) - #22

Merged
devinslick merged 8 commits into
mainfrom
api-v2
Oct 1, 2026
Merged

devinslick merged 8 commits into
mainfrom
api-v2

Conversation

@devinslick

Copy link
Copy Markdown
Owner

Summary

  • Full API v2 / Protocol v2 support for fmd-server >= 0.17.0, negotiated per-account via the salt endpoint (protoVersion); v1 accounts and older servers stay on the v1 path unchanged
  • Protocol v2 crypto per the fmd-foss/server-protocol spec (Argon2id t=1/p=4/m=128MiB + HKDF-SHA256 key schedule, AES-GCM master-key envelope, per-type KEKs / per-item DEKs, context-bound ADs) mirroring the fmd-android reference byte-for-byte
  • register_v2() for creating new protocol-v2 accounts; 401 re-auth for v2 sessions via stored K_auth
  • Public surface (create/authenticate/get_locations/decrypt_data_blob/export_auth_artifacts/from_auth_artifacts) unchanged in shape; AuthArtifacts gains protocol_version/master_key/auth_key
  • ApiV2Error now derives from FmdApiException

Testing

  • 147/147 unit + live probes green
  • Live end-to-end against fmd.devinslick.com 0.17.0: register -> upload -> fresh-client login -> fetch -> decrypt; artifacts password-free round-trip; 401 -> relogin -> fetch; wrong-password -> AuthenticationError; v1/v2 accounts coexisting
  • Verified inside Home Assistant core dev instance: config flow on a v2 account creates a loaded entry and a device_tracker with decrypted data

BREAKING: none for v1 users; major bump reflects the new protocol surface.

Implements the fmd-foss/server-protocol v2 key schedule exactly as the
fmd-android reference (CryptoV2.kt / LongTermKeys.kt / CypherUtils.java):
- derive_password_key: Argon2id(t=1,p=4,m=128MiB) over
  'fmd_v2_password'||SHA256(U)||P, HKDF-SHA256 branches for K_auth/K_pmk
- decrypt_master_key: AES-GCM unwrap of login's encMasterKey64 under K_pmk
- LongTermKeys: per-type KEKs; decrypt_data_blob parses the
  [iv|dek|tag][iv|data|tag] envelope with context-bound ADs
  (type||SHA256(U)||item_id||unix_millis_be64)
Round-trip tests mirror the Android encrypt path; 5 passing.
Spec + OpenAPI v2 saved under docs/ for reference.
- negotiate_protocol(): GET /api/v2/account/{name}/salt -> protoVersion,
  falls back to v1 when endpoint missing (pre-0.17.0 servers)
- login_v2(): Argon2id+HKDF K_auth login, encMasterKey64 unwrap to
  LongTermKeys; protocol-v1 accounts (empty encMasterKey64) are refused
  with a clear error so callers stay on the v1 path
- get_data_items()/decrypt_item(): EncryptedItem envelopes from
  GET /api/v2/data/{type}, newest first, decrypt via protocol_v2
- _request_v2(): bearer-auth JSON helper, 401/403/404 mapping
  (403 -> AuthenticationError)
- ApiV2Mixin mixed into FmdClient

Live-verified against fmd.devinslick.com 0.17.0: v1 account negotiates
proto=1, v2 salt responds, v1 refusal path works. Suite 147/147.
- protocol_v2: encrypt side (generate/encrypt master key,
  LongTermKeys.encrypt_data_blob mirroring the Android format)
- api_v2: register_v2 (client-side salt, K_auth/K_pmk, master key
  upload), post_data_items (encrypted upload helper)
- _request_v2: tolerate empty 200 bodies and non-JSON responses
- Live E2E on fmd.devinslick.com 0.17.0 with devinslick-v2test:
  register -> upload -> fresh login -> fetch -> decrypt all pass;
  server reports protoVersion=2 for the new account
- authenticate(): negotiates via salt endpoint, branches to login_v2
- get_locations(): v2 branch returns id:ts:b64 envelopes (surface-compatible)
- decrypt_data_blob(): recognises v2 envelopes, decrypts via LongTermKeys
- export/from_auth_artifacts: protocol_version + master_key fields;
  password-free resume for v2 sessions
- Live-verified: create->get_locations->decrypt and artifacts round-trip
  against fmd.devinslick.com (devinslick-v2test, protoVersion=2)
Suite 147/147.
- ApiV2Error now subclasses FmdApiException (HA coordinator catches it)
- 401 on v2 requests re-logins with stored K_auth (artifacts now carry
  auth_key); long-running integrations survive token expiry
- login_v2/register_v2 retain K_auth on the session
Live-verified: corrupted-token resume -> 401 -> relogin -> fetch+decrypt
PASS; wrong-password -> AuthenticationError; v1 account via v2 refused;
negotiation regression (v1 stays v1) PASS. Suite 147/147.
@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.48860% with 152 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
fmd_api/api_v2.py 35.76% 96 Missing and 1 partial ⚠️
fmd_api/client.py 17.50% 28 Missing and 5 partials ⚠️
fmd_api/protocol_v2.py 76.84% 20 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@devinslick
devinslick merged commit 9e5bfc9 into main Oct 1, 2026
22 of 23 checks passed
@devinslick
devinslick deleted the api-v2 branch October 1, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant