Journal Fusionner deux profils signal-desktop pour retrouver ses conversations

Posté par  . Licence CC By‑SA.
54
18
avr.
2022

Sommaire

Me revoilà pour un journal un peu technique. On part à l’aventure et on va notamment parler de Signal, de SQL, SQLite, de son extension fts5 (vite fait) et de SQLCipher (variante de SQLite capable de chiffrer les bases de données, découverte à l’occasion), et de fusion de deux bases de données Signal pour reconstruire un historique complet et « réparer » un profil Signal un peu moisi.

Généralités sur Signal

Signal est une application de messagerie instantanée chiffrée de bout en bout utilisant (pour le moment) le numéro de téléphone comme identifiant. Très peu d’état est stocké sur le serveur. En particulier, le carnet de contact et les groupes dont on fait partie n’y sont pas stockés. Il y a un appareil principal et éventuellement des appareils secondaires. L’appareil principal est en général un téléphone sur Android ou iOS, mais on peut s’en sortir autrement avec signal-cli sur n’importe quel ordinateur ou Axolotl sur un appareil GNU/Linux doté d’une carte SIM (il doit pouvoir tourner sans, mais je n’ai pas essayé) – en général un téléphone. Les appareils secondaires sont en général des ordinateurs classiques faisant tourner Signal-Desktop, mais ça pourrait être signal-cli ou tout autre client alternatif, comme siggo, scli ou signal-curses.

Chaque appareil a sa propre session Signal. Je ne pense pas que vos contacts puissent voir depuis quel appareil vous avez envoyé un message, mais vous pouvez consulter la liste des sessions enregistrées depuis l’application mobile. Sur Signal-Desktop, vous pouvez voir le nom de l’appareil dans les paramètres mais pas le modifier.

Signal-Desktop est une application Electron écrite en React stockant ses données dans une base de données SQLite chiffrée, et dans des fichiers classiques (comme les pièces jointes / les images dans les messages).

Sous GNU/Linux, Signal-Desktop stocke toutes ses données dans le dossier .config/Signal de votre répertoire personnel. Copier ce dossier sur un autre ordinateur qui a une version identique ou plus récente de Signal-Desktop va marcher, mais ne vous avisez pas à continuer à utiliser l’original parce que ça va mal se passer : les deux Signal-Desktop vont sans arrêt se déconnecter mutuellement. Ne me demandez pas comment je sais. Je suppose que les messages pourraient n’arriver qu’à l’un ou qu’à l’autre, de façon aléatoire. Signal fait la supposition que vous ne faites pas ça.

Les anciens messages ne sont pas renvoyés à un appareil nouvellement appairé, ou à une nouvelle session principale (réinstallation de l’application mobile / changement de téléphone portable sans restaurer les données). Ça permet d’empêcher quelqu’un qui réussirait à détourner (ou qui a hérité de) votre numéro de téléphone d’avoir accès à vos anciens messages. C’est aussi un des deux états de fait qui ont impliqué cette investigation. De même, si vous rejoignez un groupe, vous n’avez pas accès aux messages anciens. Pratique et pas pratique à la fois, selon ce que vous voulez faire (si vous organisez un évènement avec un groupe Signal éphémère, vous devez sans cesse répéter les infos essentielles dès que de nouvelles personnes rejoignent le groupe. Si vous intégrez quelqu’un dans un groupe de longue durée, vous ne voulez probablement pas que cette personne ait accès aux anciens échanges). Ces non-réexpéditions sont des choix fonctionnels qui facilitent également probablement l’implémentation.

Exposé de la situation

J’utilise Signal-Desktop sur deux ordinateurs. Appelons-les ordi A et ordi B. J’ai appairé ordi B un poil plus tard que ordi A, donc il n’a pas les premiers messages sur ordi B. J’ai toujours souhaité avoir tous mes messages sur les deux ordis, mais pas assez pour me pencher sur le problème : en pratique, je n’ai pas réellement besoin de mes messages de l’année dernière sur ordi B, si besoin je peux utiliser ordi A pour les consulter.

Cependant, récemment, pour une raison inconnue, ma session Signal sur ordi A a s’est retrouvée dans un état incohérent. J’ai perdu des messages récents dessus, et j’ai observé des dysfonctionnements variés. Ce weekend, je me suis alors penché sur la question…

