Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions HOW_IT_WORKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# How It Works: The "Echo" Tracking Technique 🦇

This tool allows you to see when a user is **active** on WhatsApp or Signal without them knowing, and without them actually opening your chat.

It works using a digital "Side-Channel Attack" based on **Physics and Network Latency**.

## The Concept: "The Digital Pulse"

Imagine you are throwing a tennis ball against a wall:
1. **If you are close to the wall**, the ball comes back instantly.
2. **If you are far away**, the ball takes longer to return.

In the world of messaging apps:
* **"Close to the Wall"** = The user has the app **OPEN** on their screen. Their phone is awake, the CPU is running at full speed, and the network connection is active.
* **"Far Away"** = The phone is in their pocket (Standby). The screen is off, the processor is sleeping to save battery, and the WiFi radio is in low-power mode.

## The Trick: Measuring Speed (RTT) ⏱️

We don't hack their phone. We just measure **speed**.

1. **The Probe**: The tool sends a specially crafted, invisible message to the target.
* *Technique:* It sends a "Delete Request" for a message that doesn't exist. This is invalid, but the phone **still has to process it** to say "I can't find that message".
2. **The Echo**: The target's phone receives this request and automatically sends back a tiny receipt (Ack).
3. **The Measurement**: We measure the time from **Send** to **Receive** (Round-Trip Time).

| User State | Phone Status | Reaction Speed |
| :--- | :--- | :--- |
| **🟢 Active** | Screen On, App Open, CPU High | **Fast (< 100ms)** |
| **🟡 Standby** | Screen Off, Low Power Mode | **Slow (> 500ms)** |

## The AI Brain: Adaptive Prediction 🧠

Every network is different. 100ms might be "Fast" on WiFi but "Slow" on 4G. Hardcoded numbers don't work.

This tool uses a machine learning algorithm called **K-Means Clustering**:

1. **Collect Data**: It fires probes every few seconds and collects thousands of speed measurements.
2. **Find Patterns**: It automatically notices two distinct groups of numbers:
* "There is a cluster of dots around 80ms." (Active)
* "There is a cluster of dots around 1200ms." (Standby)
3. **Draw the Line**: It calculates a dynamic **Threshold** right in the middle.

<div align="center">

```mermaid
graph LR
A[Probe Sent] --> B{Target Phone}
B -->|Fast Response| C(Online Cluster)
B -->|Slow Response| D(Standby Cluster)
C --> E[AI Analysis]
D --> E
E --> F[Status: 🟢 ONLINE]
```

</div>

## Is it detectable? 🕵️‍♂️

* **For the Target**: No. The app suppresses notifications for these specific technical messages. Their phone doesn't ring or vibrate.
* **For WhatsApp/Signal**: Maybe. If you probe too fast (e.g., 10 times a second), their servers might block you for spamming. That's why we use random intervals (Jitter) to look human.

## Summary

We are simply checking **"How fast can your phone answer a question?"**
* **Fast Answer** = You are likely holding the phone.
* **Slow Answer** = The phone is likely in your pocket.
67 changes: 51 additions & 16 deletions client/src/components/ContactCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { Square, Activity, Wifi, Smartphone, Monitor, MessageCircle } from 'lucide-react';
import { Square, Activity, Wifi, Smartphone, Monitor, MessageCircle, BarChart2, ShieldCheck, Zap } from 'lucide-react';
import clsx from 'clsx';

type Platform = 'whatsapp' | 'signal';

