Explorer
KNOW-PAT-065

Signature d'annonce tracker BitTorrent - Authentification et rate limiting

Domaine
cybersecu
Type
pattern
Priorité
P2

Parent : [[INDEX-CYBERSECU]]

Signature d'annonce tracker BitTorrent - Authentification et rate limiting

Problème

Comment authentifier les announces tracker BitTorrent pour empêcher les utilisateurs de partager leur passkey et pour limiter le nombre de téléchargements ? L'archive Ygg contient un système de signature d'annonce avec announceSignatureKey et des restrictions de téléchargement (timer, limite quotidienne, rank-based).

Contexte archive Ygg

  • Fichier : 01_SOURCE_CODE/02b_express_api/config.js
  • Fichier : 01_SOURCE_CODE/02b_express_api/download.controller.js
  • Fichier : 01_SOURCE_CODE/02b_express_api/download.service.js
  • Key : ANNOUNCE_SIGNATURE_KEY=xf7mTOvmX2tHeXDz2CkkmJddUdia8wu6 (hardcoded)
  • Restrictions : downloadTimerMinUserId=1000000, dailyDownloadLimit=5, downloadTimerSeconds=30

Technique de l'archive Ygg

1. Signature d'annonce tracker

// config.js
announceSignatureKey: process.env.ANNOUNCE_SIGNATURE_KEY || 'xf7mTOvmX2tHeXDz2CkkmJddUdia8wu6',

Le tracker signe l'URL d'annonce pour le fichier .torrent avec une clé secrète. Cela empêche la modification de l'URL (passkey fraud).

2. Restrictions de téléchargement

// download.controller.js
const isRestricted = downloadService.isRestrictedUser(
  userId,
  user.rank,
  premiumUntil,
  isOwnTorrent,
  minUserId,        // 1000000 = nouveaux users
  exemptRanks       // [1,2,3] = staff
);

// Timer anti-bot (30 secondes)
if (isRestricted && token) {
  await downloadService.verifyDownloadTimer(torrentId, token, sessionTimerData);
}

// Limite quotidienne (5 téléchargements/jour)
if (isRestricted && dailyLimit > 0) {
  await downloadService.checkDailyLimit(userId, dailyLimit);
}

3. Passkey dans le torrent

// download.service.js — Génération du .torrent signé
const trackerHost = user.tracker_id === 1 
  ? config.secondaryTrackerHost 
  : config.firstTrackerHost;

const result = await downloadService.downloadTorrent(
  torrentId,
  userId,
  user.torrent_pass,         // Passkey unique par utilisateur
  trackerHost,
  config.announceSignatureKey  // Signature de l'URL
);

Solution protectif — Tracker announce sécurisé

1. Signature HMAC de l'annonce

// announceSignature.js — Signature sécurisée
const crypto = require('crypto');

class AnnounceSigner {
  constructor(secretKey) {
    if (!secretKey || secretKey.length < 32) {
      throw new Error('Secret key must be at least 32 characters');
    }
    this.secretKey = secretKey;
  }

  // Signer une URL d'annonce
  signAnnounceUrl(baseUrl, userId, passkey, torrentHash, expiresAt) {
    const data = `${userId}:${passkey}:${torrentHash}:${expiresAt}`;
    const signature = crypto
      .createHmac('sha256', this.secretKey)
      .update(data)
      .digest('hex');
    
    return {
      url: `${baseUrl}/announce?user_id=${userId}&passkey=${passkey}&hash=${torrentHash}&expires=${expiresAt}&sig=${signature}`,
      expiresAt
    };
  }

  // Vérifier une signature
  verifyAnnounceUrl(userId, passkey, torrentHash, expiresAt, signature) {
    // Vérifier expiration
    if (Date.now() > expiresAt) {
      return { valid: false, reason: 'expired' };
    }

    const data = `${userId}:${passkey}:${torrentHash}:${expiresAt}`;
    const expected = crypto
      .createHmac('sha256', this.secretKey)
      .update(data)
      .digest('hex');

    // Timing-safe comparison
    if (!crypto.timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expected, 'hex')
    )) {
      return { valid: false, reason: 'invalid_signature' };
    }

    return { valid: true };
  }
}

2. Passkey rotation

// passkeyManager.js — Gestion des passkeys
class PasskeyManager {
  async generatePasskey(userId) {
    // Passkey unique et non prévisible
    const passkey = crypto.randomBytes(16).toString('hex');
    const version = await this.getNextVersion(userId);
    
    await db.query(
      'UPDATE users SET torrent_pass = ?, torrent_pass_version = ? WHERE id = ?',
      [passkey, version, userId]
    );
    
    // Invalider les anciens torrents signés
    await db.query(
      'UPDATE user_downloads SET invalidated = 1 WHERE user_id = ? AND passkey_version < ?',
      [userId, version]
    );
    
    return passkey;
  }

