v1.6.0 — Le grand virage FOSS

TL;DR

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 :

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

+-----------------------+ | Client (Android) | | Orbot active | +-----------+-----------+ | +-----------+-----------+ | Tor network | +-----------+-----------+ | ----------------+---------------- | | | api.onion web.onion fdroid.onion | | | +------+------+ +------+------+ +-----+------+ | nginx :8443 | | nginx :8444 | | nginx :8445| +------+------+ +------+------+ +-----+------+ | | | API Rust pages stat. Repo F-Droid Aucun nœud de sortie. Le trafic ne quitte jamais Tor. Tous les logs nginx sont désactivés : access_log off.

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à.

Note : les hidden services Tor n'apportent pas de bonus crypto sur le contenu — tout est déjà chiffré bout-en-bout. Le bénéfice est l'anonymat réseau : le serveur ne voit jamais l'IP client, et l'IP du serveur n'est pas observable depuis le réseau du client.

Ce que ça a coûté, ce que ça a rapporté

Soyons honnêtes : ce n'a pas été gratuit.

Le coût

Le bénéfice

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 :

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.

M
Maksim Trikic
Solo dev Nøbody · avril 2026
← Retour au blog Commentaire / Contact →