interface TrackerData {
rtt: number;
avg: number;
median: number;
// New metrics
onlineAvg: number;
standbyAvg: number;
threshold: number;
confidence: number;
state: string;
timestamp: number;
}
Expand Down Expand Up @@ -48,14 +51,20 @@ export function ContactCard({
}: ContactCardProps) {
const lastData = data[data.length - 1];
const currentStatus = devices.length > 0
? (devices.find(d => d.state === 'OFFLINE')?.state ||
devices.find(d => d.state.includes('Online'))?.state ||
? (devices.find(d => d.state.includes('Online'))?.state ||
devices.find(d => d.state === 'Standby')?.state ||
devices[0].state)
: 'Unknown';

// Blur phone number in privacy mode
const blurredNumber = privacyMode ? displayNumber.replace(/\d/g, '•') : displayNumber;

// Confidence as percentage
const confidencePct = lastData?.confidence ? Math.round(lastData.confidence * 100) : 0;

// Determine confidence color
const confidenceColor = confidencePct > 80 ? 'text-green-600' : confidencePct > 50 ? 'text-yellow-600' : 'text-red-500';

return (
<div className="bg-gradient-to-br from-white to-gray-50 rounded-xl shadow-lg border border-gray-200 overflow-hidden">
{/* Header with Stop Button */}
Expand Down Expand Up @@ -131,6 +140,12 @@ export function ContactCard({
<span className="flex items-center gap-1"><Smartphone size={16} /> Devices</span>
<span className="font-medium">{deviceCount || 0}</span>
</div>
{lastData?.confidence !== undefined && (
<div className="flex justify-between items-center text-sm text-gray-600">
<span className="flex items-center gap-1"><ShieldCheck size={16} /> Confidence</span>
<span className={clsx("font-bold", confidenceColor)}>{confidencePct}%</span>
</div>
)}
</div>

{/* Device List */}
Expand Down Expand Up @@ -163,23 +178,36 @@ export function ContactCard({
<div className="md:col-span-2 space-y-6">
{/* Metrics Grid */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<div className="text-sm text-gray-500 mb-1 flex items-center gap-1"><Activity size={16} /> Current Avg RTT</div>
<div className="text-2xl font-bold text-gray-900">{lastData?.avg.toFixed(0) || '-'} ms</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
<div className="text-sm text-gray-500 mb-1 flex items-center gap-1"><Activity size={16} /> Current RTT</div>
<div className="text-2xl font-bold text-gray-900">{lastData?.rtt || '-'} <span className="text-sm font-normal text-gray-500">ms</span></div>
</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<div className="text-sm text-gray-500 mb-1">Median (50)</div>
<div className="text-2xl font-bold text-gray-900">{lastData?.median.toFixed(0) || '-'} ms</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
<div className="text-sm text-gray-500 mb-1 flex items-center gap-1"><BarChart2 size={16} /> Online/Standby Avg</div>
<div className="flex items-baseline gap-2">
<span className="text-lg font-bold text-green-600">{lastData?.onlineAvg?.toFixed(0) || '-'}</span>
<span className="text-gray-400">/</span>
<span className="text-lg font-bold text-yellow-600">{lastData?.standbyAvg?.toFixed(0) || '-'}</span>
<span className="text-sm text-gray-500">ms</span>
</div>
</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200">
<div className="text-sm text-gray-500 mb-1">Threshold</div>
<div className="text-2xl font-bold text-blue-600">{lastData?.threshold.toFixed(0) || '-'} ms</div>
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
<div className="text-sm text-gray-500 mb-1 flex items-center gap-1"><Zap size={16} /> Threshold</div>
<div className="text-2xl font-bold text-blue-600">{lastData?.threshold?.toFixed(0) || '-'} <span className="text-sm font-normal text-gray-500">ms</span></div>
</div>
</div>

{/* Chart */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 h-[300px]">
<h5 className="text-sm font-medium text-gray-500 mb-4">RTT History & Threshold</h5>
<div className="flex justify-between items-center mb-4">
<h5 className="text-sm font-medium text-gray-500">RTT History & Adaptive Clustering</h5>
<div className="flex gap-4 text-xs">
<div className="flex items-center gap-1"><div className="w-3 h-0.5 bg-blue-500"></div>RTT</div>
<div className="flex items-center gap-1"><div className="w-3 h-0.5 bg-red-500 border-dashed border-t"></div>Threshold</div>
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-green-500"></div>Online Cluster</div>
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-yellow-500"></div>Standby Cluster</div>
</div>
</div>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
Expand All @@ -189,8 +217,15 @@ export function ContactCard({
labelFormatter={(t: number) => new Date(t).toLocaleTimeString()}
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
/>
<Line type="monotone" dataKey="avg" stroke="#3b82f6" strokeWidth={2} dot={false} name="Avg RTT" isAnimationActive={false} />
<Line type="step" dataKey="threshold" stroke="#ef4444" strokeDasharray="5 5" dot={false} name="Threshold" isAnimationActive={false} />
{/* Main RTT Line */}
<Line type="monotone" dataKey="rtt" stroke="#3b82f6" strokeWidth={2} dot={false} name="RTT" isAnimationActive={false} />

{/* Threshold Line */}
<Line type="step" dataKey="threshold" stroke="#ef4444" strokeDasharray="5 5" strokeWidth={2} dot={false} name="Threshold" isAnimationActive={false} />

{/* Cluster Centers (Visualized as faint lines or dots) */}
<Line type="monotone" dataKey="onlineAvg" stroke="#22c55e" strokeOpacity={0.5} strokeWidth={1} dot={false} name="Online Target" isAnimationActive={false} />
<Line type="monotone" dataKey="standbyAvg" stroke="#eab308" strokeOpacity={0.5} strokeWidth={1} dot={false} name="Standby Target" isAnimationActive={false} />
</LineChart>
</ResponsiveContainer>
</div>
Expand Down
Loading