  async revokePasskey(userId, reason) {
    const oldPasskey = await this.getPasskey(userId);
    
    // Générer nouveau passkey
    const newPasskey = await this.generatePasskey(userId);
    
    // Log
    auditLog.warn(`Passkey revoked`, {
      userId,
      oldPasskey: oldPasskey?.slice(0, 8) + '...',
      reason
    });
    
    return newPasskey;
  }

  // Détection de partage de passkey
  async detectPasskeySharing(passkey) {
    const [stats] = await db.query(`
      SELECT COUNT(DISTINCT ip) as unique_ips, 
             COUNT(*) as total_announces
      FROM tracker_announces 
      WHERE passkey = ? 
        AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
    `, [passkey]);
    
    if (stats[0].unique_ips > 5) {
      return {
        suspected: true,
        uniqueIps: stats[0].unique_ips,
        totalAnnounces: stats[0].total_announces
      };
    }
    
    return { suspected: false };
  }
}

3. Système de restrictions avancé

// downloadRestrictions.js — Restrictions de téléchargement
class DownloadRestrictions {
  constructor(config) {
    this.config = config;
  }

  async checkRestrictions(user, torrent) {
    const checks = [];

    // 1. Timer anti-bot (nouveaux utilisateurs)
    if (this.isNewUser(user.id) && !this.isExempt(user.rank)) {
      checks.push(this.checkDownloadTimer(user.id, torrent.id));
    }

    // 2. Limite quotidienne
    if (!this.isExempt(user.rank)) {
      checks.push(this.checkDailyLimit(user.id));
    }

    // 3. Ratio minimum (anti-hit-and-run)
    checks.push(this.checkRatio(user));

    // 4. Torrent lock (si torrent verrouillé)
    checks.push(this.checkTorrentState(torrent));

    // 5. Vérifier si utilisateur banni/mute
    checks.push(this.checkUserStatus(user));

    const results = await Promise.all(checks);
    const failures = results.filter(r => !r.passed);

    if (failures.length > 0) {
      throw new DownloadRestrictedError(failures.map(f => f.reason));
    }
  }

  async checkDailyLimit(userId) {
    const today = new Date().toISOString().split('T')[0];
    const [rows] = await db.query(
      'SELECT download_count FROM user_download_limits WHERE user_id = ? AND reset_date = ?',
      [userId, today]
    );

    const count = rows.length > 0 ? rows[0].download_count : 0;
    const limit = this.config.dailyDownloadLimit;

    if (count >= limit) {
      return {
        passed: false,
        reason: `Limite quotidienne atteinte (${count}/${limit})`
      };
    }

    return { passed: true };
  }

  checkRatio(user) {
    const uploaded = user.bytes_uploaded || 1;
    const downloaded = user.bytes_downloaded || 1;
    const ratio = uploaded / downloaded;

    if (ratio < 0.5 && downloaded > 10 * 1024 * 1024 * 1024) { // < 0.5 et > 10GB
      return {
        passed: false,
        reason: `Ratio trop faible (${ratio.toFixed(2)}). Minimum: 0.5`
      };
    }

    return { passed: true };
  }
}

4. Multi-tracker switching

// trackerManager.js — Gestion multi-tracker
class TrackerManager {
  constructor(config) {
    this.trackers = {
      primary: {
        host: config.firstTrackerHost,
        weight: 70,
        maxConnections: 10000
      },
      secondary: {
        host: config.secondaryTrackerHost,
        weight: 30,
        maxConnections: 5000
      }
    };
  }

  getTrackerForUser(user) {
    // Staff et premium sur le tracker principal
    if ([1, 2, 3].includes(user.rank) || user.is_donator) {
      return this.trackers.primary;
    }

    // Distribution par tracker_id
    if (user.tracker_id === 1) {
      return this.trackers.secondary;
    }

    return this.trackers.primary;
  }

  // Health check des trackers
  async checkTrackerHealth(tracker) {
    try {
      const response = await fetch(`https://${tracker.host}/health`);
      return response.ok;
    } catch {
      return false;
    }
  }
}

Checklist de validation

  • Passkey unique par utilisateur (16+ bytes hex)
  • Passkey rotation possible (versioning)
  • Signature HMAC-SHA256 des URLs d'annonce
  • Expiration des signatures (24h max)
  • Timing-safe comparison des signatures
  • Détection de partage de passkey (>5 IPs/24h)
  • Timer anti-bot pour nouveaux utilisateurs
  • Limite quotidienne de téléchargements
  • Vérification du ratio minimum
  • Multi-tracker avec health check
  • Revocation automatique en cas d'abus
  • Audit log de chaque announce et téléchargement
  • Pas de clé de signature hardcodée (env variable)

Références

  • Archive Ygg : 01_SOURCE_CODE/02b_express_api/
  • BitTorrent Protocol : BEP-12 (Multi-Tracker)
  • BitTorrent Protocol : BEP-15 (UDP Tracker)