0. Résumé exécutif
Ce dossier clarifie quatre identifiants qui sont très souvent confondus :
Client ID eD2K : 32 bits, fourni par le serveur
UserHash : 128 bits, persistant, propre au client
Kad ID : 128 bits, persistant, position dans Kad
File Hash : 128 bits, identité du contenu
le système de crédits n’est pas un système global et le UserHash n’est pas une preuve d’identité.
Le UserHash sert de clé persistante pour retrouver une entrée locale de crédit.
Le Secure User Identification ajoute la preuve cryptographique que le pair possède bien la clé privée associée à cette identité.
- le défi SecureIdent est toujours généré avec
rand()+1dans la branche 0.72a, exactement comme dans 0.50a ; on peut le remplacer localement par un générateur cryptographiquement sûr sans modifier le protocole ; - le système de crédits est une politique locale, puisque eMule et aMule appliquent des calculs légèrement différents tout en restant interopérables ;
- l’utilisateur ne dispose aujourd’hui d’aucune vision claire de la santé de son identité locale (
preferences.dat,cryptkey.dat,clients.met,preferencesKad.dat).
Un futur module Identity Health est donc une piste réaliste et compatible.
1. Client ID eD2K
Le dossier 02 a déjà couvert le Client ID.
Rappel :
taille : 32 bits
source : serveur eD2K
usage : session / joignabilité
La vieille spécification eDonkey expose déjà le changement d’ID via le serveur.
Le Protocol Guide rappelle qu’un Client ID n’est valable que pour une session client-serveur, même si un HighID conserve normalement la même valeur tant que l’IPv4 publique reste identique.
Conclusion :
le Client ID n’est pas une identité durable d’utilisateur.
2. UserHash : identité persistante eMule
2.1 Taille et stockage
Preferences.cpp
uchar CPreferences::userhash[MDX_DIGEST_SIZE];MDX_DIGEST_SIZE = 16.
Le UserHash est donc une valeur de :
128 bits
16 octets
Il est enregistré dans :
preferences.dat
L’aide officielle confirme que preferences.dat sauvegarde le UserHash utilisé pour identifier le client dans le réseau, notamment pour les crédits et les amis.
2.2 Ce n’est pas réellement un « hash »
Nom historique trompeur.
Preferences.cpp:1484-1493
while (isbadhash(userhash)) {
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(userhash, sizeof userhash);
}
userhash[5] = 14;
userhash[14] = 111;Il n’est donc pas obtenu en appliquant MD4 à une donnée personnelle.
C’est essentiellement :
128 bits aléatoires
+ deux octets marqueurs eMule
La documentation de 2005 décrit elle aussi l’User ID comme 16 octets aléatoires avec les sixième et quinzième octets fixés respectivement à 14 et 111.
Conséquence éditoriale
Sur le site, nous devons conserver le terme historique UserHash, mais expliquer qu’il s’agit plutôt d’un identifiant aléatoire persistant.
2.3 Marqueurs historiques
BaseClient.cpp:1768-1776
[5] = 13, [14] = 110 -> SO_OLDEMULE
[5] = 14, [14] = 111 -> SO_EMULE
[5] = 'M', [14] = 'L' -> SO_MLDONKEYLe UserHash transporte donc historiquement un petit marqueur de famille de client.
Ce n’est pas une authentification.
3. Quand le UserHash est reçu
BaseClient.cpp:359-375
Lors du hello client-à-client :
data.ReadHash16(m_achUserHash);
m_nUserIDHybrid = data.ReadUInt32();
nUserPort = data.ReadUInt16();Un même hello fournit donc :
UserHash 128 bits
Client ID 32 bits
Port TCP
Les deux identités coexistent directement dans le protocole.
Association aux crédits
BaseClient.cpp:611
CClientCredits *pFoundCredits =
theApp.clientcredits->GetCredit(m_achUserHash);Le UserHash est directement utilisé comme clé de recherche de l’entrée de crédit.
4. Kad ID
4.1 Génération
kademlia/kademlia/Prefs.cpp:64-77
m_uClientID.SetValueRandom();
...
m_uClientHash = CUInt128(thePrefs.GetUserHash());Puis le code tente de charger le Kad ID depuis :
preferencesKad.dat
Prefs.cpp:104-109
file.ReadUInt128(m_uClientID);
if (m_uClientID == 0)
m_uClientID.SetValueRandom();Ainsi :
Kad ID = valeur 128 bits propre à Kad
UserHash = valeur 128 bits eMule distincte
Le code conserve aussi une copie du UserHash dans m_uClientHash, mais cela ne rend pas les deux identifiants équivalents.
4.2 aMule confirme
Le corpus aMule applique le même modèle :
m_clientID = GetRandomUint128();
m_clientHash.SetValueBE(thePrefs::GetUserHash().GetHash());et recharge m_clientID depuis preferencesKad.dat.
Confirmation indépendante de l’architecture.
5. Le fichier hash : quatrième notion
Le protocole eDonkey v0.6 définit :
<File hash> ::= HASH
HASH ::= 16 bytes MD4 digest
Le fichier est identifié par le contenu, tandis que UserHash et Kad ID identifient des entités clientes/nœuds.
Il est essentiel que le site ne mélange jamais :
UserHash ≠ File Hash ≠ Kad ID
même s’ils sont tous représentables sur 16 octets.
6. Architecture réelle du système de crédits
6.1 Structure locale
ClientCredits.h
Chaque entrée contient :
abyKey[16] // UserHash distant
nUploaded...
nDownloaded...
tLastSeen
nKeySize
abySecureIdent[80] // clé publique distanteL’entrée de crédit est donc locale et indexée sur le UserHash du pair.
6.2 Sémantique des compteurs
Attention aux noms vus du point de vue du programme local :
GetDownloadedTotal()
= données que NOUS avons téléchargées depuis ce pair
= données que CE PAIR nous a uploadées
GetUploadedTotal()
= données que NOUS avons uploadées vers ce pair
= données que CE PAIR a téléchargées depuis nous
C’est la raison pour laquelle la formule officielle, décrite du point de vue du pair bénéficiaire, semble parfois inversée par rapport aux noms de fonctions.
6.3 Crédits non globaux
L’aide officielle dit explicitement :
- les crédits sont entre deux clients spécifiques ;
- le client qui reçoit les données récompense celui qui lui envoie ;
- vos propres crédits sont sauvegardés par le client qui vous les doit ;
- ils ne peuvent donc pas être « affichés globalement ».
Il n’existe aucun compteur central.
7. clients.met
ClientCredits.cpp
Nom :
CLIENTS_MET_FILENAME = "clients.met"Le fichier contient les entrées de crédits locales.
Sauvegarde
ClientCredits.cpp:283-286
if (GetTickCount() >= m_nLastSaved + MIN2MS(13))
SaveList();Sauvegarde périodique d’environ :
13 minutes
Backup
Au chargement, eMule entretient aussi :
clients.met.bak
en évitant d’écraser un backup plus gros par un fichier potentiellement endommagé.
Expiration
ClientCredits.cpp:207
const time_t tExpired =
time(NULL) - DAY2S(150);Une entrée non revue depuis environ :
150 jours
est ignorée lors du chargement.
Cette valeur était déjà présente dans eMule 0.50a.
Donc :
150 jours est une politique historique du client, pas une contrainte du protocole.
8. Calcul du multiplicateur de crédit eMule
8.1 Conditions d’identité
ClientCredits.cpp:97-107
Si SecureIdent est disponible mais que le pair est :
IS_IDFAILED
IS_IDBADGUY
IS_IDNEEDED
alors :
return 1.0f;Donc aucun bonus de crédit tant que l’identité sécurisée requise n’est pas validée.
Les fonctions AddDownloaded et AddUploaded appliquent une logique similaire et peuvent refuser de mettre à jour les compteurs.
8.2 Seuil minimal
eMule 0.72a :
if (GetDownloadedTotal() < 1048576)
return 1.0f;Le pair doit donc nous avoir fourni au moins :
1 MiB
avant qu’un bonus > 1 soit possible.
8.3 Formule 1
result = GetDownloadedTotal() * 2
/ GetUploadedTotal();Du point de vue du pair :
2 × données qu’il nous a envoyées
/ données qu’il a reçues de nous
Si nous ne lui avons encore rien envoyé :
result = 10
8.4 Formule 2
sqrt(GetDownloadedTotal()/1MiB + 2)Elle empêche d’obtenir immédiatement un énorme multiplicateur après un échange minuscule.
8.5 Formule 3 — souvent absente de la documentation publique
eMule 0.50a ET 0.72a possèdent une troisième limitation :
if (GetDownloadedTotal() < 9646899)
result3 =
(GetDownloadedTotal() - 1048576)
/ 8598323
* 2.34
+ 1;
else
result3 = 10;Le commentaire indique :
1 MB -> ~1.01
9.2MB -> ~3.34
Puis :
result = min(result, min(result2, result3));Enfin :
minimum = 1
maximum = 10
Découverte documentaire importante
L’aide officielle eMule et le guide de protocole résument essentiellement deux formules.
Le code officiel 0.50a contient déjà trois limites.
La documentation publique est donc simplifiée/incomplète sur ce point.
pour les détails comportementaux, le source fait autorité sur la vieille documentation.
9. aMule confirme que le calcul des crédits est une politique locale
Dans le corpus aMule :
if (GetDownloadedTotal() < 1000000)
return 1.0f;Puis :
result = 2 * downloaded / uploaded
result2 = sqrt(downloaded / 1048576 + 2)
result = min(result, result2)
result borné entre 1 et 10Pas de troisième rampe linéaire result3 observée dans cette implémentation.
Différence supplémentaire :
eMule : 1 048 576 octets
aMule : 1 000 000 octets
Les deux clients restent pourtant interopérables.
10. Application du crédit à la file d’upload
UploadClient.cpp:176-229
Le score local utilise notamment :
temps d’attente
× multiplicateur de crédit
× priorité du fichier
Code :
if (thePrefs.UseCreditSystem())
fBaseValue *= credits->GetScoreRatio(GetIP());
fBaseValue *= GetFilePrioAsNumber() / 10;Le système de crédits :
- ne contrôle pas le débit brut du slot ;
- modifie surtout la vitesse de progression dans la file.
Il existe ensuite d’autres règles : bannissement, friend slot, ancien client, LowID hors connexion, etc.
11. Secure User Identification : architecture
11.1 Motivation
Un UserHash est envoyé en clair lors du hello client.
Sans protection, un client malveillant pourrait recopier le UserHash d’un utilisateur ayant des crédits.
SecureIdent transforme donc :
« je connais cet identifiant »
en :
« je peux prouver que je possède la clé privée
associée à cet identifiant »
12. Clé privée
ClientCredits.cpp:348-409
Fichier :
cryptkey.dat
Si absent ou vide :
CreateKeyPair();Génération :
AutoSeededRandomPool rng;
InvertibleRSAFunction privkey;
privkey.Initialize(rng, RSAKEYSIZE);avec :
RSAKEYSIZE = 384La clé privée est encodée puis enregistrée dans cryptkey.dat.
13. Clé publique
Au démarrage :
RSASSA_PKCS1v15_SHA_Verifier pubkey(*m_pSignkey);
pubkey.GetMaterial().Save(...)eMule dérive sa clé publique de la clé privée.
La clé publique du pair distant est stockée dans l’entrée clients.met :
nKeySize
abySecureIdent[MAXPUBKEYSIZE]MAXPUBKEYSIZE = 80.
14. Les trois opcodes SecureIdent
Opcodes.h
0x85 OP_PUBLICKEY
0x86 OP_SIGNATURE
0x87 OP_SECIDENTSTATE
Ils existaient déjà dans eMule 0.50a et sont toujours présents dans 0.72a.
15. Challenge-response
BaseClient.cpp:1979-2005
eMule décide si :
clé + signature nécessaires
ou
signature seulement
Puis crée un challenge 32 bits et envoie :
OP_SECIDENTSTATE
<state 1>
<challenge 4>
Le pair signe ensuite un ensemble qui contient notamment :
- notre clé publique ;
- le challenge ;
- éventuellement une IP et un indicateur de type de challenge pour SecureIdent v2.
La signature est vérifiée à l’aide de la clé publique du pair.
16. SecureIdent v1 / v2
BaseClient.cpp:1860-1877
Le client choisit v1 par défaut sauf lorsque le pair impose v2.
v2 ajoute une notion d’IP au contenu signé.
ClientCredits.cpp:470-495
Le vérificateur traite :
CRYPT_CIP_LOCALCLIENT
CRYPT_CIP_REMOTECLIENT
CRYPT_CIP_NONECLIENT
Le but est de mieux lier la preuve à la situation de connexion et de réduire la réutilisation d’une signature dans un autre contexte.
18. GetRandomUInt32() n’est pas une solution suffisante
La branche possède :
uint32 GetRandomUInt32()mais son implémentation combine plusieurs appels à :
rand()et reseed ensuite srand() avec GetTickCount().
Ce n’est donc pas un CSPRNG.
Pour SecureIdent, remplacer simplement :
rand()+1par :
GetRandomUInt32()améliorerait éventuellement l’espace de sortie, mais ne constituerait pas un durcissement cryptographique satisfaisant.
La bonne solution est d’utiliser le générateur cryptographique déjà présent.
19. RSA 384 et SHA-1
La classe utilisée est :
RSASSA_PKCS1v15_SHA_Signer
RSASSA_PKCS1v15_SHA_Verifieravec :
RSA 384 bits
Il s’agit de paramètres cryptographiques historiques très faibles au regard des standards modernes.
Mais attention
Ils sont intégrés au protocole d’interopérabilité.
Changer unilatéralement :
- la taille RSA ;
- le format de clé ;
- l’algorithme de signature ;
- la taille maximale de clé publique ;
Stratégie raisonnable
Court terme : - durcir ce qui n’affecte pas le wire format ; - vérifier les entrées ; - conserver les protections anti-flood ; - utiliser un challenge CSPRNG.
Long terme : étudier éventuellement une extension SecureIdent nouvelle négociée entre clients compatibles, tout en maintenant v1/v2 pour le réseau existant.
20. Perte de cryptkey.dat
L’aide officielle dit clairement :
cryptkey.datcontient la clé privée ;- perdre cette clé empêche de prouver les crédits déjà connus par les pairs ;
- conserver l’ancien UserHash avec une nouvelle clé provoque une incohérence ;
- en cas de perte définitive, il faut aussi repartir avec un nouveau UserHash.
Le code confirme cela.
Lorsqu’une nouvelle clé publique est attachée pour la première fois à une entrée jusque-là non sécurisée :
ClientCredits.cpp:305-320
les anciens compteurs sont remis à une valeur minimale :
downloaded = 1
uploaded = 1afin de ne pas migrer aveuglément des crédits non sécurisés vers une nouvelle identité cryptographique.
23. Réparation automatique prudente
preferences.dat présent ?
UserHash valide ?
cryptkey.dat présent ?
SecureIdent activé ?
clé privée décodable ?
clé publique dérivable ?
preferencesKad.dat valide ?
Kad ID nul ?
clients.met lisible ?
backup clients.met meilleur que le fichier principal ?
Puis présenter un diagnostic.
Il ne faut surtout pas régénérer silencieusement une identité valide simplement parce qu’un fichier est temporairement inaccessible.
La priorité doit être :
détecter
→ expliquer
→ sauvegarder
→ proposer une réparation
et non :
écraser et recréer
24. Politique de crédits : chantier séparé
Les différences eMule/aMule montrent qu’on peut techniquement modifier la politique locale.
Mais une modification des crédits affecte :
- l’équité de la file ;
- l’incitation au partage ;
- le comportement vis-à-vis de clients anciens ;
- l’écosystème social du réseau.
ratio moyen des clients en queue
distribution des multiplicateurs
temps gagné grâce au crédit
pourcentage de clients identifiés
crédits expirant sans jamais être réutilisés
Puis comparer plusieurs politiques en simulation.
26. Protection des données locales
Le UserHash et les clés ne sont pas des mots de passe de compte central.
Mais ils représentent une identité réseau persistante et des droits accumulés localement chez d’autres pairs.
- éviter de les copier dans les logs publics ;
- masquer les hashes complets dans les captures diagnostiques par défaut ;
- distinguer clairement clé publique et clé privée ;
- ne jamais exporter
cryptkey.datsans action explicite de l’utilisateur.
27. Classification protocole / politique / stockage
Protocole / compatibilité
UserHash 16 octets dans les hello
OP_PUBLICKEY
OP_SIGNATURE
OP_SECIDENTSTATE
format SecureIdent v1/v2
taille/champ des messages
Identité locale historique
preferences.dat
cryptkey.dat
preferencesKad.dat
Politique locale
formule de crédit
expiration à 150 jours
fréquence de sauvegarde
usage exact du bonus
diagnostics
29. Conclusion
préserver les identités et les formats historiques ne signifie pas conserver toutes les implémentations internes de 2002.
Le UserHash, les opcodes et les formats SecureIdent sont des éléments d’interopérabilité.
Mais :
- la génération du challenge ;
- le diagnostic ;
- la sauvegarde ;
- la validation des fichiers ;
- le calcul local des crédits ;
sont des domaines où le logiciel peut évoluer.
La première modification concrète issue de notre étude est donc déjà identifiable :
remplacer le challenge SecureIdent généré par
rand()+1par un vrai nombre aléatoire cryptographique de 32 bits, sans changer un seul octet du protocole.