Attention : je décris des étapes pas garanties. Je ne suis pas familier avec le fonctionnement précis de Signal-Desktop. Je vous invite pas à reproduire ces étapes. Elles pourraient détruire vos messages, ou vous donner l’impression qu’elles marchent mais en fait causer des problèmes plus ou moins subtils plus tard, y compris des problèmes de réceptions de messages. L’intérêt de ce journal est pour moi de garder trace de mes étapes, et pour vous de lire une session d’investigations et de peut-être découvrir certaines techniques. Si vous tentez de reproduire ces choses, vous le faites à vos risques et périls en toute connaissance de cause.

Malgré les risques, je me suis quand même lancé dans l’aventure. Je pars du principe que dans le pire des cas :

  • je devrais toujours pouvoir recevoir mes messages à l’aide de l’application mobile (Axolotl), non touchée par ces bidouilles
  • je garde une copie des fichiers de travail (le dossier de configuration de Signal-Desktop des deux ordinateurs), que je peux toujours consulter avec Signal-Desktop hors connexion en les copiant là où il s’attend à les trouver
  • au pire, on désinstalle tout et on recommence de zéro
  • dans tous les cas, ma session sur l’ordi A est un peu niquée alors je n’ai pas grand-chose à perdre au final…

Ouvrir la base de données

Bref, assez parlé, mettons les mains dans le cambouis. En inspectant le dossier .config/Signal, on tombe rapidement sur le fichier sql/db.sqlite, on se doute vite que c’est une base de données SQLite. il y a aussi les dossiers attachments.noindex et avatars.noindex qui contiennent des données binaires liées aux messages ou au profil tel que les images.

Premier réflexe : ouverture du fichier avec SQLite Browser. Et là on se rend compte tout de suite que ça demande une clé de déchiffrement.

SQLite Browser demande une clé de déchiffrement

Un petit tour sur internet nous informe que la clé à utiliser est stockée dans le fichier .config/Signal/config.json :

{
  "key": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "mediaPermissions": true,
  "mediaCameraPermissions": true
}

Et on finit par comprendre qu’il faut utiliser l’option « Clé de chiffrement » et la préfixer par 0x pour ouvrir la base. En ouvrant la base, on se rend compte que les données messages ne s’affichent pas. L’interface les affiche en rouge et prétend que les valeurs sont en cours de chargement, mais on se rend compte en regardant les logs que l’interface essaie d’utiliser la fonction json_extract et n’y arrive pas. Je n’ai pas creusé, j’ai continué mes investigations avec SqliteMan qui y arrivait sur une version non chiffrée de la base.

SQLite Browser ne trouve pas la fonction `json_extract`

sqlite3 n’est pas capable d’ouvrir une base chiffrée, il faudra pour cela utiliser SQLCipher, que je découvre pour l’occasion. Après de longues recherches, je comprends qu’il faut préfixer la clé par x pour ouvrir la base de données avec SQLCipher. On peut utiliser SQLCipher pour manipuler les bases de données chiffrées ou leur retirer le chiffrement.

SQLite version 3.37.2 2022-01-06 13:25:41 (SQLCipher 4.5.1 community)
Enter ".help" for usage hints.
$ cp ~/.config/Signal/sql/db.sqlite ordiA.sqlite
$ sqlcipher ordiB.sqlite
sqlite> pragma key = "x'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'";
ok
sqlite> select count(*) from messages;
11000

Si la clé n’est pas bonne, vous obtiendrez le message suivant au moment de faire des requêtes SQL :

Error: in prepare, file is not a database (26)

Vous pouvez également vous attacher à une base chiffrée de cette manière :

sqlite> attach database 'ordiB.sqlite' as ordiB key = "x'9876543210fedcba0987654321fedcba0987654321fedcba0987654321fedcba'";
sqlite> select count(*) from ordiB.messages;

Pour ouvrir ou vous attacher à des bases de données non chiffrées, simplement laisser key vide :

pragma key = "";
…
sqlite> attach database 'ordiB.sqlite' as ordiB key = "";

On peut changer la clé de chiffrement de la base de données ouverte en utilisant pragma rekey = "XXX"; après pragma key, mais on en a pas besoin. On peut sauvegarder une version non chiffrée de cette manière :

