Nøbody v1.6.0 boucle une refonte de 6 mois pour rendre l'app 100 % FOSS : relicensing AGPL-3.0, Firebase remplacé par UnifiedPush, 3 hidden services Tor, soumission au dépôt F-Droid principal, fin des dépendances Google Play Services.
Le coût : ~6 mois de travail. Le bénéfice : une app vérifiable, redistribuable, et utilisable sans aucun service Google. Cet article raconte le pourquoi, le comment et le ce-qui-suit.
Pourquoi il fallait passer en FOSS
Nøbody est une app de réseau social anonyme : pas de numéro de téléphone, pas d'email, pas de Google ID. Le pitch tient en une phrase, et le code l'a respecté depuis le début. Sauf que l'app reposait sur trois choses qui faisaient tousser tout développeur sensible à la liberté logicielle :
- Firebase Cloud Messaging (FCM) pour les notifications push. C'est-à-dire un SDK propriétaire de Google déclenchant un appel réseau vers les serveurs de Google — sur une app vendue comme « sans Google ».
- Un dépôt F-Droid secondaire (notre propre repo). Fonctionnel, mais pas dans la liste de packages F-Droid « principale », donc invisible à 99 % des utilisateurs F-Droid.
- Une licence trop permissive (MIT) qui aurait permis à un acteur malveillant de fork le code, l'incorporer dans une app surveillée, sans rendre les modifications.
L'objectif était simple : corriger les trois en même temps. Pas une fonctionnalité, mais un nettoyage architectural complet.
Le choix AGPL-3.0
Pourquoi AGPL et pas MIT ou GPLv3 ? Le modèle client-serveur de Nøbody voulait du copyleft réseau. La GPLv3 ne couvre que la distribution du binaire : si quelqu'un héberge un fork modifié sans jamais distribuer l'app, la GPLv3 ne l'oblige à rien.
L'AGPL ferme cette « ASP loophole » : même un service réseau qui n'est jamais distribué doit publier ses modifications. C'est exactement ce qui protège un projet de surveillance contre le vol par un hébergeur malveillant.
Le bonus, peut-être le plus important : une licence forte, c'est aussi une assurance pour les utilisateurs. Si je disparais, le code reste libre et copyleft. Personne ne peut le re-fermer en post-mortem.
La migration Firebase → UnifiedPush
Firebase Cloud Messaging, c'est facile. Trop facile. Trois lignes de Gradle et un onMessageReceived() et c'est fini. C'est aussi un appel non-désiré vers Google, même sur une app qui prétend ne pas en faire.
UnifiedPush est la spécification du Fediverse pour le push. Un protocole simple : un « distributor » (NTfy, ntfy.sh, Conversations) reçoit un POST HTTP du serveur, et réveille l'app correspondante. Pas de SDK, pas de tracker, pas de Google.
Le côté client (Flutter / Android)
Le côté client Flutter passe par un canal méthode (MethodChannel) vers une UnifiedPushReceiver Kotlin. L'app s'enregistre une fois auprès du distributor, reçoit un endpoint HTTPS, et l'envoie au serveur de Nøbody.
// android/.../UnifiedPushReceiver.kt
class NbReceiver : MessagingReceiver() {
override fun onNewEndpoint(ctx: Context, ep: PushEndpoint, instance: String) {
// POST le nouvel endpoint au backend, signe
// avec Ed25519 privée, jamais avec un identifiant fixe
Backend.registerEndpoint(ep.url)
}
override fun onMessage(ctx: Context, msg: PushMessage, instance: String) {
// Le payload est un blob NaCl chiffré bout-en-bout
// Le distributor ne voit qu'une chaine opaque
LocalNotifier.show(decrypt(msg.content))
}
}
Subtilité importante : le payload reçu par UnifiedPush est opaque pour le distributor. Le serveur de Nøbody chiffre le contenu avec NaCl avant l'envoi ; même un distributor compromis ne voit qu'un blob aléatoire.
Le côté serveur (Rust)
Avant : le serveur appelait l'API FCM de Google avec une clé de service. Après : le serveur fait un POST HTTPS direct sur l'endpoint UnifiedPush du client. Aucun intermédiaire, aucun rate-limit FCM, aucun service externe.
// crates/push/src/dispatch.rs
pub async fn notify(c: &Client, ep: &Endpoint, payload: &[u8])
-> Result<(), PushError> {
let ciphertext = nacl_box(payload, &ep.pk);
c.post(&ep.url)
.header("TTL", "86400")
.body(ciphertext)
.send().await?
.error_for_status()?;
Ok(())
}
Le compromis : les utilisateurs doivent installer un distributor (NTfy en plus, à quelques Mo). En contrepartie, ils choisissent leur fournisseur de push, qui peut être auto-hébergé ou tournant sur Tor. C'est plus de liberté, pas moins.
Ajout de Tor : 3 hidden services
L'absence de logs c'est bien. Empiêcher la corrélation IP à la source, c'est mieux. v1.6.0 ajoute trois hidden services Tor : l'API, le site web public, et le dépôt F-Droid. Pas de proxy, pas de bridge, pas de magie : tor tourne sur le même VPS et écoute en local sur les sockets onion.
L'architecture en ASCII
Concrètement, dans /etc/tor/torrc :
# API hidden service
HiddenServiceDir /var/lib/tor/nb-api/
HiddenServicePort 443 127.0.0.1:8443
# Site web
HiddenServiceDir /var/lib/tor/nb-web/
HiddenServicePort 443 127.0.0.1:8444
# Dépôt F-Droid
HiddenServiceDir /var/lib/tor/nb-fdroid/
HiddenServicePort 443 127.0.0.1:8445
Le client Android détecte si Orbot est actif et bascule automatiquement vers les .onion via SOCKS5 sur 127.0.0.1:9050. Si Orbot n'est pas là, l'app utilise le clearnet TLS-1.3. Les utilisateurs n'ont jamais à configurer quoi que ce soit : c'est juste là.
Ce que ça a coûté, ce que ça a rapporté
Soyons honnêtes : ce n'a pas été gratuit.
Le coût
- ~6 mois de travail pour un solo dev (moi).
- ~12 000 lignes de code changées entre client et serveur.
- L'audit licence (chasse aux dépendances incompatibles AGPL) a pris 2 semaines à lui seul.
- Tests de non-régression sur 14 variantes Android (de 8.0 à 15) avant chaque release candidat.
- Réécriture complète du build Gradle pour reproductibilité (même hash SHA-256 sur 3 machines différentes).
Le bénéfice
- L'app passe l'inclusion-check F-Droid principal (en attente de revue mais sans blocker connu).
- Plus aucune trace de Google dans le binaire : ni FCM, ni Crashlytics, ni Maps, ni AdMob, ni rien.
- Les utilisateurs paranoiaques peuvent configurer Orbot et tout passe par Tor sans cliquer un seul bouton de plus.
- L'AGPL crochete définitivement la possibilité qu'un fork hostile le re-licencie.
- La candidature au grant NLnet NGI0 est crédible : ils financent du FOSS, pas du semi-libre.
Et un bénéfice plus mou, mais réel : l'absence de Google et la présence de Tor changent la conversation. Au lieu de devoir expliquer « je ne collecte pas vos données » (que personne ne peut vérifier), le code montre tout. C'est plus honnête, et ça se défend mieux.
Ce qui suit
v1.6.0 boucle un cycle, mais ne ferme pas la roadmap. Trois gros chantiers s'ouvrent maintenant :
- Le grant NGI0 : dossier déposé en avril, réponse attendue cet été. Si on l'obtient, ça finance un audit sécurité externe et 6 mois de travail iOS.
- iOS : le portage Flutter / Swift démarre. Objectif TestFlight Q3 2026, public Q4. Pas avant. Pas de promesse vapor.
- L'audit indépendant : l'objectif est qu'un cabinet tiers regarde le code, le binaire reproductible, et la config serveur. Le rapport sera publié integralement, même les findings gênants.
Pour le reste, il y a la page roadmap. Tout est public, tout est daté, et on dira ce qu'on n'a pas livré quand on ne l'aura pas livré. C'est la différence entre un projet et un pitch deck.
Si vous avez des questions techniques, des idées de RFC, ou simplement envie d'engueuler un choix d'architecture : contact. Les issues sur Codeberg sont aussi ouvertes.
Merci d'avoir lu jusqu'ici. La suite arrive plus vite qu'on ne le croit.