Installation

Requirements

  • Android 8.0+ (API 26)
  • No root required
  • Internet permission for key generation

Install APK

# Download from GitHub Releases
# Enable "Install unknown apps" for your browser
# Open APK and install

Permissions

Chameleon requests minimal permissions:

  • INTERNET — key generation, GitHub release checks
  • QUERY_ALL_PACKAGES — app drawer, launcher
  • BIND_APPWIDGET — home screen widgets
  • POST_NOTIFICATIONS — key expiry warnings
  • USE_BIOMETRIC — vault unlock

First Run

On first launch, Chameleon shows the Key screen. You have two options:

  1. Go to channel: Tap "Go to channel", subscribe, come back, tap "Create my key"
  2. Paste key: If you already have a key, tap "Paste key instead" and enter it

The key is device-bound — it won't work on another phone.

Key Flow

Channel Trip (The Only Real Gate)

YouTube exposes no subscription API. The only verifiable action is: you left for the channel and came back. The app creates a signed token (timestamp + random) valid for 30 minutes.

// Token format: timestamp:random
// Example: 1700000000000:123456
// Valid for 30 minutes

Key Generation

On return with valid token, the app derives the key:

nonce = random(1000, 9999)
deviceId = Settings.Secure.ANDROID_ID
key = SHA256(deviceId + ":" + nonce + ":" + packageName).substring(0, 8)
formatted = NNNN-HHHH-HHHHL (with checksum char)

Key Format

Keys follow the format: NNNN-HHHH-HHHHL

  • NNNN — 4-digit nonce (1000-9999), changes every generation
  • HHHH-HHHH — 8-char hash (first 8 chars of SHA-256)
  • L — checksum character (A-Z), derived from hash + nonce

Example: 1234-A1B2-C3D4E

Channel Trip Verification

The Only Real Gate

YouTube exposes no subscription state to apps. Any app claiming to verify your subscription is lying — it would require reading your Google account.

The channel trip checks one honest thing: you left for the channel and came back. The app creates a signed token (timestamp + random) valid for 30 minutes. When you return with a valid token, the app mints your key.

Token Format

timestamp:random
// Example: 1700000000000:123456
// Valid for 30 minutes from timestamp

Extensions

  • Each key lasts 3 days from creation
  • Can be extended +1 day, up to 7 times (max 10 days total)
  • Extension resets the expiry clock from current time
  • Extensions are tracked per-key, not per-device
  • After 7 extensions, a new channel trip is required

Vault Key Types

Type Purpose Sync
DEVICE App auth, device-bound No
SYNC Cross-device sync QR + passphrase
EMERGENCY One-time recovery No
API External service auth Manual
BACKUP Vault backup keys Manual
CUSTOM User-defined Manual

Access Policies

Each vault key can have an optional access policy:

  • requireBiometric — force biometric auth for decrypt
  • maxUses — limit decrypt operations (default unlimited)
  • allowedPackages — restrict which apps can request decrypt
  • expiresAt — auto-expire key after timestamp

Policies are enforced by VaultManager on every decrypt operation.

QR Sync Protocol

Device-to-device vault transfer via animated QR codes. No cloud, no server.

Format

{
  "version": 1,
  "timestamp": 1700000000000,
  "entries": [...],
  "checksum": "sha256(...)"
}

Animation

  • Split into frames ≤ 2KB each (QR capacity)
  • 10 FPS, looped with 2s pause between cycles
  • Frame header: frame index + total frames
  • Receiver reassembles, verifies checksum, imports

Icon Packs

  • Supports AdaptiveIcon format (foreground + background layers)
  • Bitmap fallback for legacy packs
  • Per-app icon override via long-press → Edit
  • Pack format: JSON manifest + PNG/SVG assets
  • Built-in default pack (stroke-based, consistent weight)

Gestures

GestureAction
Swipe upOpen app drawer
Swipe downNotification shade
Pinch inOverview (recents)
Long press homeWidget picker

Widgets

WidgetSizesFeatures
Key Status1×1, 2×1Status, expiry, extensions, quick extend
System Monitor2×2, 4×2CPU/RAM/battery sparklines, tap to open
Vault Quick1×1, 2×1Lock/unlock, emergency code, quick add

KeyManager API

// Get singleton
KeyManager.getInstance(context)

// Observe state
LiveData state = keyManager.state

// Create key (requires channel trip)
KeyData key = keyManager.createKey()

// Activate from string
boolean ok = keyManager.activateKey("NNNN-HHHH-HHHHL")

// Extend
boolean ok = keyManager.extendKey(1)

// Revoke all
boolean ok = keyManager.revokeKey()

// Mark channel trip (auto-called by KeyActivity)
boolean ok = keyManager.markChannelTrip()

// Device ID
String deviceId = keyManager.getDeviceId()

VaultManager API

// Get singleton
VaultManager.getInstance(context)

// Observe keys
LiveData> keys = vaultManager.keys
LiveData isUnlocked = vaultManager.isUnlocked

// Unlock (biometric/passphrase)
boolean ok = vaultManager.unlock(passphrase)

// Lock
vaultManager.lock()

// Add key
VaultKey key = vaultManager.addKey(name, value, KeyType.CUSTOM)

// Remove key
vaultManager.removeKey(id)

// Get key
VaultKey key = vaultManager.getKey(id)

// Export/Import
String json = vaultManager.exportVault(passphrase)
boolean ok = vaultManager.importVault(json, passphrase)

// QR Sync
String qrData = vaultManager.generateSyncQR()
boolean ok = vaultManager.importFromQR(qrData)

// Emergency code
String code = vaultManager.generateEmergencyCode()