$ cp ~/.config/Signal/sql/db.sqlite ordiA.sqlite
SQLite version 3.37.2 2022-01-06 13:25:41 (SQLCipher 4.5.1 community)
Enter ".help" for usage hints.
sqlite> pragma key = "x'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'";
ok
sqlite> attach database 'ordiA.nonchiffre.sqlite' as plaintext key = '';
sqlite> select sqlcipher_export('plaintext');

(ou en utilisant pragma rekey = '' mais je ne me souviens plus d’avoir essayé). Voilà, vous savez utiliser SQLCipher aussi bien que moi).

La base de donnée de Signal a pas mal de tables. Après inspection, celles qui nous intéressent plus particulièrement sont :

  • messages : elle contient tous les messages reçus (et envoyés ?), au sens large (changements de statut, mises à jour de groupes, accusés de réceptions / de lecture, etc). Dans le protocole Signal, tout changement d’état passe par des messages et tout semble stocké dans cette table, vous pouvez probablement rejouer précisément toute l’histoire avec cette table (à vérifier, c’est juste une intuition). Je pars du principe que le serveur Signal fait une diffusion basique des messages à chaque session enregistrée et que toutes ces sessions reçoivent exactement les mêmes messages, ou en tout cas des messages équivalents. Avec possiblement quelques messages spécifiques pour chaque session. Je n’ai pas vérifié cette hypothèse, et d’ailleurs j’ai pu observer des petites différences d’ordre dans les messages entre la session de l’ordi A et de l’ordi B, alors elle n’est pas forcément très vraie mais partons là-dessus quand même, c’est probablement une approximation acceptable. Mais du coup, pour la fusion, il ne suffit possiblement pas de se contenter de récupérer les messages d’ordi A pas présent dans ordi B et de les insérer au bon endroit (peut-être que ça marche, mais je n’ai pas essayé).
  • conversations : elle contient vos conversations privées ou de groupe.
  • reactions : depuis la révision 29 de la base de données, elle contient les réactions aux messages, et la révision 37 ajoute la référence au message de cette réaction (les réactions, sur Signal, sont des messages comme les autres, comme on peut d’ailleurs le constater dans Axolotl, qui ne gère pas encore les réactions).
  • sessions contient les données de sessions avec les correspondants, par conversation. Je n’ai pas trop creusé son fonctionnement.

Effectuer la fusion

Avant tout, fermez Signal sur ordi A et sur ordi B. Vous ne voulez pas recevoir de messages pendant la manipulation pour éviter d’interférer ou créer des incohérences.

Pour fusionner les deux profils pour avoir un profil complet sur l’ordi A, on s’assure d’avoir ouvert les deux bases de données (ordi A et ordi B) avec la même dernière version de signal-desktop. Signal-desktop fait des migrations et si les bases de données ne sont pas à la même version, ça va être la merde. Ensuite, voici l’idée générale :

Créer la copie de travail

On fait une copie de la base de données d’ordi A, vers fusion.sqlite par exemple, et on ouvre cette copie avec sqlcipher.

Suppression des messages « communs »

On repère l’identifiant du premier message reçu par ordi B dans ordi A avec SqliteMan et on le supprime ainsi que tous les suivants:

delete from messages where rowid > 263;

(même chose pour la table reactions, l’étape 2 aussi)

Insertion des messages

On insère les messages de l’autre base :

insert into messages select * from ordiB.messages;

En réalité, ça ne marche pas. Il a fallu indiquer les champs /explicitement. Pour avoir la liste des champs :

