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)