Security: Found a vulnerability? Please read our Security Policy and report it privately to security@ethos-protocol.app — do not open a public issue. A
security.txt(RFC 9116) is published at/.well-known/security.txtand will also be served fromhttps://ethos-protocol.app/.well-known/security.txtonce the domain is configured.
Ethos-Protocol mobile apps (iOS + Android) provide a native interface for managing vaults, checking in, and receiving expiry reminders. Both apps share the same REST API contract and feature set.
mobile/
├── shared/
│ └── api-contract.md # Shared API spec (iOS + Android)
├── ios/EthosProtocol/
│ └── Sources/
│ ├── App/ # Entry point, app lifecycle
│ ├── Models/ # Vault, AuthToken, etc.
│ ├── Services/
│ │ ├── APIClient.swift # Ktor-style async HTTP client
│ │ ├── PasskeyService.swift # ASAuthorization / WebAuthn
│ │ ├── KeychainService.swift# Secure token storage
│ │ ├── NotificationService.swift # APNs + local reminders
│ │ └── OfflineSupport.swift # NetworkMonitor + disk cache
│ ├── ViewModels/ # AuthStore, VaultStore (ObservableObject)
│ └── Views/ # SwiftUI screens
└── android/app/src/main/java/com/ethosprotocol/
├── api/
│ ├── ApiClient.kt # Ktor HTTP client
│ └── Infrastructure.kt # NetworkMonitor, OfflineCache, TokenProvider
├── models/ # Kotlinx.serialization data classes
├── services/
│ ├── PasskeyService.kt # CredentialManager / WebAuthn
│ ├── PushService.kt # Firebase Messaging
│ └── NotificationHelper.kt# Local notification display
├── ui/
│ ├── ViewModels.kt # AuthViewModel, VaultViewModel (Hilt)
│ ├── MainActivity.kt # NavHost entry point
│ ├── screens/Screens.kt # Compose screens
│ └── theme/Theme.kt # Material3 dynamic color
└── di/AppModule.kt # Hilt DI bindings
- iOS:
ASAuthorizationPlatformPublicKeyCredentialProvider(iOS 16+) - Android:
CredentialManagerAPI (Android 9+, API 28+) - Flow:
getChallenge()→ device biometric prompt →verifyPasskey()→ JWT stored in Keychain/SharedPreferences - Relying party:
ethos-protocol.app(requires.well-known/assetlinks.json+ Apple App Site Association)
- iOS: APNs via
UNUserNotificationCenter. Device token registered to backend on first launch.- Local reminders scheduled 24h before vault expiry via
UNTimeIntervalNotificationTrigger - Actionable notification category
CHECK_INwith inline "Check In" action
- Local reminders scheduled 24h before vault expiry via
- Android: Firebase Cloud Messaging (FCM). Token refreshed via
onNewToken.- Notification channel
ttl_reminders(IMPORTANCE_HIGH) - Deep-link intent to
MainActivitywithvault_idextra
- Notification channel
NetworkMonitorchecks live connectivity before every requestOfflineCachestores last successful GET responses keyed by URL (SHA-256 filename)- On network unavailable: cached data served transparently; mutations show "offline" error
- iOS:
CryptoKit.SHA256for cache keys; Android:MessageDigest("SHA-256") - Offline check-in queue: a check-in made while offline is queued for retry rather
than just failing.
- iOS:
PendingCheckInStore(disk-backed JSON) is the sole insertion point;CheckInSyncTaskdrains it via aBGProcessingTaskonce connectivity returns. This is the only check-in queue implementation — an earlier duplicate (CheckInQueue/CheckInSyncService) was removed in 8d8d59d; seePendingCheckInStoreTests/CheckInSyncTaskTestsfor the regression guard. - Android:
PendingActionDao/PendingActionDatabase(Room), drained byPendingActionSyncWorker(WorkManager)
- iOS:
- iOS:
@StateObject/ObservableObjectstores (AuthStore,VaultStore) injected via SwiftUI environment - Android: Hilt-injected
ViewModels withStateFlow+collectAsStateWithLifecycle
- Install XcodeGen (
brew install xcodegen) — the.xcodeprojis generated, not checked in - From
ios/EthosProtocol, runmkdir -p Xcode && xcodegen generate --project Xcodeto produceXcode/EthosProtocol.xcodeproj(anEthosProtocolapp target +TTLWidgetwidget extension, perproject.yml) — theXcode/directory must exist beforexcodegen generateruns, or the copy step fails - Open
ios/EthosProtocol/Xcode/EthosProtocol.xcodeprojin Xcode 15+ - Set your Apple Developer Team in signing settings for both the
EthosProtocolandTTLWidgettargets (project.ymlleavesDEVELOPMENT_TEAMblank on purpose — bundle IDscom.ethosprotocol/com.ethosprotocol.TTLWidgetare already set) API_BASE_URLis already set inEthosProtocol/Info.plistandTTLWidget/Info.plist; edit both (they're separate bundles, read independently at runtime) if you need to point at a different environment- Certificate pinning is not active by default: both
Info.plists declareTLS_PUBLIC_KEY_PINSas the$(TLS_PUBLIC_KEY_PIN_CURRENT)/$(TLS_PUBLIC_KEY_PIN_BACKUP)build settings (declared blank inproject.yml), andPinningDelegateignores blank/unexpanded entries. Set both settings to Base64-encoded SPKI SHA-256 hashes — in Xcode's build settings, an.xcconfig, or on thexcodebuildinvocation (xcodebuild … TLS_PUBLIC_KEY_PIN_CURRENT=… TLS_PUBLIC_KEY_PIN_BACKUP=…) — before shipping a Release build; seeSources/Services/CertificatePinning.swiftfor the rotation strategy.ios-ci.yml'sbuild-and-testjob runs.github/scripts/check_tls_pinning.py --configuration Releaseagainst both files and fails the build if either is missing or empty; Debug builds are exempt (PinningDelegateintentionally treats an empty pin set as "pinning disabled" for local dev) - Configure Apple App Site Association at
https://ethos-protocol.app/.well-known/apple-app-site-association, listing this app's App ID under bothapplinks(Universal Links) andwebcredentials(platform passkeys) - In the Apple Developer portal, enable Push Notifications, Associated Domains, iCloud (Key-Value storage), and Keychain Sharing capabilities for the
com.ethosprotocolApp ID, and Keychain Sharing forcom.ethosprotocol.TTLWidget— matchingEthosProtocol/EthosProtocol.entitlements/TTLWidget/TTLWidget.entitlements. Set up an APNs key in App Store Connect for push. - Re-run
mkdir -p Xcode && xcodegen generate --project Xcodeany timeproject.ymlchanges; the generatedXcode/directory is disposable and shouldn't be committed
- Open
androidin Android Studio Hedgehog+ - Add
google-services.jsonfrom Firebase Console - Configure
assetlinks.jsonathttps://ethos-protocol.app/.well-known/assetlinks.json - Set
API_BASE_URLinbuild.gradle.ktsbuildConfigField - Configure the certificate pins for release builds by setting
ETHOS_CERT_PINS(environment variable) orethos.certPins(in~/.gradle/gradle.properties, never committed) to a comma-separated list of Base64 SHA-256 SPKI digests — the current certificate's pin plus a backup for the next one. This is required before any release build: pinning is not active by default. When unset,CertificatePinner's pin set is empty — pinning is disabled and the system trust store decides (#169), so there is no compiled-in pin that could reject every real certificate. CI'sVerify release certificate pins are not placeholdersstep reports an unconfigured release build (#173), and fails it outright once the pins are configured but wrong, or once release signing is configured (i.e. the artifact is actually shippable). Debug builds are not gated, since an empty pin set disables pinning for local/dev hosts. Compute a pin with:openssl s_client -connect api.ethos-protocol.app:443 2>/dev/null \ | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary | openssl enc -base64
cd ios/EthosProtocol
swift testCovers: model decoding, Keychain round-trip, offline cache, Base64URL encoding.
Tests run against the SPM package (Package.swift) directly and don't require the
XcodeGen-generated project; CI runs this the same way, via xcodebuild test against
an iOS Simulator destination (swift test alone defaults to macOS, which can't build
the app's iOS-only framework imports).
CI runs a weekly (and per-PR on Package.swift / Package.resolved changes) vulnerability
scan against all pinned SPM dependencies using osv-scanner,
querying the OSV database. The scan fails the build for any dependency
with a published CVE at CVSS ≥ 7.0 (high or critical). This mirrors the Android
android-dependency-check.yml OWASP scan.
Workflow: .github/workflows/ios-dependency-check.yml
False-positive suppressions: ios/EthosProtocol/spm-vulnerability-suppressions.toml
(follows the same pattern as android/dependency-check-suppressions.xml — each entry
requires a documented rationale).
To run locally:
brew install osv-scanner
cd ios/EthosProtocol
osv-scanner --lockfile "swift:Package.resolved" --fail-on-severity HIGHcd android
./gradlew test # Unit tests (JVM)
./gradlew connectedAndroidTest # Instrumented tests (device/emulator)Covers: ViewModel state transitions, model logic, Compose UI smoke tests.
<<<<<<< HEAD
The repo runs a dependency scan for both platforms with the same trigger model:
pushtomainwhen dependency manifests changepull_requesttomainfor the same dependency-focused paths- weekly
scheduleruns to catch newly disclosed CVEs between dependency bumps
Workflow files:
- Android:
.github/workflows/android-dependency-check.yml - iOS:
.github/workflows/ios-dependency-check.yml
Both workflows treat dependency-scan failures as a consistent, human-readable warning in the job log and create a scheduled-run issue alert when the scan fails outside a PR context.
Release notes are expected to stay aligned with the "Known gaps" table in PARITY.md. To prevent a release from claiming a parity issue is closed while the table still lists it as open, CI includes a parity audit workflow:
- Workflow:
.github/workflows/release-notes-parity-check.yml - Script:
.github/scripts/release_notes_parity_check.py
The validator:
- extracts issue numbers from the "Known gaps" table in PARITY.md
- scans merged PRs for parity-gap issue references and close verbs such as "closes #..." or "fixes #..."
- checks the current release notes for the same claim patterns
- fails when a listed parity gap is explicitly called out as closed without the table being updated
Run it locally from the repo root with:
python3 .github/scripts/release_notes_parity_check.py \
--parity-file PARITY.md \
--prs-file merged-prs.json \
--release-notes-file release-notes.mdTo generate the JSON input for the PR scan:
gh pr list --state merged --limit 200 --json number,title,body > merged-prs.jsonThis keeps parity-status messaging consistent with the cross-platform tracking table and helps release notes communicate platform catch-up progress accurately.
.github/workflows/staging-smoke-test.yml runs scripts/smoke_test_staging.sh
against a staging deployment (a separate STAGING_API_BASE_URL from the
per-client API_BASE_URL set in Info.plist / build.gradle.kts — staging
is a fixed CI-only target, not something either app build points at). It
exercises auth, GET /vaults, and POST /vaults/{id}/checkin to catch a
backend/client contract mismatch (see shared/api-contract.md) before a
release build is cut. The workflow is exposed via workflow_call so a release
workflow can add needs: on it once one exists.
pr-365-merge