Naar de inhoud
Documentatie doorbladeren

De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.

Besloten

Security design

Threats, the security problems found in v1 and v2, and the concrete measures v3 takes against each.

Bijgewerkt: 18 September 2026

Incico stores exactly the data people most want kept private: home addresses, private phone numbers, birthdays, children’s details. The v3 security design starts from what went wrong before.

Findings in v1 and v2

The inventory of the existing code found the following. None of it is carried into v3; several items need action on the old repositories regardless.

#FindingWhereAction for the old code
F1TLS certificate validation disabled unconditionally in the shipped MAUI clientIncico-App .../HttpsClientHandlerService.csWithdraw v1 from the stores or hot-fix before any further distribution
F2Production secrets in source: SQL passwords, storage keys, Service Bus keys, OAuth client secrets, SMTP password, RSA signing keysIncico-Api, Incico-IS, Incico-Service appsettings and pipelinesRotate every credential; the repository history is compromised
F3Google service-account private key (playstore-console@incico-8b116) committed in the v2 folderIncicoV2/incico2/incico-8b116-*.jsonRevoke the key in Google Cloud IAM, create a new one outside any repository
F4OAuth tokens for Google/Microsoft stored in plaintext columnsSyncService tableDelete with the v1 database
F5JWT audience validation off; CORS allow-any; account lockout off; password rules bypassed on reset; ROPC grant enabledIncico-Api, Incico-ISRetired with v1
F6Tokens in browser localStorage on the web clientNotSoSecureStorage.csRetired (no web app in v3)
F7Debug-level logging of full request/response bodies including PIILoggingMiddleware.csRetired
F8Firebase API keys and release keystore committedIncico-AppNew keystore for v3; old one treated as leaked
F9v2 authentication entirely simulated: demo credentials pre-filled in the sign-in form, verification codes never checked, social buttons only navigateIncicoV2 features/auth/signin/useSignInFlow.ts:14-55Not shipped; superseded
F10v2 “Sign out” only navigates; tokens stay in the secure store and the persisted auth state stays signed inIncicoV2 app/profiles/settings.tsx:36-38Not shipped; v3 sign-out wipes cache and secure store (tested)
F11v2 web build keeps tokens in localStorage; no 401 refresh interceptor; expiresIn never checkedIncicoV2 services/storage/SecureStorage.ts:13-23No web app in v3
F12v2 passes contact name, e-mail, phone and photo URL as route query parameters between screensIncicoV2 app/contacts/search.tsx:43-53v3 passes ids only
F13v2 requests RECORD_AUDIO and (via a native module) SYSTEM_ALERT_WINDOW without any feature using them; iOS camera string describes a scanning feature that does not existIncicoV2 app.json:30-46v3 declares only permissions with a shipped feature and an in-app explanation
F14v2 logs verification codes and e-mail addresses with console.log, not stripped from release bundles; minifier mangling disabled for productionIncicoV2 useSignInFlow.ts:41, PhonesForm.tsx:314, metro.config.js:9-12v3 log allow-list test; R8 obfuscation on
F15v2 has no account deletion, no privacy policy link, no consent screenIncicoV2 app-widev3 Settings → Privacy, onboarding step 2.4

Threat model

Assets: profile details, connection graph, photos, sign-in credentials, push tokens. Adversaries: a curious or malicious connected user trying to see more than shared; a stranger enumerating users; a stolen or rooted phone; a compromised third-party (Google) token; a leaked repository; a malicious app on the same device; Incico operators themselves (data minimisation).

Measures

Identity and sessions

  • Supabase Auth with e-mail + password and magic link, plus Google and Apple sign-in. Apple sign-in is mandatory on iOS because Google sign-in is offered.
  • Native token flows: Google via Credential Manager (Android) and ASAuthorizationController (Apple); the app exchanges the provider ID token with signInWithIdToken. No browser redirect, no custom URL scheme, nothing another app can intercept.
  • Magic-link and password-reset links open the app through verified app links / universal links on https://www.incico.eu/auth/.... The website hosts assetlinks.json and apple-app-site-association.
  • Refresh-token rotation on, reuse interval 10 s (Supabase default in Saymail’s config), access token lifetime 1 hour.
  • E-mail verification required before onboarding continues. Password policy: minimum 10 characters, checked against the HaveIBeenPwned range API by Supabase Auth’s leaked-password protection.
  • Rate limits on sign-in, OTP and password reset at the Auth layer; CAPTCHA (Turnstile) on sign-up and password reset.
  • Account lockout is Supabase’s built-in throttling; there is no custom identity server to misconfigure.
  • Sessions can be listed and revoked from Settings (“Signed-in devices”) through list_sessions() and revoke_session(), which read and delete only the caller’s own auth.sessions rows and never return the IP address (A84).

Data access

  • RLS default-deny on every table; cross-user reads only via snapshots computed server-side (see Access control).
  • Search by hashed exact tokens only, rate-limited and audited. No name-prefix search of strangers; name search matches whole lower-cased words and only on names the owner marked searchable.
  • Blocking is honoured in search, invitations and snapshots.
  • Photos in a private bucket behind a policy function; no public URLs.
  • The service-role key exists only in Supabase’s edge-function secrets and in the deploy script’s environment, never in an app.