pragma table_info(messages)
0|rowid|INTEGER|0||1
1|id|STRING|0||0
2|json|TEXT|0||0
3|readStatus|INTEGER|0||0
4|expires_at|INTEGER|0||0
5|sent_at|INTEGER|0||0
6|schemaVersion|INTEGER|0||0
7|conversationId|STRING|0||0
8|received_at|INTEGER|0||0
9|source|STRING|0||0
10|deprecatedSourceDevice|STRING|0||0
11|hasAttachments|INTEGER|0||0
12|hasFileAttachments|INTEGER|0||0
13|hasVisualMediaAttachments|INTEGER|0||0
14|expireTimer|INTEGER|0||0
15|expirationStartTimestamp|INTEGER|0||0
16|type|STRING|0||0
17|body|TEXT|0||0
18|messageTimer|INTEGER|0||0
19|messageTimerStart|INTEGER|0||0
20|messageTimerExpiresAt|INTEGER|0||0
21|isErased|INTEGER|0||0
22|isViewOnce|INTEGER|0||0
23|sourceUuid|TEXT|0||0
24|serverGuid|STRING|0||0
25|sourceDevice|INTEGER|0||0
26|storyId|STRING|0||0
27|isChangeCreatedByUs|INTEGER|1|0|0

En collant dans Kate, sélection multiple sur les 10 premières lignes, espace pour aligner tous les numéros à droite, sélection multiple sur la première colonne pour la supprimer, remplacement expression régulière \|.+ par , CTRL+A pour tout sélectionner, CTRL+J pour joindre toutes les lignes, on obtient une liste de champs séparés par des virgules. Requête SQL :

insert into messages(id, json, readStatus, expires_at, sent_at, schemaVersion, conversationId, received_at, source, deprecatedSourceDevice, hasAttachments, hasFileAttachments, hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, isViewOnce, sourceUuid, serverGuid, sourceDevice, storyId, isChangeCreatedByUs) select id, json, readStatus, expires_at, sent_at, schemaVersion, conversationId, received_at, source, deprecatedSourceDevice, hasAttachments, hasFileAttachments, hasVisualMediaAttachments, expireTimer, expirationStartTimestamp, type, body, messageTimer, messageTimerStart, messageTimerExpiresAt, isErased, isViewOnce, sourceUuid, serverGuid, sourceDevice, storyId, isChangeCreatedByUs from ordiB.messages;

Mais avant ça, il faut traiter la table messages_fts et ses copines messages_fts_config, messages_fts_content, messages_fts_data, messages_fts_docsize, messages_fts_idx. Ces tables contiennent le contenu des messages, et il faut sûrement faire attention à maintenir la cohérence. Bon, spoiler (divulgâcheur) : il s’agit d’une table virtuelle fts5 créée à l’aide de l’extension SQLite fts5, qui fournit de la recherche complète de texte (full text search).

Ces tables sont créées avec :

create virtual table messages_fts using fts5(id unindexed, body);

Avant d’insérer les nouveaux messages, on supprime cette table virtuelle :

drop table messages_fts;

Et après l’insertion :

insert into messages_fts(id, body) select id, body from messages_fts;

Petite digression : malheureusement, la version de SQLCipher dans les dépôts d’openSUSE n’est pas compilée avec l’extension fts5. J’ai donc été amené à compiler SQLCipher moi-même. Ça se fait bien, la bibliothèque n’a pas de dépendances qui ne seraient pas déjà présentes dans un système GNU/Linux standard :

./configure --enable-fts5 --enable-tempstore=yes CFLAGS="-DSQLITE_HAS_CODEC" LDFLAGS="-lcrypto"
make
sudo make install
rehash # pour les utilisateurs de zsh

Import des conversations

Il faut importer les conversations qui sont dans ordiB et pas dans ordiA. Chez moi, c’est juste une conversation. Mais plus gênant, les identifiants de conversations (dans le champ id), qui sont des UUID, ne sont pas les mêmes dans les deux bases de données. On peut créer une table des correspondances. Pour les conversations privées, le numéro de la personne est dans le champ e164, qui est nul dans le cas des groupes. Pour les groupes, l’identifiant du groupe est dans le champ groupId, qui est nul dans le cas des discussions privées. On pourra faire la correspondance de cette manière :

select
    cA.id as conversationIdA,
    cB.id as conversationIdB
from conversations cA inner join ordiB.conversations cB on (
    cA.e164 = cB.e164 or
    cA.groupId = cB.groupId
);

Ça se passe bien avec les valeurs nulles, qui sont ignorés par les tests d’égalité dans la condition de jointure.

Avec un peu d’édition de texte, pour chaque conversationIdA, on exécute la ligne suivante :

delete from conversations where id = 'conversationIdA';

Ensuite, on peut importer toutes les conversations :

insert into conversations select * from ordiB.conversations;

