├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── net
│ │ └── rec0de
│ │ └── android
│ │ └── watchwitch
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── net
│ │ │ └── rec0de
│ │ │ └── android
│ │ │ └── watchwitch
│ │ │ ├── IdsLogger.kt
│ │ │ ├── KeyReceiver.kt
│ │ │ ├── KeystoreHelper.kt
│ │ │ ├── Logger.kt
│ │ │ ├── LongTermStorage.kt
│ │ │ ├── NotificationListenerService.kt
│ │ │ ├── RoutingManager.kt
│ │ │ ├── TcpServerService.kt
│ │ │ ├── UdpHandler.kt
│ │ │ ├── Utils.kt
│ │ │ ├── WatchState.kt
│ │ │ ├── activities
│ │ │ ├── ChatActivity.kt
│ │ │ ├── FilesActivity.kt
│ │ │ ├── HealthLogActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MapActivity.kt
│ │ │ ├── NetworkLogActivity.kt
│ │ │ ├── NotificationConsentDialog.kt
│ │ │ ├── ProcessFirewallActivity.kt
│ │ │ └── WatchStateActivity.kt
│ │ │ ├── adapter
│ │ │ ├── AlarmsAdapter.kt
│ │ │ ├── ChatbubbleAdapter.kt
│ │ │ ├── FilesAdapter.kt
│ │ │ ├── FirewallProcessAdapter.kt
│ │ │ ├── HealthLogAdapter.kt
│ │ │ ├── NetworkStatsAdapter.kt
│ │ │ └── OpenAppsAdapter.kt
│ │ │ ├── alloy
│ │ │ ├── AlloyController.kt
│ │ │ ├── AlloyHandler.kt
│ │ │ ├── AlloyMessage.kt
│ │ │ ├── AlloyService.kt
│ │ │ ├── ConcreteMessages.kt
│ │ │ └── UTunControlMessage.kt
│ │ │ ├── bitmage
│ │ │ └── bitmage.kt
│ │ │ ├── decoders
│ │ │ ├── aoverc
│ │ │ │ ├── Decryptor.kt
│ │ │ │ ├── KeystoreBackedDecryptor.kt
│ │ │ │ └── MPKeys.kt
│ │ │ ├── bplist
│ │ │ │ ├── ActivityStatisticsDecoder.kt
│ │ │ │ ├── BPListParser.kt
│ │ │ │ ├── KeyedArchiveDecoder.kt
│ │ │ │ ├── KeyedArchiveEncoder.kt
│ │ │ │ └── bplistObjects.kt
│ │ │ ├── compression
│ │ │ │ └── GzipDecoder.kt
│ │ │ └── protobuf
│ │ │ │ └── ProtobufParser.kt
│ │ │ ├── ike
│ │ │ ├── IKEMessage.kt
│ │ │ ├── IKEPayload.kt
│ │ │ ├── IKETransport.kt
│ │ │ └── IKEv2Session.kt
│ │ │ ├── nwsc
│ │ │ ├── NWSCManager.kt
│ │ │ └── NWSCPacket.kt
│ │ │ ├── servicehandlers
│ │ │ ├── CoreDuet.kt
│ │ │ ├── FindMyLocalDevice.kt
│ │ │ ├── GenericResourceTransferReceiver.kt
│ │ │ ├── PreferencesSync.kt
│ │ │ ├── Screenshotter.kt
│ │ │ ├── health
│ │ │ │ ├── HealthObjects.kt
│ │ │ │ ├── HealthSync.kt
│ │ │ │ ├── NanoSyncEntity.kt
│ │ │ │ ├── NanoSyncMessage.kt
│ │ │ │ ├── Samples.kt
│ │ │ │ ├── SyncStatusKey.kt
│ │ │ │ └── db
│ │ │ │ │ ├── DatabaseSecretProvider.kt
│ │ │ │ │ ├── DatabaseWrangler.kt
│ │ │ │ │ ├── HealthSyncContract.kt
│ │ │ │ │ ├── HealthSyncHelper.kt
│ │ │ │ │ ├── HealthSyncSecureContract.kt
│ │ │ │ │ └── HealthSyncSecureHelper.kt
│ │ │ └── messaging
│ │ │ │ ├── Action.kt
│ │ │ │ ├── ActionRequests.kt
│ │ │ │ ├── Bulletin.kt
│ │ │ │ ├── BulletinAttachment.kt
│ │ │ │ ├── BulletinDistributorService.kt
│ │ │ │ ├── BulletinRequest.kt
│ │ │ │ ├── BulletinSummary.kt
│ │ │ │ ├── DidPlayLightsAndSirens.kt
│ │ │ │ ├── RemoveBulletinRequest.kt
│ │ │ │ ├── SectionIcon.kt
│ │ │ │ ├── SettingsSyncObjects.kt
│ │ │ │ └── Trailer.kt
│ │ │ ├── shoes
│ │ │ ├── NetworkStats.kt
│ │ │ ├── ShoesMessages.kt
│ │ │ ├── ShoesProxyHandler.kt
│ │ │ └── ShoesService.kt
│ │ │ └── watchsim
│ │ │ ├── AlloyControlClient.kt
│ │ │ ├── AlloyDataClient.kt
│ │ │ ├── HealthDataMockup.kt
│ │ │ ├── NwscSim.kt
│ │ │ ├── ShoesMockup.kt
│ │ │ ├── SimulatedWatch.kt
│ │ │ └── WatchStateMockup.kt
│ └── res
│ │ ├── drawable
│ │ ├── chat_inputbar.xml
│ │ ├── chatbubble_incoming.xml
│ │ ├── chatbubble_outgoing.xml
│ │ ├── graphic_notification_permission.png
│ │ ├── ic_launcher_background.xml
│ │ ├── icon_alarm_disabled.xml
│ │ ├── icon_alarm_enabled.xml
│ │ ├── icon_bell_mute.xml
│ │ ├── icon_bell_ringing.xml
│ │ ├── icon_bell_unknown.xml
│ │ ├── icon_delete.xml
│ │ ├── icon_download.xml
│ │ ├── icon_health_audio.xml
│ │ ├── icon_health_cycletracking.xml
│ │ ├── icon_health_energy.xml
│ │ ├── icon_health_gps.xml
│ │ ├── icon_health_heart.xml
│ │ ├── icon_health_unknown.xml
│ │ ├── icon_health_walk.xml
│ │ ├── icon_health_workout.xml
│ │ ├── icon_key.xml
│ │ ├── icon_network_download.xml
│ │ ├── icon_network_packet.xml
│ │ ├── icon_network_upload.xml
│ │ ├── icon_paper_plane.xml
│ │ ├── icon_refresh.xml
│ │ ├── witch_monochrome.xml
│ │ └── witch_monochrome_lowdpi.xml
│ │ ├── layout
│ │ ├── activity_chat.xml
│ │ ├── activity_files.xml
│ │ ├── activity_health_log.xml
│ │ ├── activity_main.xml
│ │ ├── activity_map.xml
│ │ ├── activity_network_log.xml
│ │ ├── activity_process_firewall.xml
│ │ ├── activity_watch_state.xml
│ │ ├── alarms_item.xml
│ │ ├── apps_item.xml
│ │ ├── chatbubble.xml
│ │ ├── filelist_item.xml
│ │ ├── firewall_process_item.xml
│ │ ├── healthlog_item.xml
│ │ ├── netstats_item.xml
│ │ └── notification_consent.xml
│ │ ├── mipmap-anydpi-v26
│ │ └── ic_launcher.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher_foreground.png
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── net
│ └── rec0de
│ └── android
│ └── watchwitch
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── img
├── banner.png
├── screenshots-1.png
└── screenshots-2.png
└── settings.gradle
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: set up JDK 17
17 | uses: actions/setup-java@v4
18 | with:
19 | java-version: '17'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x gradlew
25 | - name: Build with Gradle
26 | run: ./gradlew build
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
5 | }
6 |
7 | android {
8 | namespace 'net.rec0de.android.watchwitch'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | applicationId "net.rec0de.android.watchwitch"
13 | minSdk 29
14 | targetSdk 33
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_11
29 | targetCompatibility JavaVersion.VERSION_11
30 | }
31 | kotlinOptions {
32 | jvmTarget = "11"
33 | }
34 | buildFeatures {
35 | viewBinding true
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation 'androidx.appcompat:appcompat:1.4.1'
41 | implementation 'com.google.android.material:material:1.5.0'
42 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
43 | implementation 'org.bouncycastle:bcpkix-jdk18on:1.73'
44 | implementation 'org.bouncycastle:bcprov-jdk18on:1.73'
45 | implementation 'androidx.datastore:datastore-preferences:1.0.0'
46 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
47 | implementation 'androidx.core:core-ktx:1.10.0'
48 | implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
49 | implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
50 | implementation 'androidx.legacy:legacy-support-v4:1.0.0'
51 | implementation 'androidx.recyclerview:recyclerview:1.3.1'
52 | testImplementation 'junit:junit:4.13.2'
53 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
55 | implementation 'com.google.android.flexbox:flexbox:3.0.0'
56 | implementation 'org.osmdroid:osmdroid-android:6.1.14' // gps track map view
57 | implementation("com.patrykandpatrick.vico:core:1.10.0") // ECG visualization
58 | implementation("com.patrykandpatrick.vico:views:1.10.0") // ECG visualization
59 | implementation("net.zetetic:sqlcipher-android:4.5.6@aar") // health database encryption
60 | implementation("androidx.sqlite:sqlite-ktx:2.2.0")
61 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/net/rec0de/android/watchwitch/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package net.rec0de.android.watchwitch
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("net.rec0de.android.watchwitch", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
19 |
22 |
25 |
28 |
31 |
34 |
37 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
60 |
69 |
75 |
76 |
77 |
78 |
79 |
82 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/java/net/rec0de/android/watchwitch/IdsLogger.kt:
--------------------------------------------------------------------------------
1 | package net.rec0de.android.watchwitch
2 |
3 | import android.content.Context
4 | import net.rec0de.android.watchwitch.bitmage.fromIndex
5 | import net.rec0de.android.watchwitch.bitmage.hex
6 | import java.io.BufferedOutputStream
7 | import java.text.SimpleDateFormat
8 | import java.util.Date
9 | import java.util.Locale
10 |
11 | object IdsLogger {
12 |
13 | private var output: BufferedOutputStream? = null
14 |
15 | fun init(context: Context) {
16 | val time = SimpleDateFormat("yyyy-MM-dd-HHmm", Locale.US).format(Date())
17 | val name = "session-$time.idslog"
18 | val file = context.openFileOutput(name, Context.MODE_PRIVATE)
19 | output = BufferedOutputStream(file)
20 | }
21 |
22 | fun logAlloy(send: Boolean, bytes: ByteArray) {
23 | val dir = if(send) "snd" else "rcv"
24 | output?.write("$dir utun ${bytes[0].toInt()} ${bytes.fromIndex(5).hex()}\n".toByteArray(Charsets.US_ASCII))
25 | }
26 |
27 | fun logControl(send: Boolean, bytes: ByteArray) {
28 | val dir = if(send) "snd" else "rcv"
29 | output?.write("$dir utunctrl ${bytes.hex()}\n".toByteArray(Charsets.US_ASCII))
30 | }
31 |
32 | fun flush() {
33 | output?.flush()
34 | output?.close()
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/net/rec0de/android/watchwitch/KeyReceiver.kt:
--------------------------------------------------------------------------------
1 | package net.rec0de.android.watchwitch
2 |
3 | import android.util.Base64
4 | import kotlinx.serialization.decodeFromString
5 | import kotlinx.serialization.json.Json
6 | import net.rec0de.android.watchwitch.bitmage.fromIndex
7 | import net.rec0de.android.watchwitch.decoders.aoverc.MPKeys
8 | import net.rec0de.android.watchwitch.servicehandlers.health.HealthSync
9 | import java.net.DatagramPacket
10 | import java.net.DatagramSocket
11 | import java.security.MessageDigest
12 |
13 |
14 | class KeyReceiver : Thread() {
15 | private val serverPort = 0x7777
16 | private val maxDatagramSize = 10000
17 |
18 | var socket: DatagramSocket? = null
19 |
20 | override fun run() {
21 | val lmessage = ByteArray(maxDatagramSize)
22 | val packet = DatagramPacket(lmessage, lmessage.size)
23 | try {
24 | socket = DatagramSocket(serverPort)
25 | Logger.log("listening for keys on :$serverPort", 1)
26 | while (true) {
27 | socket!!.receive(packet)
28 |
29 | Logger.log("received key message", 1)
30 |
31 | if (currentThread().isInterrupted)
32 | break
33 |
34 | val trimmed = packet.data.sliceArray(0 until packet.length)
35 |
36 | val plainKey = LongTermStorage.keyTransitSecret
37 | val md = MessageDigest.getInstance("SHA-256");
38 | md.update(plainKey)
39 | val keyBytes = md.digest()
40 |
41 | val nonce = trimmed.sliceArray(0 until 12)
42 | val ciphertext = trimmed.fromIndex(12)
43 |
44 | val json = try {
45 | Utils.chachaPolyDecrypt(keyBytes, nonce, byteArrayOf(), ciphertext).decodeToString()
46 | } catch (e: Exception) {
47 | Logger.log("failed to decrypt keys, do you have the right key?", 0)
48 | continue
49 | }
50 |
51 | val map = Json.decodeFromString