On the device

  • Session tokens in Android Keystore-backed storage (EncryptedSharedPreferences or its successor androidx.security DataStore) and the iOS Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, exactly like Saymail’s SecretStore and Keychain with the same contract: secrets never in the database, logs or analytics.
  • Google Contacts OAuth uses a public client with PKCE per platform (Android and iOS client ids registered for the app’s signing certificate and bundle id). No client secret in the binary. This fixes the weakness noted in Saymail, where a desktop client secret is embedded.
  • Nothing the app stores leaves the device in a cloud backup or a device transfer, the local cache database included (A98). On Android android:allowBackup="false" turns off cloud backup, and device transfer up to Android 11; from Android 12 it no longer stops a device-to-device transfer, so data_extraction_rules.xml stays as a single rule that excludes every storage domain from both cloud backup and device transfer. There is no fullBackupContent file and no per-file exclusion list to keep in step with new stores. After a transfer the person signs in again and the cache is fetched afresh. On iOS the cache file carries isExcludedFromBackup. The cache file is protected with the platform’s file encryption. Optionally the cache is wiped when the account signs out.
  • No screenshots of sensitive screens prevention is applied (a contact card is not a secret in the way a QR pairing code is); this is a deliberate choice.
  • Certificate pinning is not used (Supabase rotates certificates); the platform trust store is used unmodified. There is no code path that overrides TrustManager, HostnameVerifier, or App Transport Security.
  • Debug and developer menus live in a debug source set / #if DEBUG only, as in Saymail.
  • Release builds are shrunk and obfuscated with R8 and the mapping file is uploaded to Play; crash reports are symbolicated server-side. (This differs from Saymail’s -dontobfuscate; see the questions page.)

Server side

  • Edge functions verify the JWT and re-check ownership; they never trust ids in the body.
  • The fan-out call to push-fanout is a trigger the repository owns rather than a hosted database webhook, so it is migrated like everything else. Its URL and its shared secret live in Vault (push_fanout_url, push_fanout_secret), never in a migration: the URL differs per project and the secret must never be in git. The function sends the secret as x-webhook-secret and the edge function compares it in constant time. Without both Vault entries the trigger is a silent no-op, so a local stack or a fresh project keeps working and only loses the push.
  • pg_net’s queue is readable by the client roles at the database level. The extension grants anon and authenticated usage on schema net and read access to net.http_request_queue, which means a queued push row, including its x-webhook-secret header, is visible to those roles in Postgres. Those grants were made by supabase_admin, and a migration running as postgres cannot revoke them: the statements are accepted and change nothing. What keeps the queue out of reach is PostgREST, which serves only the schemas named in [api] schemas — public and graphql_public. net must never be added to that list, and that is the control this risk is accepted on.
  • Push payloads carry only a notification kind and an id, never contact details; the app fetches the change after the tap or in the background.
  • E-mail addresses enter the search index only once verified: anyone can type anyone’s address, and an unverified one would let an impostor be found under somebody else’s name. Phone numbers are indexed without that proof, because SMS verification is out of the launch scope (assumption A10); the day it arrives, one line in derive_search_tokens changes.
  • audit_events records invitations, searches, blocks, deletions and exports with a hashed IP, retained 12 months. A search row records how many tokens arrived and nothing else: storing the tokens would rebuild, one search at a time, exactly the address book the hashing exists to prevent.
  • Postgres roles: anon can call nothing except the Auth endpoints; authenticated reaches tables under RLS, the column-level update grants and the whitelisted functions; every other function is revoked from anon, authenticated, public.
  • Verification codes are never stored. email_verifications keeps a random salt and sha256(salt ‖ code), so a leaked backup cannot be replayed, and the comparison is constant-time.
  • Backups: Supabase daily backups, point-in-time recovery on the production project, 30-day retention. Account deletion removes live rows immediately; backups age out within 30 days. The privacy policy states this.

Supply chain and repository

  • No secrets in the repository. .env at the repo root (gitignored) for the Google client ids per environment, Android key.properties gitignored, keystore outside the repo, iOS signing via App Store Connect API key on the Mac mini, as in Saymail.
  • A pre-commit hook runs gitleaks on staged files; CI runs it on every push.
  • Dependencies pinned via Gradle version catalog and Swift Package Manager Package.resolved; Dependabot enabled. iOS aims for the Saymail posture of zero third-party dependencies except supabase-swift.
  • Third-party licences listed in an in-app “Open-source licences” screen (a gap noted in Saymail).

Privacy by design

  • Data minimisation: no analytics before consent; typed events only (PostHog EU) with an allow-list test that no property may contain an e-mail, phone or name.
  • Diagnostics logs never include detail values: on Android every log call goes through one AppLog facade that redacts e-mail addresses, phone numbers, tokens and secure-store key names before logcat or the diagnostics log sees a line (a unit test runs those patterns through it), and tools/log-check.mjs greps the call sites in CI for templates of sensitive names (Telemetry and privacy, A88). Crash reports carry classes and frames, never an exception’s message (A89).
  • Data export and account deletion are one tap each in Settings and also reachable from the website’s account page (/{lang}/account/delete/, A91) for users who lost their phone. That page signs in with an e-mail code through a first-party script and keeps the token in sessionStorage only while the page is open; the two edge functions answer CORS for the website’s origins only, never *.
  • The app never reads the device address book unless the user connects the “Device contacts” service, and then only with the platform permission dialog.

Store compliance checklist

  • Play Data Safety and Apple Privacy Nutrition labels derived from the tables in Telemetry and privacy.
  • Apple: Sign in with Apple offered; account deletion in-app; PrivacyInfo.xcprivacy lists UserDefaults reason CA92.1 and collected types Contact Info, Photos (user content), Device ID (for push), all linked to the user, none used for tracking.
  • Google: READ_CONTACTS/WRITE_CONTACTS declared only with the device-contacts feature and requested at the moment of connecting the service; no QUERY_ALL_PACKAGES, no background location, no SMS permissions.