Ensuite, il faut remplacer les identifiants de conversations des tables messages, reactions et sessions. À l’aide de la requête de correspondance, à coup d’édition multi ligne dans Kate, on peut produire des requêtes du type :

update messages set conversationId='conversationIdB' where conversationId='conversationIdA'
update reactions set conversationId='conversationIdB' where conversationId='conversationIdA'
update sessions set conversationId='conversationIdB' where conversationId='conversationIdA'

Les sessions : je ne touche pas

Pour la table sessions, je n’ai pas trop cherché ce qu’il faudrait faire. Il faut peut-être éventuellement importer les sessions des conversations qui existaient dans ordiB et pas dans ordiA mais plutôt laisser les autres telles quelles.

Modification du profil Signal

On n’a plus qu’à faire une sauvegarde du profil Signal, à remplacer la base de données et à importer les attachements :

cp -r ~/.config/Signal{,.bak}
cp fusion.sqlite ~/.config/Signal/sql/db.sqlite
rsync -avp configOrdiB/Signal/attachements.noindex/ ~/.config/Signal/attachements.noindex/
rsync -avp configOrdiB/Signal/avatars.noindex/ ~/.config/Signal/avatars.noindex/

On teste les modifications en coupant votre connexion internet et en lançant signal-desktop, hors ligne donc. Comme ça, on peut voir si on obtient les résultats attendus sans risquer de tout pourrir en recevant des messages dans un état incohérent et se compliquer la vie, voire jamais pouvoir récupérer correctement ces messages.

Bonus : pourquoi une base de données chiffrée ?

On peut se demander pourquoi la base de données Signal est chiffrée alors que la clé de déchiffrement est stockée en clair juste à côté.

Evan Hahn, un développeur de Signal, affirme :

We use SQLCipher for two reasons:

  1. To encrypt the data at rest. Some users don't have full disk encryption, and this can (theoretically) help them.
  2. SQLCipher, unlike "vanilla" SQLite, deletes data from disk. If, for example, a message expires, we can ensure it's actually deleted instead of marking it deleted but not actually deleting it.

Je ne suis pas super convaincu par la première raison (ça aiderait les gens qui n’ont pas de chiffrage sur leur ordinateur). À la limite, ça peut peut-être aider pour contrer les outils d’indexation un peu zélés qui iraient indexer le contenu des bases de données. La deuxième raison me parait plus convaincante : SQLCipher fournirait plus de garanties lors de la suppression de données. La position actuelle de l’équipe de développement semble être d’inviter les gens inquiets à utiliser du chiffrement au niveau de la partition et d’utiliser l’écran de verrouillage. Des gens ne sont pas satisfaits par cette réponse parce que les données seraient accessibles à un processus malicieux même en cas de chiffrement de la partition quand le système est démarré. M’est d’avis qu’il faudrait probablement que Signal demande un mot de passe au démarrage pour résoudre de problème ou que les systèmes d’exploitations fournissent des systèmes d’isolation un peu poussés entre applications, et que l’équipe de développement de Signal ne veut pas se risquer à implémenter des mécanismes de sécurité un peu hasardeux et elle a de toute façon d’autres priorités.

Conclusion

Avec beaucoup de précautions, en s’y reprenant à plusieurs reprises, avec des recherches internet, un peu d’intuition et de lecture du code de Signal (en particulier le code des migrations SQL), j’ai fini par réussir la fusion. En tout cas, à première vue (seul le futur permettra de dire si effectivement ça s’est vraiment bien passé). J’ai retrouvé toutes mes conversations, y compris les premiers messages et les messages qui manquaient sur l’ordi A sur l’ordi A, et la conversation qui n’était jamais apparue sur l’ordi A pour une raison inconnue. J’ai manifestement pu envoyer et recevoir des messages. Et ce, sans changement de clé de session ou sans avoir à enregistrer un nouvel appareil avec l’application mobile.

Encore une fois, je ne recommande pas de se lancer dans cette bidouille : c’est un peu risqué et ça prend du temps peut-être mieux dépenser ailleurs. Malgré tout, j’ai appris des choses et je suis content de partager l’aventure, et si ça peut servir à quelqu’un aussi ou plus fou que moi avec la conservation des historiques, ou pour quelqu’un qui s’intéresserait à SQLCipher ou au fonctionnement de Signal, ou même à une manière partir en exploration dans ce genre de situation, tant mieux.

