Lattice Network · Android
Messaggi, foto, note vocali e chiamate cifrati end-to-end con crittografia post-quantistica ibrida (ML-KEM-768 + X25519, triplo ratchet). Il nostro server vede solo buste opache: nessun contenuto, nessun indirizzo IP conservato. L'app non è sul Play Store per scelta: l'APK si scarica da qui e si verifica da soli con l'impronta SHA-256.
Versione precedente, se ti serve tornare indietro: lattice-pulse-1.1.0.apk
Ogni impronta qui sotto si rifà in un comando:
sha256sum lattice-pulse-<versione>.apk. Dalla v6.6.4 ogni rilascio porta anche la
firma ML-DSA-65 del suo manifest (latest.json):
è la firma, non la nostra parola, a dire che quell'impronta l'abbiamo dichiarata noi.
Le versioni precedenti restano scaricabili con l'impronta calcolata sul file: archivio,
non certificato — e lo scriviamo invece di far finta.
| Versione | Impronta SHA-256 | Dimensione | Data | Certificazione |
|---|---|---|---|---|
| v8.2.3 · build 309 | 1fde3023ed158698278a1b6cd9ed2834eb27ca8995329bc73f875e4c350827a6 | 60.8 MB | 2026-09-28 | firmata ML-DSA-65 |
| v8.2.2 · build 308 | 1c216b0444296777d9625b5cd20b7ca1f03ef1d719c08a234e025f668b3aa9d8 | 60.8 MB | 2026-09-27 | firmata ML-DSA-65 |
| v8.1.6 · build 306 | f36e09398015b07219011cf01c02cbbaa00f4c1b9be26af9dd872223e1090d8c | 60.8 MB | 2026-09-27 | firmata ML-DSA-65 |
| v8.1.5 · build 305 | ebad8e18a679d48b4d54be68e4e8939a1c63b054983ceafd949d3c5147abd15b | 60.8 MB | 2026-09-27 | firmata ML-DSA-65 |
| v8.1.4 · build 304 | a170eb98da088160e04899db382f0d58522b0a241192ae66c66cfef3989172f3 | 60.8 MB | 2026-09-27 | firmata ML-DSA-65 |
| v8.1.3 · build 303 | 313ec857b8a0e9c3e05c113b58e2efbc5a435d959df45ac85efbbd93a83ae2f2 | 60.8 MB | 2026-09-27 | firmata ML-DSA-65 |
| v8.1.2 · build 302 | cc08b85a2223d2fc278fc2389ef39add28b839495623dafb235fb9f16b016991 | 60.7 MB | 2026-09-26 | firmata ML-DSA-65 |
| v8.1.0 · build 301 | 262c872f6eb64e1f6a9ae2452dd3360a93028dc04ec9178f05fd46a28acffd5a | 60.7 MB | 2026-09-26 | firmata ML-DSA-65 |
| v8.0.7 · build 300 | d2e1da9bd4d085445423d4af75056a8e77886412c4b8cf5b8c49f7c8b3e7cdbb | 60.6 MB | 2026-09-26 | firmata ML-DSA-65 |
| v8.0.5 · build 299 | 9e3de5e19feb03735f261fb455ef87ab2ef4a965499c15e7d2712fdd920bafc3 | 60.6 MB | 2026-09-25 | firmata ML-DSA-65 |
| v7.9.6 · build 298 | 01292df06d0aec76e9434e84634a24e0353821dd92795e6c7cd0636ec5cc2140 | 60.6 MB | 2026-09-25 | firmata ML-DSA-65 |
| v7.9.0 · build 297 | a62d218076ac8b4d5e7aa9442875e63083463388a4b2992a786aa8e5a6b02032 | 60.6 MB | 2026-09-25 | firmata ML-DSA-65 |
| v7.8.0 · build 296 | f6b90aceae41bad4ca20670cebbfb9e5a393dd62f3ee950c3ea20584cd35249a | 60.6 MB | 2026-09-24 | firmata ML-DSA-65 |
| v7.7.0 · build 295 | 2a2a7d091711f2a01fe6d615fba77ef1a0bed79b14cb6fd7410569a62c870ece | 60.6 MB | 2026-09-24 | firmata ML-DSA-65 |
| v7.6.0 · build 294 | 96b09c6b9867586eb9085a4e697c593039b0199532058657591116d3b8a33bf9 | 60.6 MB | 2026-09-24 | firmata ML-DSA-65 |
| v7.5.5 · build 291 | 865849fe8cc213bd801aef1a1da995ffe2e56ac68797c91dc4070773f52ced04 | 60.6 MB | 2026-09-24 | firmata ML-DSA-65 |
| v7.5.4 · build 289 | f0d094f7caa7c0d93bb924356d4a61a6ac27865d238dd6e514af19a128e5c771 | 60.6 MB | 2026-09-22 | firmata ML-DSA-65 |
| v7.5.2 · build 287 | 28164eba7bb0896c60a6e3d954386b2edadca18c76ed07f4bc508f66178c91d2 | 60.6 MB | 2026-09-22 | firmata ML-DSA-65 |
| v7.5.0 · build 285 | a5fdc42eb90df5a6a2adcac491adddd8ced7577cf2b1eb6d82d33703f58ce7a3 | 60.6 MB | 2026-09-22 | firmata ML-DSA-65 |
| v7.3.2 · build 283 | c811ddc27af4d4f7c1d972191cd9caff33b599603b4248d3e6b18cf507081b5f | 60.6 MB | 2026-09-22 | firmata ML-DSA-65 |
| v7.3.1 · build 282 | 78ddbefe13fd37b0d8b90fc766e78fd66f87be041c1aaa51d7df143a08d40d35 | 60.6 MB | 2026-09-22 | firmata ML-DSA-65 |
| v7.3.0 · build 281 | a9549edcee4072dfbef356234a76dfd9475bbcb4422218766b8b4eff284c46db | 60.6 MB | 2026-09-21 | firmata ML-DSA-65 |
| v7.2.2 · build 280 | a1c730f6324739659d80603e9c85172ffe02b2edb5e0ae55195b6228fed65843 | 60.6 MB | 2026-09-21 | firmata ML-DSA-65 |
| v7.2.1 · build 279 | 4bf0583d1cf0c86656effa181e34d722185370466e01b02d292c5229ec009fc2 | 61.5 MB | 2026-09-21 | firmata ML-DSA-65 |
| v7.2.0 · build 278 | 5ff50b7cfe949e224b16e3d1e148e33f7f16e9eabccc1128037d50e9eeeb241b | 61.5 MB | 2026-09-21 | firmata ML-DSA-65 |
| v7.1.0 · build 277 | b2a596335083552107ee4c964e729f93167b5479a4c4e2966a76763b4a1a92bd | 61.8 MB | 2026-09-20 | firmata ML-DSA-65 |
| v7.0.3 · build 276 | daf243a9cc33775ee57228faa6a0c892435a53d4fc64d3132fb1ed72fbfbe7fb | 61.8 MB | 2026-09-20 | firmata ML-DSA-65 |
| v7.0.2 · build 275 | aec32e0dfc7aa403fea5f100160ef2bc9cf4cdb7cd8ff31ddbbe623c4d913acc | 61.8 MB | 2026-09-20 | firmata ML-DSA-65 |
| v7.0.0 · build 274 | 2e79365ae5bcfccee5cfbe79830e29bb6aee228308619b1434a37c3c00348f27 | 61.5 MB | 2026-09-20 | firmata ML-DSA-65 |
| v6.9.2 · build 272 | 4316b235aafced587b2acd576e206ff35c6ae5d46dc003ac969ac1e08b81aaf3 | 61.5 MB | 2026-09-19 | firmata ML-DSA-65 |
| v6.9.1 · build 271 | 96d31785efcd052e36f26f6d3c9e02a6e2313a43f4e7e51d1e34517cf50b704f | 61.4 MB | 2026-09-18 | firmata ML-DSA-65 |
| v6.9.0 · build 270 | 0de4bf23146928f1d8261328896dff9784694c6395943c8b813754b053e96ee2 | 61.4 MB | 2026-09-17 | firmata ML-DSA-65 |
| v6.8.0 · build 269 | dbd67ae1b6c62f0658a490c2160e45d10dd9c4edabb0efeb643c4d65397b2ce8 | 61.4 MB | 2026-09-16 | firmata ML-DSA-65 |
| v6.7.1 · build 268 | 32a9b3b8624edf28f3b5ec350ab8b5b6da4141bf8fab5bfa82b398e6cea4fd47 | 61.4 MB | 2026-09-16 | firmata ML-DSA-65 |
| v6.7.0 · build 267 | f0119d0cadd0a6d335e893335bfea7971c45e5811915ab7d25b568056b7313b7 | 61.4 MB | 2026-09-16 | firmata ML-DSA-65 |
| v6.6.4 · build 266 | 3db33d64f57536a9cda1c61bf95c5b054f097169185a8de3eca07e93fed25b37 | 61.4 MB | 2026-09-16 | firmata ML-DSA-65 |
| v6.6.3 | 751ab10682235da8c453d59246fc3688127e83d6b0533020444d6f8eb98e1113 | 61.4 MB | 2026-09-16 | archivio · impronta calcolata |
| v6.6.2 | c956457aba609f66d7e90194f7d2d00c058db457b6eed12f184c2aaecbd60b01 | 61.4 MB | 2026-09-16 | archivio · impronta calcolata |
| v6.6.1 | ed9b1a58c042e60281c6b46d60e45c9f6d602a457c9c1ff7b393ad327a1e09ad | 61.4 MB | 2026-09-16 | archivio · impronta calcolata |
| v6.6.0 | 41008cd3f22ff680154edf96618a5a7393bba877229c37352984f5e304bbc660 | 61.4 MB | 2026-09-16 | archivio · impronta calcolata |
| v6.5.1 | 3c7ee31c87d446b643d028d51f14c03a230286b0fff38c8630a3d09fa4531ea3 | 61.4 MB | 2026-09-16 | archivio · impronta calcolata |
| v6.5.0 | 77ed6e13072a672845dabb43127f0b313220ea67dd10cecce6fea8d28fdfed28 | 61.4 MB | 2026-09-15 | archivio · impronta calcolata |
| v5.1.0 | b4d4297e98b22204e9dee0eff795bec6923826d59fda761fb5fcd600acbdde5c | 61.3 MB | 2026-09-13 | archivio · impronta calcolata |
| v5.0.1 | a1c6a3cc32fdcb379477046c18df4d1e0cbc7f16314e0c9320193bd9d4fc2876 | 61.3 MB | 2026-09-13 | archivio · impronta calcolata |
| v5.0.0 | f3ea414e65160640c9839b5ec597228a671f1961ee224c87a4f6a7c069d73559 | 61.3 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.5.3 | d6ea9b1c5194b33f8bad83a64e50c33b0d3905d08cea5c00e90c4f485b1d5b84 | 61.3 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.5.2 | 3ecf6784ff97bed7975bc104be9dcc7311de2c457c6db138e741876fb49a2da0 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.5.1 | 43c6e6dae764a03a6b397f7bfabf391d6be65be0d9663a09983c54c619c6efb2 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.5.0 | 84abf3417e8ed2cc09032089bf2640a6a7491db980845b4018b5649b5c756650 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.9 | c1485e1a90b8280047b3b90c86d7cf50ea219cbde313795bb0f0780604247baf | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.8 | 0b1c87d0e062fe0f5487ce20e464b0ce471f1fb3e81717204a902b76b742def2 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.7 | 08ca663b2bb9193d55f1960249c55f0f0d45098997792b55a7537be8163bc9a0 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.6 | ca85fbe7911400489040dcaf14acaab8b21c17d3972fd85c43c3b91289e0084d | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.5 | 35df0c733f2c860f640524aa065289b06709a146f3fb85f48a2bedd54b7017a4 | 61.2 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.4 | 77e2798a21af740ac15bb9dfa886b97419ad70f2e012794a0f678cf3ecb664ba | 61.1 MB | 2026-09-13 | archivio · impronta calcolata |
| v4.4.3 | 13a1e8d62aeca2fc396ee4fc171a1ad9b6ad4c90f4839f446df170aba0b956b5 | 61.1 MB | 2026-09-13 | archivio · impronta calcolata |
| v1.1.1 · build 312 | af124b1286d25b74bbd18ae18e26a313034d7f54067a37376587fff7e855e2d8 | 60.9 MB | 2026-09-28 | firmata ML-DSA-65 |
| v1.1.0 · build 310 | 3cf954acb8eb7c2edf37f8cadb9c2b9e1a631ee1d7d3c99f73de18ef17811adc | 60.8 MB | 2026-09-28 | archivio · impronta calcolata |
Elenco leggibile da una macchina: storico.json
Questa è la firma post-quantistica del manifest dell'ultima versione. Verificarla a mano richiede tre cose e nessuna fiducia: il manifest, la chiave pubblica del progetto e un comando.
# 1 · il manifest firmato e la chiave pubblica del sito curl -s https://lattice-network.it/downloads/latest.json -o latest.json curl -sO https://lattice-network.it/downloads/verify-apk.mjs curl -s https://lattice-network.it/web-manifest.json | python3 -c "import json,sys;print(json.load(sys.stdin)['pk'])" > pk.hex # 2 · l'impronta dell'APK che hai scaricato deve coincidere sha256sum lattice-pulse.apk # 3 · la firma copre versione, build, indirizzo, impronta e dimensione, in quest'ordine # lattice-apk-v1|<versione>|<build>|<url>|<sha256>|<byte> node verify-apk.mjs latest.json pk.hex lattice-pulse.apk
Lo strumento: verify-apk.mjs (Node 20+, una sola
dipendenza: @noble/post-quantum). Controlla la firma del manifest e poi l'impronta del
file che hai scaricato. Se prendi la chiave pubblica da un canale diverso dal nostro sito, la
verifica smette di dipendere dal nostro sito: è esattamente il punto.
Firma (ML-DSA-65, 3309 byte):
5272ae56b22944e3a2f6e8f680ab6f0c2919d7497d064c28bd828b926c2d73184adc59f59408fdabf6391ae5e336f806e160e1cb3c16880f13379b122ca2ae8b09615a2ec8d175ce6bf578f7f74f65bb1f82aa3e8419077743c7a62bb8895f704327b56d1bb93d7c8045652897591477ef6779232ed8941fae81c8c7705a5610b4e39c1ee58f19d71aa1c81c52a8ce759fe546fe7d635ac932b20dc370735b26750a8b7b3ff9fc8bb5382907483f2ed065afe4d34fe60b33389c7147b32b146b4b7cbff857cccbff766ed945d6745b69c4ca2559ba45aa843c908d0b4a32f17a3367cd9771da6ae089be7093f263ce4a017989166e03c65530d32a15f38cb59e…
Un foglio solo, da allegare a una gara o da girare a un ufficio acquisti: dice con dei numeri — non con una promessa — che nei nostri server non c'è e non può entrare nulla di Google. I numeri non li scriviamo a mano: il server apre il proprio binario in esecuzione e conta quante volte compaiono gli indirizzi dei servizi di Google. Zero significa che quel codice non esiste, non che è disattivato.
Scarica l'attestazione (PDF, 8 KB)Verificabile da soli, senza fidarsi di noi:
curl -s https://lattice-network.it/api/public/zero-google/nodo
curl -s https://lattice-network.it/downloads/zerogoogle-nodo.json
curl -s https://lattice-network.it/downloads/attestato-nodo.pdf.sig
La pagina della prova, aggiornata dal vivo → · referto JSON · firma del PDF · versione della rete
Due fogli con misure prese sul sistema in funzione, non dichiarazioni: quanto ci mette un messaggio ad arrivare, quanto ci mette una chiamata ad aprire il canale sul relay, che cosa succede a chi prova a usare il relay con credenziali scadute o manomesse, e l'esito «Zero Google» contato dentro il binario in esecuzione. In fondo, il comando per rifare ogni singola misura.
messaggio consegnato 10 ms (p95 88) · apertura chat 6 ms ·
biglietto del relay 9 ms · canale sul relay IT 2 ms ·
canale su TLS/443 9 ms
tre relay sovrani: IT · DE · FI, ognuno su 3478 UDP/TCP, 5349 TLS e 443 TLS · credenziali valide 600 s
regressione: 551 superati, 0 falliti · esito: versione sigillata
impronta SHA3-256 del PDF: de66fb0843da0d422aa1d088efe7a14083c5189045b63bff…
Scarica il referto di audit · PDF firma ML-DSA-65 misure in JSON
Fino alla v2.1.0 l'APK era firmato con la chiave di debug di Android, la cui password è pubblica: il file della chiave era l'unico segreto, ed è un modo fragile di firmare un'app che parla di privacy. Dalla v2.2.0 c'è una chiave di release RSA 4096 generata sul nostro server, che da lì non esce.
Il prezzo lo pagate voi, una volta sola: Android — giustamente — non accetta un aggiornamento firmato con una chiave diversa. Quindi: esportate il vostro file chiave (Impostazioni → Copia di sicurezza) se non l'avete già, disinstallate Lattice Pulse, installate questa versione, rientrate col file chiave.
Chi ha già la v2.2.0 non deve fare niente di tutto questo: la chiave di firma è la stessa, la v2.3.0 si installa sopra come un normale aggiornamento.
Lo scriviamo grosso e in cima perché è un fastidio reale causato da un errore reale. Nasconderlo in fondo alla pagina sarebbe stato peggio del fastidio.
Aggiorna sempre da questa pagina se l’app non si apre o si chiude da sola: l’impronta SHA-256 qui sopra è la prova che il file è quello firmato da noi. Nelle versioni fino alla 1.3.7 il banner di aggiornamento dentro l’app aveva un difetto che la chiudeva: da quelle versioni bisogna passare da qui una volta.
sha256sum lattice-pulse.apk — deve dare esattamente l'impronta qui sopra.Non chiediamo di crederci sulla parola: il sorgente del client Android è in rete, e a ogni pubblicazione dell'APK viene riallineato automaticamente, così quello che leggi corrisponde alla versione che hai in mano.
Attenzione a come si legge la barra dei linguaggi di GitHub: quel 93% di
JavaScript riguarda il client Android. Backend, nodi, consenso e ledger sono
~20.000 righe di Rust (49 moduli, framework Axum) e non sono pubblicati: sono la
parte che non deve essere clonata, si installano come immagine firmata e la loro
matematica è dimostrata a parte in
Dimostrazione QBFT 7 su 10. Le primitive crittografiche di quel Rust sono però pubbliche e si provano da sole (cargo test, 28 prove): github.com/Jabo86/lattice-crypto. Il tunnel di resistenza alla
censura è in Go (libXray). La crittografia che protegge i messaggi gira sul
dispositivo, quindi sta nel repository che puoi leggere.
Cosa non c'è, e per scelta: le chiavi di firma, le credenziali dei servizi di notifica e il codice del server. L'assenza di quei file non impedisce la verifica, perché il confronto riguarda il contenuto dell'APK, non la firma.
Quante richieste di dati abbiamo ricevuto, cosa il server può e non può vedere, il canarino firmato, e la cronologia completa della verifica riproducibile (11 → 3 → 0 voci diverse). Una pagina di numeri e di cose controllabili, non di aggettivi.
Il canarino è sorvegliato ogni giorno: un controllo automatico verifica la firma e la data e scrive canary-status.json, che l'app legge e mostra nella sezione Sicurezza come VERIFICATO o ALLARME. L'app calcola la scadenza da sola: se la dichiarazione non viene rifirmata, lo dice senza che il server debba collaborare — che è l'unico modo in cui un canarino ha senso.
La firma prova che l'APK viene da noi. Non prova cosa c'è dentro. L'unica cosa che lo prova è ricompilare il sorgente e ritrovarsi lo stesso binario. Perché sia possibile servono tre pezzi, e diciamo dove siamo su ognuno:
sha256 413d5f7c8c0a1c0e7930832534a677453d34eed294c08560865dcc503ed48ced): tutto quello che serve a compilare, con dentro
README-VERIFICA.md passo per passo e ESCLUSIONI.md che elenca cosa non
c'è e perché. L'archivio è deterministico anche lui: rifarlo dallo stesso codice dà lo stesso
file, byte per byte.Due impronte, e vanno tenute distinte:
apk_sha256 — l'impronta del file intero. Serve a verificare che il file che
avete scaricato è quello che abbiamo pubblicato. Non è riproducibile: la firma Android v2/v3
contiene valori casuali, quindi due firme dello stesso contenuto danno file diversi.content_sha256 — l'impronta di tutto il contenuto dell'APK, voce per
voce dello zip, con i soli file di firma esclusi. Questa è riproducibile: chi ricompila lo
stesso sorgente ottiene esattamente lo stesso valore.La via breve: un comando. Un'immagine che contiene già tutto — JDK 17, SDK e NDK, Go, Node, yarn — scarica l'APK pubblicato, controlla che sia quello dichiarato nel manifest, ricompila tutto dal sorgente (nucleo del trasporto compreso) e confronta voce per voce. Non installa niente sul vostro computer.
curl -O https://lattice-network.it/downloads/Dockerfile.verify curl -O https://lattice-network.it/downloads/verify-apk.sh docker build -t lattice-verify -f Dockerfile.verify . docker run --rm --privileged lattice-verify 2.3.2
Serve --privileged per un motivo
preciso e non negoziabile: i compilatori nativi incorporano il percorso assoluto dei sorgenti
dentro le librerie .so, quindi la compilazione avviene da un percorso canonico
montato con un bind mount. Senza quello il codice sarebbe identico e le librerie diverse.
La via lunga, a mano. Servono JDK 17, Android SDK con NDK, Go 1.26+, Node 20+, yarn, e privilegi di root per il percorso canonico:
curl -O https://lattice-network.it/downloads/lattice-pulse-src-2.3.2.tgz curl -O https://lattice-network.it/downloads/repro-2.3.2.json tar xzf lattice-pulse-src-2.3.2.tgz && cd lattice-pulse-2.3.2 yarn install --frozen-lockfile echo "sdk.dir=$ANDROID_HOME" > android/local.properties # la nostra chiave di firma non è nell'archivio, e non ci sarà: generate la vostra. # la firma è esclusa dal confronto, quindi non cambia il risultato. keytool -genkeypair -keystore android/app/debug.keystore -storepass android \ -keypass android -alias androiddebugkey -keyalg RSA -keysize 2048 \ -validity 10000 -dname "CN=Android Debug,O=Android,C=US" bash tools/repro-build.sh /tmp/mio.apk python3 tools/apkrepro.py check /tmp/mio.apk ../repro-2.3.2.json
Il passo a passo completo è dentro l'archivio, in README-VERIFICA.md.
Lo strumento elenca quali voci non combaciano, non solo che qualcosa non torna: se una
differenza esiste, si vede dove. Le build sono deterministiche perché fissiamo tutto ciò che
normalmente cambia da una volta all'altra — fuso orario, lingua di sistema, la data incorporata
(SOURCE_DATE_EPOCH, pubblicata nel manifest), le cache di compilazione, che vengono
buttate.
La prova, e vi raccontiamo anche come è andata, perché il modo in cui è andata è la parte utile. Ricompilare due volte nella nostra cartella dava subito 1.663 voci su 1.663 identiche: un risultato che sembra una vittoria e non vale niente, perché nessuno verifica dalla nostra cartella. La prova vera è estrarre l'archivio pubblicato altrove, con un'altra chiave di firma, e confrontare. È andata così:
1° tentativo 11 voci diverse su 1663 librerie native e dex
2° tentativo 3 voci diverse su 1663 dex e profili ART
3° tentativo 0 voci diverse su 1663 RIPRODUCIBILE (v2.1.0)
v2.2.0 0 voci diverse su 1662 RIPRODUCIBILE, nucleo Xray compreso
v2.3.0 build 157 1 voce diversa su 1662 resources.arsc: dentro c'era l'IP
della macchina che compila
v2.3.0 build 158 0 voci diverse su 1662 RIPRODUCIBILE da un
container pulito, non dalla nostra cartella
Le prime undici erano i compilatori nativi che incorporano il percorso assoluto dei
sorgenti dentro le .so: codice identico, librerie diverse. Risolto compilando sempre
da un percorso canonico — /lattice-src, un bind mount — sia noi sia chi verifica; ed
è per questo che servono i privilegi di root. Le ultime tre erano un buco nell'archivio: mancava
modules/app-disguise, un modulo nativo locale che sta fuori da src/ e
android/. Senza di esso il progetto compila comunque — semplicemente senza quel
modulo, nove classi in meno nel dex. Un difetto che nessuna dichiarazione avrebbe trovato: l'ha
trovato la verifica, fatta per davvero.
Il risultato finale, ripetibile da chiunque: un APK compilato solo partendo dall'archivio pubblicato, in un'altra cartella e con un'altra chiave, ha 1.663 voci su 1.663 identiche byte per byte a quello che scaricate da qui.
Le impronte della v2.1.0:
content_sha256 8cb4f342092d81a2039002ba0ca072fc6457bd06faa22e544bb588fcde0e794d
apk_sha256 48f465a7f5c9b77e53ed33b2eb6e4f9cbef4db9904533bf99a818c27d7172159
src_sha256 5aaf50101455645c11cbf68bbe3e00461e88a1548d7d071d7fc183cb855a2dc6
libv2ray.aar ba84bad0493194eefe8c53df8085bf7e34e260483b4d92772a873cd82c07ae33 (compilato da noi)
firma APK RSA 4096 · SHA-256 del certificato
45:11:53:AA:02:9A:67:B9:61:29:DA:64:76:27:3B:3D:9E:6F:A4:34:25:2F:CD:2C:8E:17:2B:9D:8D:AF:F1:C3
SOURCE_DATE_EPOCH 1757000000
La build di pubblicazione è la stessa che pubblichiamo: rebuild-apk.sh passa da
repro-build.sh, non da un comando a mano. Se fossero due catene diverse, il manifest
sarebbe carta straccia. Dalla v2.1.0 il manifest di riproducibilità viene pubblicato
accanto a ogni versione, come repro-<versione>.json, con l'elenco completo delle
impronte voce per voce; il riassunto in chiaro sta in repro.txt.
Per la v2.0.0 e le precedenti non c'è, perché sono state compilate prima della catena
deterministica: un manifest che nessuno può verificare è una promessa vuota.
Il buco che avevamo dichiarato qui — libv2ray.aar, il nucleo Xray del trasporto
offuscato, un binario compilato da qualcun altro e messo dentro l'APK così com'era — dalla
v2.2.0 non c'è più. Quel nucleo si compila qui, da sorgente, con Go e gomobile, a ogni build.
Non è più un file versionato: è un prodotto della compilazione, e infatti è scomparso
dall'archivio del sorgente. Nella stessa verifica che ha confrontato l'APK, il nucleo è stato
ricompilato dall'archivio pubblicato e ha dato lo stesso .aar, byte per byte. Da qui in
avanti ogni byte eseguibile dell'APK viene da un compilatore che gira sul nostro server, su
codice che pubblichiamo.
Resta la fiducia negli strumenti: il compilatore Go, il JDK, l'SDK e l'NDK di Android non li scriviamo noi. Ogni build del mondo ha questo limite. Dirlo è l'unica cosa che possiamo fare in più degli altri.
Onestà: la riproducibilità dimostra che quel binario viene da quel sorgente. Non dimostra che il sorgente non abbia difetti — per quello serve un audit esterno, che non abbiamo ancora e che continuiamo a dire di non avere.
Documentazione · 1 / 3
Lattice non è una singola app: è una rete con quattro pezzi, di cui solo uno è nostro e quell'uno è progettato per essere il meno informato di tutti. Qui c'è tutto quello che fa, senza sconti: se un pezzo ha un limite, il limite è scritto.
| Pezzo | Che cos'è | Chi lo controlla |
|---|---|---|
| Lattice Pulse | L'app Android che scarichi qui. Dentro c'è tutta la crittografia: chiavi, cifratura, decifratura, firma. Il testo in chiaro non esce mai da questo perimetro. | Il tuo telefono |
| Il relay | Il nostro server (Rust/Axum + MongoDB). Fa il postino di buste chiuse e il tabellone dove depositare la propria chiave pubblica. Non ha le chiavi private di nessuno. | Noi — ma cieco |
| Nodo Sovrano | La rete mesh fra telefoni vicini: Wi-Fi, Wi-Fi Direct e Bluetooth LE. Instrada a cipolla su 4 salti ibridi (ML-KEM-768 + X25519 per strato), con pacchetti tutti della stessa dimensione, e dentro trasporta le stesse buste del triplo ratchet. Funziona a internet spento. sperimentale | Nessuno: sono i telefoni |
| Canale on-premise | Per le aziende: lo stesso relay installato sui server del cliente, così le buste non passano nemmeno da noi. | Il cliente |
| Funzione | Come è protetta |
|---|---|
| Chat 1 a 1 | Cifratura end-to-end con triplo ratchet ibrido: una chiave nuova per ogni messaggio, una chiave nuova su curva X25519 a ogni cambio di turno e una radice nuova ML-KEM-768 ogni pochi turni. Per leggere una riga servono rotte entrambe le matematiche. Chi copia il tuo telefono viene tagliato fuori dopo un solo scambio. |
| Gruppi | Una busta cifrata per ciascun destinatario (nessuna chiave di gruppo condivisa da revocare): chi esce dal gruppo non riceve più nulla, senza rinegoziare niente. |
| Canali broadcast | Chiave di canale AES-256 imbustata con ML-KEM-768 per ogni iscritto. Il server distribuisce il testo cifrato e non conosce la chiave. |
| Foto, file, note vocali | Cifrati sul telefono con AES-256-GCM prima del caricamento. Il server conserva un blocco opaco: non può generare anteprime né vedere il tipo di contenuto. |
| Chiamate audio/video | WebRTC punto-a-punto con DTLS-SRTP. Le offerte di sessione (SDP) sono firmate con ML-DSA-65, quindi il server non può inserirsi come terzo ascoltatore. Se la rete impone un relay, il TURN vede pacchetti cifrati. |
| Verifica del contatto | QR code e numero di sicurezza a 60 cifre derivato dalle impronte delle due identità: se i numeri combaciano, nessuno si è messo in mezzo. |
| PIN, PIN di panico, autodistruzione | Il database locale è cifrato con una chiave legata al dispositivo. Il PIN di panico sblocca un profilo innocuo e cancella il vero. Con l'autodistruzione le chiavi vengono azzerate e i dispositivi revocati sul server. |
| Backup cifrato | Archivio esportabile, chiave derivata dalla tua password con scrypt (N=2¹⁴, r=8, p=1) e contenuto in AES-256-GCM. La password non lascia il telefono e non esiste alcun recupero: se la perdi, il backup è definitivamente illeggibile. |
| Nodo Sovrano, Ponte, Corriere | Chat sulla mesh senza internet, con lo stesso triplo ratchet delle chat via server: una chiave nuova per ogni messaggio anche qui. Se un vicino ha rete la tua busta esce da lui (Ponte); se nessuno ce l'ha, un vicino la porta con sé sigillata e la consegna quando ritrova la rete (Corriere). |
| Aggiornamenti | Nessun Play Store. Il manifest latest.json è firmato con ML-DSA-65: l'app rifiuta ogni APK la cui firma o impronta SHA-256 non combacia. |
Documentazione · 2 / 3
Un modello di minaccia serve a poco se elenca solo le vittorie. Questa tabella dice, per ogni avversario, se Lattice regge ✔, se regge in parte ~ o se non vi protegge ✘.
| Avversario | Esito | Perché |
|---|---|---|
| Chi ascolta la rete (Wi-Fi, operatore, ISP) | ✔ | Tutto è dentro TLS e, sotto, già cifrato end-to-end. Il traffico è indistinguibile dal rumore e le dimensioni sono normalizzate. |
| Il nostro server, o chi ce lo sequestra | ✔ | Il server non ha chiavi private: conserva buste opache e chiavi pubbliche. Nessun IP registrato, nessun log dei contenuti, nessuna rubrica caricata, nessun hash di password da rubare. Quello che sa lo trovate due righe più sotto, alla voce grafo sociale. |
| Un computer quantistico (anche fra 15 anni) | ✔ | Nessuna curva ellittica nella riservatezza dei messaggi: chiavi e sessioni usano ML-KEM-768, le firme ML-DSA-65. Chi registra oggi il traffico per decifrarlo domani non ci riesce. |
| Uno che si mette in mezzo (MITM sulle chiavi) | ~ | Le chiavi pubbliche arrivano dal nostro tabellone: se il server mentisse, potrebbe darvi una chiave sbagliata. È esattamente per questo che esiste il numero di sicurezza: verificate il contatto una volta e l'inganno diventa impossibile. |
| Chi ti prende il telefono sbloccato in mano | ✘ | Nessuna crittografia salva un dispositivo già aperto. Mitigazioni: PIN, PIN di panico, blocco automatico, autodistruzione. |
| Chi vuole ricostruire il tuo grafo sociale | ~ | Per consegnare un messaggio il relay deve sapere a chi consegnarlo: nei record nuovi l'indirizzo di chi scrive non c'è più (resta solo un indice fra i partecipanti), ma la coppia che conversa il server la conosce. Non i contenuti, non i nomi dei gruppi, non gli allegati: chi parla con chi. Chi non vuole nemmeno questo ha tre strade reali, tutte già nel prodotto: la cassetta postale anonima (qui sotto), il canale on-premise dove il relay è vostro, e il Nodo Sovrano dove non c'è nessun relay. Lo scriviamo perché è l'unica cosa che il nostro server sa di voi. |
| Chi vuole sapere che usi Lattice | ✘ | Il tuo indirizzo IP contatta un nostro server: chi osserva la tua linea vede che parli con
lattice-network.it. Il contenuto no, il fatto sì. Il tunnel offuscato riduce il
problema ma non lo elimina. |
| Analisi dei tempi sulla mesh locale | ~ | Su una Wi-Fi condivisa un osservatore può seguire in parte le cadenze degli inoltri. Le abbiamo rese casuali e aggiungiamo traffico di copertura, e da questa versione nemmeno la destinazione di uno strato è più leggibile senza rompere due matematiche — ma il problema dei tempi non è risolto: per questo il Nodo Sovrano resta sperimentale. |
| Chi ti costringe a consegnare la password | ✘ | Nessun software risolve la coercizione. Il PIN di panico è una risposta parziale e va preparata prima, non dopo. |
Cosa non promettiamo: non abbiamo ancora un audit esterno indipendente pubblicato, e la nostra crittografia non è ibrida (non affianca cioè una curva classica a ML-KEM). Sono le due cose su cui stiamo lavorando, e le trovate scritte anche nella roadmap qui sotto.
Documentazione · 3 / 3
Questa è la specifica reale, quella che corrisponde al codice pubblicato in questa versione.
Ogni numero qui sotto è verificabile: sono le stesse costanti che trovate nei moduli
crypto.js, ratchetCore.js e mesh/onion.js dentro l'APK.
| Scopo | Algoritmo | Parametri |
|---|---|---|
| Apertura sessione | ML-KEM-1024 (FIPS 203) | pk 1568 B · ct 1568 B — NIST livello 5, il massimo previsto dallo standard. Protegge la radice, cioè il segreto più longevo: si paga una volta per sessione |
| Passo del ratchet | ML-KEM-768 (FIPS 203) | pk 1184 B · ct 1088 B · segreto 32 B — NIST livello 3, rifatto ogni pochi turni: qui il parametro alto sarebbe banda buttata |
| Firme | ML-DSA-65 (FIPS 204) | pk 1952 B · firma 3309 B — NIST livello 3 |
| Cifratura autenticata | AES-256-GCM | IV 96 bit casuale · tag 128 bit · AAD sull'intestazione |
| Derivazione di chiavi | HKDF-SHA-256 | etichette di dominio separate per radice, catena e aggancio |
| Impronte e ID | SHA-256 / SHA-512 | ID nodo mesh = primi 16 B di SHA-256(pk) |
| Password del backup | scrypt | N=2¹⁴, r=8, p=1 → 32 B |
Nessun RSA, nessun Diffie-Hellman su curve nella riservatezza dei messaggi. La libreria è
@noble/post-quantum, implementazione pura in JavaScript senza codice nativo opaco.
L'identità di un utente è una coppia di chiavi ML-DSA-65 generata sul telefono: la privata non viene mai trasmessa, e sta nel file chiave che l'utente custodisce. L'accesso non usa password: il server manda un nonce casuale, il telefono lo firma, il server verifica la firma con la chiave pubblica che ha in archivio. Non esiste quindi alcun hash di password da rubare, e un archivio del server compromesso non permette a nessuno di autenticarsi come voi.
Ogni telefono registra inoltre una propria chiave di dispositivo ML-KEM-768 più un fondo di chiavi usa-e-getta. Un dispositivo revocato o inattivo viene ripulito automaticamente insieme alle sue chiavi orfane.
L'apertura di una sessione mescola due segreti di natura diversa: un'incapsulazione ML-KEM-768 sulla chiave d'identità post-quantistica del destinatario e un Diffie-Hellman X25519 effimero contro la sua chiave d'identità su curva. I due segreti vengono concatenati e passati a HKDF: per ricostruire la radice bisogna rompere entrambi i mondi, i reticoli e le curve ellittiche. È la ragione per cui esiste PQXDH, ed è la ragione per cui l'abbiamo adottato: ML-KEM è una primitiva del 2024, e non riteniamo prudente appoggiarci solo a lei.
La radice iniziale è inoltre legata alla trascrizione: dipende dall'identità di chi riceve, dall'identificativo della chiave usa-e-getta consumata, dai cifrati e dal dispositivo mittente. Un incapsulamento catturato non è riutilizzabile in nessun altro contesto — niente unknown key-share, niente riuso in una sessione diversa.
La chiave usa-e-getta viene distrutta nell'istante in cui ha aperto la sessione, non dopo giorni. È la differenza fra forward secrecy adesso e forward secrecy fra una settimana.
E l'apertura usa ML-KEM-1024, il livello NIST 5: il parametro più alto che lo standard definisce. Non è simmetria estetica, è dove va messo il margine — la radice sopravvive a tutta la conversazione, mentre un passo del ratchet vive pochi turni e lì il livello 3 è la scelta giusta. La chiave di livello 5 nasce dallo stesso segreto d'identità con un'etichetta diversa: niente in più da custodire, e il numero di sicurezza non cambia, così chi si è già verificato non vede falsi allarmi. Il livello dichiarato è coperto dal dato autenticato della busta: farlo scendere per strada fa fallire la decifratura, non declassa la sessione in silenzio. Con chi ha una versione precedente si resta a 768, come prima.
Apertura della sessione ss1 = ML-KEM-1024.Encaps(IK_kem_destinatario) -> ct1 (livello NIST 5) ss2 = X25519(eph_mittente, IK_curva_destinatario) -> epk tr = "…init-v2" | H(IK_kem_dest ‖ IK_curva_dest) | pk_id | ct1 | epk | dev_mittente rk = HKDF-SHA256(ikm = ss1 ‖ ss2, info = tr)
Tre catene lavorano insieme, e ognuna risolve un problema diverso.
| Catena | Cadenza | Che cosa garantisce |
|---|---|---|
| Simmetrica HKDF-SHA-256 | ogni messaggio | Forward secrecy. La chiave del messaggio viene usata una volta e cancellata: quella di ieri non esiste più da nessuna parte. |
| Su curva X25519 | ogni cambio di turno | Post-compromise security continua. Chi ha copiato il telefono viene tagliato fuori dopo un solo scambio. Costa 32 byte: si può fare sempre. |
| Post-quantistica ML-KEM-768 | ogni pochi turni | Rinnovo post-quantistico. Costa 2,6 KB fra chiave e cifrato: farlo per scrivere «ok» sarebbe uno spreco. La sua eredità resta però dentro la radice per sempre. |
Turno nuovo (a ogni cambio di direzione) (xpk, xsk) = X25519.KeyGen() -> xpk nell'intestazione (32 B) ssX = X25519(xsk, xpk_altro) ssKem = ML-KEM-768.Encaps(pk_altro) -> solo nei turni post-quantistici rk' , ck = HKDF-SHA256(ikm = ssKem ‖ ssX, salt = rk, info = "…root-v2", 64 B) Ogni messaggio ck' , mk = HKDF-SHA256(ck, info = "…chain-v2", 64 B) busta = AES-256-GCM(mk, iv, AAD = intera intestazione).Encrypt(testo) mk viene usata una volta sola e cancellata
Una precisazione che conta, e la diciamo per non essere fraintesi: la riservatezza è sempre ibrida, anche nei turni in cui il passo ML-KEM non viene rifatto, perché il suo segreto è già cotto dentro la radice e la radice non dimentica nulla. Quello che è rado è soltanto la velocità di rinnovo post-quantistica, cioè quanto tempo un intruso post-quantistico resta dentro dopo aver copiato il telefono: al massimo tre turni o venti minuti, non uno solo. Contro un intruso classico il taglio resta immediato, a ogni singolo turno.
Una chiave pubblica ML-KEM-768 pesa 1184 byte, un cifrato 1088. Trasmetterli in ogni messaggio, in esadecimale, portava una riga di testo a 11.477 byte. Quando una busta del genere deve viaggiare sulla mesh — cioè con il Ponte o il Corriere, dove un pacchetto è di 8192 byte fissi con 3652 byte utili — significava spezzare un singolo messaggio in quattro pacchetti, ognuno con quattro salti: sedici trasmissioni radio per dire «arrivo». Più frammenti, più probabilità di perderne uno, più occasioni per un osservatore.
| Messaggio di testo | Peso | Pacchetti mesh |
|---|---|---|
| Ratchet precedente (v1) | 11.477 byte | 4 |
| Triplo ratchet, turno post-quantistico | 3.259 byte | 1 |
| Triplo ratchet, turno normale | 191 byte | 1 |
Come: la chiave ML-KEM non viaggia più in ogni messaggio ma si richiama con un'impronta da 8 byte, e tutto passa da esadecimale a base64. E una proprietà che ci teniamo stretta: ogni messaggio porta con sé tutto il necessario per aprirsi. Non esiste un «messaggio chiave» che, perdendosi, blocca la catena — i messaggi possono arrivare in qualunque ordine, che è esattamente quello che succede su una radio.
Fuori ordine e perdite: fino a 300 messaggi possono essere scavalcati in un colpo e 600 chiavi restano da parte per i ritardatari. Una sessione desincronizzata viene riagganciata automaticamente dalla catena di guarigione, invece di lasciare messaggi illeggibili.
Ogni dispositivo annuncia le proprie capacità. Se il telefono dell'altro sa fare l'ibrido e la
sua chiave su curva è disponibile, si usa il triplo ratchet (rat2). Altrimenti si
scende al ratchet post-quantistico puro (rat1), che è ciò che c'era prima —
mai a nulla di più debole, e mai al silenzio. Chi ha una versione molto vecchia riceve ancora le
buste a chiave usa-e-getta. Il livello effettivo di ogni chat è scritto nella schermata di
verifica del contatto, dentro l'app: non ve lo chiediamo per fede.
I gruppi non usano una chiave condivisa: ogni messaggio viene cifrato una volta per ciascun destinatario con la sua sessione a doppio ratchet. Costa più banda, ma dà due proprietà che una chiave di gruppo non può dare: chi esce non può leggere il futuro senza che si debba rinegoziare nulla, e la compromissione di un membro non espone gli altri. I canali broadcast, dove i destinatari sono migliaia, usano invece una chiave AES-256 di canale imbustata con ML-KEM-768 per ogni iscritto.
Con il Nodo Sovrano attivo i telefoni si trovano da soli su Wi-Fi (UDP broadcast), Wi-Fi Direct e Bluetooth LE, e si scambiano pacchetti senza alcun server. Le chiavi mesh — sia quella ML-KEM sia quella su curva — sono derivate dall'identità: nessuna chiave in più da custodire. L'ID pubblico di un nodo resta un'impronta a 16 byte della sola chiave ML-KEM, così un telefono aggiornato e uno vecchio continuano a riconoscersi e la rete non si spacca in due.
Formato del pacchetto — invariabile
dimensione 8192 byte, sempre, per ogni tratta
salti 4 (3 relay + il nodo che consegna)
per strato ct ML-KEM-768 1088 B + curva effimera X25519 32 B + IV 12 B
cifrato: next(16) + flag(1) + len(2) + tag(16)
chiave di strato HKDF-SHA256( ss_MLKEM ‖ ss_X25519 , info = "…mesh-onion-v3" )
contenuto utile 3524 byte, sempre a lunghezza fissa
riempimento byte casuali, indistinguibili dal cifrato
Ogni relay sa esattamente due cose: da chi ha ricevuto lo strato e a quale ID consegnarlo. Non sa quanti salti sono passati, quanti ne restano, quanto è lungo il messaggio, né chi sia il mittente. E da questa versione, per sapere anche solo a chi è diretto uno strato servono rotte entrambe le matematiche: chi registra oggi il traffico radio per ricostruirlo domani con un computer quantistico deve anche rompere le curve ellittiche, e viceversa. Il prezzo è 32 byte per strato — 128 su 8192, cioè 128 byte di contenuto utile in meno. Le cadenze di inoltro sono randomizzate e viene generato traffico di copertura per attenuare l'analisi dei tempi.
Se anche un solo salto del percorso ha una versione precedente dell'app, l'intero pacchetto scende alla cipolla post-quantistica pura: un percorso a geometria mista non esiste, sarebbe un pacchetto con due forme diverse. Un nodo aggiornato apre comunque anche gli strati vecchio stile.
Fino alla versione 1.8.0 questo era il punto più debole di tutto il sistema, e lo scrivevamo: sulla rete locale il testo era protetto soltanto dalla chiave mesh del destinatario, che nasce dalla sua identità e non ruota mai. Nessuna forward secrecy, nessuna auto-guarigione: chi avesse copiato un telefono avrebbe letto tutto il traffico mesh passato e futuro.
Adesso dentro la cipolla c'è la stessa busta del triplo ratchet ibrido che passerebbe dal server. Una chiave nuova per ogni messaggio, una curva nuova a ogni turno, una radice post-quantistica rinnovata. È diventato possibile solo grazie all'intestazione snella della 1.8.0: una busta a regime pesa ~200 byte invece di 11,5 KB, e in un pacchetto mesh ci sta. Prima non ci sarebbe entrata.
Quando la busta non ci sta — succede al primo messaggio, quello che porta l'aggancio della sessione, perché contiene tre pezzi di materiale crittografico — viene spezzata in pezzi, ognuno dentro la sua cipolla e per la sua strada, e ricomposta a destinazione. I pezzi vivono solo in memoria e scadono in cinque minuti: sul telefono non resta nulla. Per i relay sono pacchetti da 8192 byte come tutti gli altri.
Cosa resta fuori, e lo diciamo: sulla mesh viaggia il testo. Foto, file e note vocali richiedono ancora il server. E se una sessione ratchet non esiste ancora — un contatto con cui non si è mai scambiato niente, e nessuna rete per aprirla — il messaggio parte comunque col formato precedente invece di restare a terra: mai il silenzio, mai qualcosa di più debole di quello che c'era prima.
Servono a un caso preciso: tu non hai rete, il destinatario è lontano. La busta è già cifrata punto-a-punto, quindi può viaggiare in mano a chiunque.
| Situazione | Cosa accade |
|---|---|
| Ponte un vicino ha internet |
La busta parte sulla mesh come cipolla a 4 salti verso quel vicino, che la spinge al relay con un ticket anonimo prenotato in precedenza dal mittente. Consegna immediata. |
| Corriere nessuno ha internet |
La busta viene affidata a un vicino qualsiasi, che la prende in custodia sigillata e la consegna appena ritrova una connessione: anche ore dopo, anche chilometri più in là. |
Cosa non può fare chi vi trasporta: leggere il messaggio (è cifrato per il destinatario), sapere chi sia il mittente o il destinatario (non appaiono nel pacchetto), alterare la busta (l'AAD del ratchet non tornerebbe), o fingere di aver consegnato. Nel suo telefono resta solo un numero: quanti pacchetti sta portando. Il ticket è un gettone di spedizione anonimo che non contiene l'identità di chi lo ha chiesto. Limiti volutamente severi perché nessuno possa usare il vostro telefono come deposito: massimo 20 pacchetti in custodia, scadenza a 24 ore, poi vengono buttati. Gli allegati non passano da qui: restano al server, la mesh trasporta solo testo.
Alla voce «grafo sociale» qui sopra abbiamo scritto che il relay conosce la coppia che conversa. Questa è la risposta, e non è una promessa: è una modalità che si accende.
Con la cassetta anonima i messaggi non partono più a nome del tuo account. Vengono depositati a un indirizzo casuale — una cassetta che non è collegata alla tua identità — usando gettoni monouso che il tuo contatto ti consegna dentro la chat già cifrata. Il deposito avviene senza autenticarsi: il server non ha nulla a cui associarlo.
| Cosa il server vede di un deposito | |
|---|---|
| Il testo del messaggio | ✘ mai |
| L'indirizzo Lattice di chi scrive | ✘ mai |
| L'indirizzo Lattice di chi riceve | ✘ mai |
| L'identificativo della conversazione | ✘ mai |
| L'identificativo del dispositivo | ✘ mai |
| Che due account Lattice si stiano parlando | ✘ mai |
| Un gettone monouso e un blocco opaco | ✔ questo, e nient'altro |
Due depositi consecutivi non hanno un solo byte in comune: gettone diverso, incapsulazione diversa, cifrato diverso, e nessun campo che permetta di metterli in fila. Un gettone speso non si può riusare. Le notifiche sono agganciate alla cassetta e non alla tua identità, quindi il telefono si sveglia senza che il server sappia di chi è l'account.
Fino alla versione 2.0.0 questa modalità aveva un prezzo che abbiamo sempre dichiarato: dentro la busta anonima viaggiava la cifratura a colpo singolo, quindi accenderla significava rinunciare al ratchet. Adesso dentro il deposito c'è la stessa busta del triplo ratchet ibrido di tutte le altre chat: chiave nuova per ogni messaggio, curva nuova a ogni turno, auto-guarigione. Accenderla non costa più nulla in robustezza.
Cosa resta visibile, perché va detto: il primo scambio di gettoni passa dalla chat normale. Il server vede una volta che vi siete scritti, e da quel momento in poi la conversazione diventa opaca e ogni messaggio successivo è inaccostabile agli altri. È una modalità da accendere su entrambi i telefoni, in «PIN app e autodistruzione»: la teniamo volontaria perché cambia il modo in cui i messaggi vengono consegnati, e una cosa così non si accende alle spalle di chi la usa.
Il problema: la chiave con cui gli altri aprono una conversazione nuova con voi protegge la radice di quella conversazione, cioè il segreto più longevo che esiste, e resta esposta su un tabellone pubblico per tutta la sua vita. Ma non si può cambiare l'identità per ricambiarla: il numero di sicurezza cambierebbe, e ogni contatto che vi ha verificato vedrebbe un allarme. È il motivo per cui, in pratica, nessuno la ruota mai.
La soluzione è separare le due cose. L'identità — ML-DSA-65 per le firme, la ML-KEM-768 e la curva X25519 su cui si calcolano numero di sicurezza e codice QR — resta ferma alla generazione 0 per sempre. Accanto, si pubblica una coppia di chiavi d'aggancio (X25519 + ML-KEM-1024) che porta un contatore di generazione e si ricambia da sola ogni trenta giorni, oppure a mano quando volete, da «Chiavi d'aggancio» nelle impostazioni.
Ogni generazione è derivata dal segreto d'identità con una separazione di dominio sul numero di generazione: non se ne conserva nessuna, si rifanno quando servono. Sul filo non viaggia quale generazione è stata usata: chi riceve prova la corrente e le sei precedenti (circa mezzo anno per chi è stato offline) e vince quella che apre. Serve una nota, perché è un errore facile: ML-KEM non fallisce mai la decapsulazione — con la chiave sbagliata restituisce un segreto pseudocasuale, senza dire niente. Quindi la generazione giusta non si riconosce dalla decapsulazione, ma solo aprendo per davvero il primo messaggio: il tentativo si fa su una copia dello stato, e nessun campo in più finisce nella busta. Un tentativo in più su una conversazione nuova, zero informazioni regalate a chi guarda.
Il limite, detto chiaro: siccome le generazioni sono derivate dal segreto d'identità, chi rubasse il vostro file chiave otterrebbe tutte le generazioni, passate e future. La rotazione accorcia la vita di una chiave pubblicata e riduce quanto vale registrare traffico oggi sperando di romperlo domani; non sostituisce la custodia del file chiave, e non difende un telefono già compromesso. Contro quello lavora il ratchet, che ricambia le chiavi a ogni turno.
Il problema, in un gruppo offline, è banale e fastidioso: cinque membri, quattro a portata di radio e uno lontano. Prima non partiva niente per nessuno — la consegna era tutto-o-niente, e un solo membro assente bloccava il messaggio anche per chi era nella stessa stanza.
Adesso la consegna è per destinatario. Chi si è presentato sulla mesh riceve subito. Chi non si è mai presentato torna al ponte o alla coda, ma con solo le sue buste: ogni busta porta scritto per chi è, quindi ridurre l'elenco è esatto e nessuno riceve il messaggio due volte.
E poi il passaparola. La copia destinata a un membro che non si vede adesso viene affidata a un membro che si vede: appena quello incrocia il destinatario, gliela consegna. Non è una funzione nuova del protocollo, è la custodia del corriere applicata dentro un gruppo. Chi fa da corriere vede il nome del destinatario — ma è nel gruppo, quindi lo sapeva già — e un blocco di byte che non può leggere (il contenuto è cifrato col ratchet fra mittente e destinatario) né modificare (il sigillo è calcolato col segreto mesh di quella coppia, che lui non ha). Se la stessa copia arriva sia per via diretta sia per passaparola, il destinatario ne tiene una: il doppione si riconosce dal sigillo, identico. La custodia ha un tetto di 40 copie e scade dopo 12 ore, così nessuno può usare il telefono di un altro come magazzino.
Un dettaglio che il banco di prova ha scovato e che vale la pena raccontare, perché è il tipo di errore che si scrive senza accorgersene: il passaparola, così com'era, avrebbe potuto trasportare anche il formato precedente (v1), il cui corpo è protetto dai soli strati della cipolla. Ma il membro-corriere sbuccia il suo strato — quindi lo avrebbe letto. Adesso la custodia si affida solo alle buste del ratchet. Un messaggio in chiaro non si dà a nessuno, nemmeno a un membro del gruppo: meglio che resti in coda.
Il caso non è ipotetico: il telefono è stato fuori dalle vostre mani. Un controllo alla frontiera, un fermo, un albergo. Chi l'ha avuto in mano può aver copiato dei file, e i file si copiano. Due cose, in particolare, valgono la fatica: lo stato delle sessioni e i segreti delle chiavi usa-e-getta non ancora consumate, che stanno sul telefono per definizione.
Un tocco, sei passaggi, in quest'ordine — e l'ordine non è arbitrario:
Cronologia, identità e numero di sicurezza non si toccano. Un passaggio che fallisce (niente rete) non ferma gli altri e finisce nella ricevuta col suo nome, invece di sparire.
Il limite, dichiarato dentro la schermata stessa: l'identità ML-DSA-65 non si rifà, perché cambiarla vorrebbe dire cambiare numero di sicurezza e far scattare un allarme a ogni contatto verificato. Quindi se il sospetto è che vi abbiano copiato IL FILE CHIAVE, o che il telefono sia stato modificato a livello di sistema, quel pulsante non basta: serve l'autodistruzione e un'identità nuova su un telefono diverso.
L'APK non passa da nessuno store. Il manifest degli aggiornamenti contiene versione, dimensione e
impronta SHA-256, ed è firmato con ML-DSA-65 dalla chiave di produzione: l'app scarica, ricalcola
l'impronta e verifica la firma, e rifiuta il file se una delle due non combacia. Potete rifare la
stessa verifica a mano con sha256sum, ed è il motivo per cui l'impronta sta scritta in
cima a questa pagina.
| Voce | Stato | Cosa cambierebbe |
|---|---|---|
| Ratchet ibrido (curva + post-quantistico) | fatto · v1.8.0 | X25519 accanto a ML-KEM in handshake e a ogni turno: per leggervi servono rotti entrambi. |
| Aggancio legato alla trascrizione | fatto · v1.8.0 | La radice iniziale dipende da identità, chiave usa-e-getta e cifrati. La usa-e-getta viene distrutta all'istante, non dopo giorni. |
| Passo post-quantistico rado | fatto · v1.8.0 | Il costo ML-KEM si paga ogni pochi turni invece che a ogni messaggio: 191 byte per una riga di testo invece di 11.477. |
| Cipolla mesh ibrida (v3) | fatto · v1.9.0 | ML-KEM-768 + X25519 in ognuno dei quattro strati. Costo reale: 128 byte su 8192. |
| Ratchet sulla chat mesh | fatto · v1.9.0 | Era l'ultimo punto in cui la chiave non ruotava mai. Adesso sulla mesh viaggia la stessa busta del triplo ratchet, con frammentazione quando serve. |
| Allegati sulla mesh | in valutazione | Note vocali e immagini spezzate in pacchetti mesh. La frammentazione ora esiste: è il pezzo che mancava. |
| Rotazione delle chiavi d'aggancio | fatto · v2.1.0 | Curva X25519 e ML-KEM-1024 d'aggancio ruotano ogni trenta giorni, o a mano. Identità, numero di sicurezza e codice QR non si muovono: chi vi ha verificato non vede allarmi. |
| ML-KEM-1024 nell'aggancio | fatto · v2.0.0 | Livello NIST 5 dove protegge la radice. Numero di sicurezza invariato, nessun declassamento possibile per strada. |
| Sigillo mittente col ratchet dentro | fatto · v2.0.0 | La cassetta anonima ora trasporta il triplo ratchet invece della cifratura a colpo singolo: accenderla non costa più robustezza. |
| Sigillo mittente acceso di serie | dopo le prove sul campo | Cambia il modo in cui i messaggi vengono consegnati: prima va provato in mano alle persone, poi si valuta se diventi il modo normale di funzionare. |
| Chat di gruppo sulla mesh | fatto · v2.2.0 | Consegna per destinatario invece di tutto-o-niente, e passaparola: la copia di chi non si vede la porta un membro che si vede. Sulla mesh passa solo testo, per scelta: allegati e vocali hanno bisogno dei blob sul server, e una radio locale non è il posto dove spingere megabyte. Il P2P serve a far arrivare le parole quando non c'è altro. |
Compilare libv2ray.aardal sorgente | fatto · v2.2.0 | Il nucleo Xray si compila da sorgente con Go e gomobile a ogni build, e due compilazioni danno lo stesso file. Zero binari importati dentro l'APK. |
| Build riproducibili | fatto · v2.1.0 | Due compilazioni indipendenti dello stesso sorgente danno 1.663 voci identiche byte per
byte. Si pubblica il content_sha256 di ogni versione e lo strumento che dice
quale voce non combacia. La differenza fra fidarsi e verificare. |
| Audit esterno indipendente | non ancora | Finché non c'è, l'unica garanzia che possiamo darvi è il codice verificabile e le prove automatiche che pubblichiamo. Lo diciamo, invece di far finta. |