357 lines
11 KiB
Markdown
357 lines
11 KiB
Markdown
# TextMe Web App Reverse Engineering Report
|
|
|
|
**Date:** January 28, 2026
|
|
**Target:** web.textme-app.com / api.textme-app.com
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
TextMe is a free calling/texting app with a web interface. The web app uses **QR code-based authentication** (similar to WhatsApp Web), making automated API access challenging without a valid mobile app session. The backend is Django-based with JWT authentication, running on AWS infrastructure.
|
|
|
|
**Viability Assessment:** ⚠️ **CHALLENGING** - Building an unofficial API wrapper is technically possible but requires:
|
|
1. A valid mobile app account to authenticate
|
|
2. Session token extraction from an authenticated session
|
|
3. Maintaining WebSocket connections for real-time events
|
|
|
|
---
|
|
|
|
## 1. Infrastructure Overview
|
|
|
|
### Domains Discovered
|
|
| Domain | Purpose |
|
|
|--------|---------|
|
|
| `web.textme-app.com` | Web client (AngularJS SPA) |
|
|
| `api.textme-app.com` | REST API server |
|
|
| `webauth.textme-app.com` | QR code WebSocket auth |
|
|
| `pubsub.textme-app.com` | Real-time messaging WebSocket |
|
|
| `sentry.go-text.me` | Error tracking |
|
|
| `ng.textme-app.com` | Offer wall / monetization |
|
|
| `go-text.me` | Marketing / assets |
|
|
|
|
### Tech Stack
|
|
- **Frontend:** AngularJS + Angular Material
|
|
- **Backend:** Django (evident from CSRF token naming)
|
|
- **Auth:** JWT tokens (`WWW-Authenticate: JWT realm="api"`)
|
|
- **Real-time:** NGINX PushStream + WebSockets
|
|
- **VoIP:** SIP.js for voice/video calls
|
|
- **Hosting:** AWS (ELB detected)
|
|
- **Analytics:** Google Analytics, Google Tag Manager
|
|
- **Error Tracking:** Sentry
|
|
|
|
---
|
|
|
|
## 2. Authentication System
|
|
|
|
### Primary Auth Method: QR Code Login
|
|
The web app uses a **WhatsApp Web-style QR code authentication**:
|
|
|
|
1. Web client generates a session UUID: `TMWEB-{uuid}`
|
|
2. Opens WebSocket: `wss://webauth.textme-app.com/ws/TMWEB-{uuid}/`
|
|
3. QR code encodes this session ID
|
|
4. Mobile app scans QR and authenticates the session
|
|
5. WebSocket receives auth token, web client stores JWT
|
|
|
|
### Secondary Auth Methods (in mobile app)
|
|
- Email/password via `/api/auth-token/`
|
|
- Social auth via `/api/auth-token-social/` (Google, Facebook)
|
|
- Token refresh via `/api/auth-token-refresh/`
|
|
|
|
### CSRF Protection
|
|
```
|
|
Cookie Name: csrftoken
|
|
Header Name: X-CSRFToken
|
|
```
|
|
|
|
### JWT Token Flow
|
|
1. After successful auth, JWT token issued
|
|
2. Token sent in `Authorization: JWT <token>` header
|
|
3. Token refresh endpoint available for session extension
|
|
|
|
---
|
|
|
|
## 3. API Endpoints Discovered
|
|
|
|
### Authentication
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/auth-token/` | POST | Username/password login |
|
|
| `/api/auth-token-social/` | POST | Social media OAuth login |
|
|
| `/api/auth-token-refresh/` | POST | Refresh JWT token |
|
|
| `/api/auth-token-voip-jwt/` | POST | Get VoIP-specific JWT |
|
|
| `/api/register-device/` | POST | Register mobile device |
|
|
|
|
### User Management
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/user-info/` | GET | Get current user info |
|
|
| `/api/users/` | GET | User lookup/search |
|
|
| `/api/profile-picture/` | POST | Upload profile picture |
|
|
| `/api/public-profile/` | GET | Public profile data |
|
|
| `/api/settings/` | GET/PUT | User settings |
|
|
| `/api/change-password/` | POST | Change password |
|
|
| `/api/reset-password/` | POST | Reset password |
|
|
|
|
### Phone Numbers
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/phone-number/available-countries/` | GET | List available countries |
|
|
| `/api/phone-number/choose/` | POST | Select a phone number |
|
|
|
|
### Messaging (Inferred)
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/event/conversation/` | WebSocket | Real-time conversation events |
|
|
| `/api/group/` | GET/POST | Group chat management |
|
|
| `/api/attachment/` | POST | Send attachments |
|
|
| `/api/attachment/get-upload-url/` | POST | Get presigned upload URL |
|
|
|
|
### Calls
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/call/` | POST | Initiate/manage calls |
|
|
|
|
### Store/Monetization
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/store/stickers/` | GET | Available stickers |
|
|
| `/api/store/stickers/packages/` | GET | Sticker packs |
|
|
| `/api/inapp/transactions/` | GET/POST | In-app purchases |
|
|
| `/api/pricing/` | GET | Pricing info |
|
|
|
|
### Misc
|
|
| Endpoint | Method | Description |
|
|
|----------|--------|-------------|
|
|
| `/api/web/signup/` | POST | Web signup |
|
|
| `/api/opengraph-resolver/` | POST | Resolve OpenGraph metadata |
|
|
|
|
---
|
|
|
|
## 4. Real-Time Communication
|
|
|
|
### PushStream (NGINX Module)
|
|
Used for real-time messaging with multiple transport options:
|
|
- WebSocket: `wss://pubsub.textme-app.com/ws/textme`
|
|
- EventSource: `/ev`
|
|
- Long Polling: `/lp`
|
|
- Stream: `/sub`
|
|
|
|
### WebSocket Authentication
|
|
```
|
|
wss://webauth.textme-app.com/ws/TMWEB-{session-uuid}/
|
|
```
|
|
|
|
### VoIP (SIP.js)
|
|
Voice/video calls use SIP protocol over WebSocket:
|
|
- Codecs: OPUS, PCMA, PCMU
|
|
- Transport: TCP/WebSocket
|
|
|
|
---
|
|
|
|
## 5. Security Measures Observed
|
|
|
|
### Anti-Bot Measures
|
|
1. **Google reCAPTCHA** - Integrated but may not be active for all endpoints
|
|
2. **403 Forbidden on suspicious requests** - POST to auth endpoints blocked
|
|
3. **AWS WAF** - Detected blocking patterns
|
|
|
|
### Rate Limiting
|
|
- Not explicitly tested, but expected on auth endpoints
|
|
|
|
### CORS
|
|
- Strict CORS policy, likely whitelisting only official domains
|
|
|
|
---
|
|
|
|
## 6. Mobile App Details
|
|
|
|
- **Package:** `com.textmeinc.textme3`
|
|
- **Facebook App ID:** `288600224541553`
|
|
- **Available on:** iOS, Android
|
|
- **Company:** TextMe, Inc.
|
|
|
|
---
|
|
|
|
## 7. Proof of Concept (Python)
|
|
|
|
```python
|
|
"""
|
|
TextMe Unofficial API Client - Proof of Concept
|
|
|
|
WARNING: This is for educational purposes only.
|
|
Requires a valid JWT token obtained from an authenticated session.
|
|
"""
|
|
|
|
import requests
|
|
import websocket
|
|
import json
|
|
import uuid
|
|
from typing import Optional, Dict, Any
|
|
|
|
class TextMeAPI:
|
|
BASE_URL = "https://api.textme-app.com"
|
|
WS_AUTH_URL = "wss://webauth.textme-app.com/ws"
|
|
WS_PUBSUB_URL = "wss://pubsub.textme-app.com/ws/textme"
|
|
|
|
def __init__(self, jwt_token: Optional[str] = None):
|
|
self.jwt_token = jwt_token
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
})
|
|
if jwt_token:
|
|
self.session.headers["Authorization"] = f"JWT {jwt_token}"
|
|
|
|
def get_qr_session_id(self) -> str:
|
|
"""Generate a session ID for QR code authentication."""
|
|
return f"TMWEB-{uuid.uuid4()}"
|
|
|
|
def wait_for_qr_auth(self, session_id: str) -> Optional[str]:
|
|
"""
|
|
Connect to WebSocket and wait for QR code scan.
|
|
Returns JWT token on successful auth.
|
|
"""
|
|
ws_url = f"{self.WS_AUTH_URL}/{session_id}/"
|
|
|
|
def on_message(ws, message):
|
|
data = json.loads(message)
|
|
if "token" in data:
|
|
self.jwt_token = data["token"]
|
|
self.session.headers["Authorization"] = f"JWT {self.jwt_token}"
|
|
ws.close()
|
|
return data["token"]
|
|
|
|
ws = websocket.WebSocketApp(
|
|
ws_url,
|
|
on_message=on_message,
|
|
)
|
|
ws.run_forever()
|
|
return self.jwt_token
|
|
|
|
def get_user_info(self) -> Dict[str, Any]:
|
|
"""Get current user information."""
|
|
response = self.session.get(f"{self.BASE_URL}/api/user-info/")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def get_settings(self) -> Dict[str, Any]:
|
|
"""Get user settings."""
|
|
response = self.session.get(f"{self.BASE_URL}/api/settings/")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def get_available_countries(self) -> Dict[str, Any]:
|
|
"""Get available countries for phone numbers."""
|
|
response = self.session.get(f"{self.BASE_URL}/api/phone-number/available-countries/")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
def refresh_token(self) -> str:
|
|
"""Refresh the JWT token."""
|
|
response = self.session.post(
|
|
f"{self.BASE_URL}/api/auth-token-refresh/",
|
|
json={"token": self.jwt_token}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
self.jwt_token = data["token"]
|
|
self.session.headers["Authorization"] = f"JWT {self.jwt_token}"
|
|
return self.jwt_token
|
|
|
|
def send_message(self, conversation_id: str, text: str) -> Dict[str, Any]:
|
|
"""
|
|
Send a text message.
|
|
NOTE: Exact endpoint structure needs verification from network capture.
|
|
"""
|
|
# This is speculative - actual endpoint needs verification
|
|
response = self.session.post(
|
|
f"{self.BASE_URL}/api/conversation/{conversation_id}/message/",
|
|
json={"text": text}
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
# Usage Example (requires manual token extraction)
|
|
if __name__ == "__main__":
|
|
# Method 1: QR Code Auth (needs mobile app scan)
|
|
api = TextMeAPI()
|
|
session_id = api.get_qr_session_id()
|
|
print(f"Scan this QR code with TextMe app:")
|
|
print(f"Session: {session_id}")
|
|
# token = api.wait_for_qr_auth(session_id)
|
|
|
|
# Method 2: Use extracted token
|
|
# token = "your_jwt_token_here"
|
|
# api = TextMeAPI(jwt_token=token)
|
|
# user = api.get_user_info()
|
|
# print(user)
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Obstacles for Unofficial API
|
|
|
|
### Critical Barriers
|
|
1. **QR Code Only Auth** - No username/password on web, requires mobile app
|
|
2. **AWS WAF** - Blocks suspicious POST requests
|
|
3. **CSRF Protection** - Django CSRF tokens required
|
|
4. **Device Registration** - May require registered device ID
|
|
|
|
### Potential Workarounds
|
|
1. **Mobile App Interception** - Proxy mobile traffic to capture tokens
|
|
2. **APK Reverse Engineering** - Decompile Android app for API secrets
|
|
3. **Selenium/Puppeteer** - Automate web login with real QR scan
|
|
4. **Browser Extension** - Inject into authenticated session
|
|
|
|
---
|
|
|
|
## 9. Recommendations
|
|
|
|
### For Building an Unofficial API
|
|
1. **Start with mobile app** - Proxy traffic with mitmproxy/Charles
|
|
2. **Extract tokens** - Get JWT from authenticated session
|
|
3. **Map conversation endpoints** - Need network capture for message send/receive
|
|
4. **Handle real-time** - Implement WebSocket client for pubsub
|
|
5. **Token management** - Implement refresh token rotation
|
|
|
|
### Legal Considerations
|
|
- Check TextMe Terms of Service before automated access
|
|
- Unofficial APIs may violate ToS
|
|
- Consider rate limiting to avoid account bans
|
|
|
|
---
|
|
|
|
## 10. Missing Information (Needs Further Research)
|
|
|
|
- [ ] Exact message send endpoint structure
|
|
- [ ] Conversation list endpoint
|
|
- [ ] Message history endpoint
|
|
- [ ] Contact management endpoints
|
|
- [ ] Exact JWT payload structure
|
|
- [ ] Device registration requirements
|
|
- [ ] Rate limit thresholds
|
|
|
|
---
|
|
|
|
## Appendix: Raw Findings
|
|
|
|
### Sentry DSN
|
|
```
|
|
https://87d11b479de34e519af45bc5a47d4a9e@sentry.go-text.me/6
|
|
```
|
|
|
|
### Facebook App ID
|
|
```
|
|
288600224541553
|
|
```
|
|
|
|
### Detected Server Headers
|
|
```
|
|
Server: nginx
|
|
Server: awselb/2.0
|
|
WWW-Authenticate: JWT realm="api"
|
|
```
|