Certaines étapes auraient pu être « rendues propres » par exemple en générant des lignes SQL avec des outils Unix ou des scripts quelconques, mais j’ai justement pris le parti de ne pas le faire. Ce n’est pas comme ça que j’ai procédé, j’y suis plutôt allé à la bourrin avec un éditeur de texte et j’ai dû coup décidé de détailler un peu cette partie. C’est justement un endroit où je me suis demandé comment être efficace, comment faire une boucle en SQLite, ouh là là il faut faire des triggers récursifs, « allez viens en vrai on va être super efficace avec l’éditeur de texte ». C’est pour moi une partie importante de comment je suis effectivement efficace quand je suis en mode résolution de problèmes, donc autant le partager comme ça. Les scripts seraient à écrire s’il y avait à faire cette manipulation souvent, mais quand ce n’est pas le cas, ce n’est pas toujours la méthode la plus rapide et des méthodes plus artisanales / intuitives conviennent parfois mieux et c’est important de le noter et d’avoir ça dans sa boite à outils. Pour exécuter des scripts, il aurait de toute façon fallu que je colle les données dans des fichiers texte, et donc j’aurais probablement utilisé un éditeur de texte, et du coup à ce stade autant utiliser les outils fournis par celui-ci. Faire les choses manuellement une fois peut de toute façon rendre l’écriture de scripts plus rapide : on sait que le résultat fonctionne, et on peut en avoir une meilleure intuition.

  • # Axolotl

    Posté par  . Évalué à 2. Dernière modification le 19 avril 2022 à 16:26.

    Il semble que le (premier) lien pour Axolotl soit erroné (à moins que le but ne soit d'égarer le badaud ?) ; j'imagine que ce devrait être plutôt https://axolotl.chat/

    • [^] # Re: Axolotl

      Posté par  . Évalué à 3.

      Oups, exact.

      Aussi :

      Ça permet d’empêcher quelqu’un qui réussirait à détourner (ou qui a hérité de) votre numéro de téléphone de ne pas avoir accès à vos anciens messages

      → d'avoir accès à vos anciens messages

      Ces tables sont créés avec:

      Il manque une espace insécable avant les deux-points.

      Je crois qu'il y en a d'autres mais elles ne me sautent pas aux yeux là maintenant.

  • # Le point après quelques jours : c'est subtilement cassé

    Posté par  . Évalué à 4. Dernière modification le 22 avril 2022 à 21:07.

    Après la manip, Il manque des messages sur ordi B et j'ai des soucis pour rejoindre des groupes. De plus j'ai fortuitement supprimé la base de donnée sur ordi A en rappelant une commande depuis l'historique un peu trop rapidement.

    → on réinitialise les sessions des deux ordis, et on recommence de zéro, ça sera le plus simple et le plus sûr.

  • # Passionnant

    Posté par  (site web personnel) . Évalué à 2.

    Hello raphj,
    Cette analyse était passionnante, merci ! Malgré ta dernière remarque, un détail qui tient à pas grand-chose à mon sens ; j'ai notamment retenu l'usage SQLCipher et la fusion de base.
    J'avais justement un ami gêné par une décorrelation de son profil Signal mobile et desktop; je lui ai passé le lien.

    Si tu avances, ici ou sous la forme d'un autre journal, n'hésite pas ; ce sera toujours bien reçu 😊.

    • [^] # Re: Passionnant

      Posté par  . Évalué à 3.

      Merci pour ton retour !

      Je n'utilise pas actuellement l'application mobile officielle de Signal, donc je ne sais pas trop comment ça marche. Par ailleurs, j'ai fini par avoir des soucis avec cette procédure donc je ne conseille pas à ton ami de l'appliquer sauf s'il est prêt à perdre ses messages. Cela dit, je ne regrette pas d'avoir écrit ce journal parce que ça permet effectivement de creuser un peu les choses, y compris SQLCipher en effet, et c'est ré-applicable dans d'autres situations.

      Cependant, je retiens ton invitation à donner des nouvelles dans le futur si j'en ai.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.