Questo documento descrive il modello di sicurezza dell'app iOS e, soprattutto,
i punti in cui differisce dal desktop. Le primitive crittografiche non sono
descritte qui: sono le stesse del core, e la fonte è
SECURITY.md dell'upstream
insieme a FORMAT_SPEC.md.
Usare la segnalazione privata di GitHub (Security → Report a vulnerability) sul repository, non una issue pubblica. Se riguarda il formato o il core crittografico, va segnalata sul repository Cryptera: questo repository contiene l'interfaccia iOS, non la crittografia.
È la regola architetturale che tiene in piedi tutto il resto. Il core
crypto_core_rs è consumato come dipendenza git con tag pinnato, e Swift si
occupa solo di interfaccia, accesso ai file e presentazione degli errori.
Anche l'orchestrazione — costruzione del TAR, suffissi, sanificazione dei nomi,
parametri dei profili — vive in Rust (rust/cryptera-ffi), perché determina il
contenuto del file prodotto. Una differenza lì produrrebbe file che il
desktop apre in modo diverso, o non apre affatto.
La compatibilità non è affermata, è verificata a ogni esecuzione dei test: file prodotti dal desktop vengono decifrati dal codice iOS e confrontati byte per byte (milestone M7, il gate di rilascio).
- Nessuna richiesta di rete. Nessuna telemetria, nessun crash reporter di
terze parti, nessun SDK esterno. Un controllo automatico verifica sul
.appprodotto che non siano linkati framework di rete (scripts/check-release-bundle.sh). - Nessun aggiornamento in-app. L'updater firmato del desktop non è stato portato, ed è una scelta: su App Store è vietato, e un controllo versione che rimandi altrove lo è altrettanto.
- Nessun recupero password. Non esiste, né lato app né altrove. Una password persa significa dati persi, e l'app lo dice esplicitamente prima della prima cifratura.
- Nessun logging diagnostico. Non c'è alcuna chiamata a
os_log,Loggeroprintnel codice dell'app: non esiste un percorso per cui un nome di file finisca nei log di sistema, perché non si scrive nulla nei log.
È il limite più importante di questo porting, e non va nascosto.
Lato Rust la password è avvolta in Zeroizing, quindi viene azzerata quando esce
di scope. Ma prima di arrivare lì attraversa Swift, dove è una String — un tipo
a valore, con storage gestito dal runtime, copiabile implicitamente e senza alcun
modo documentato di sovrascriverne la memoria. SecureField protegge dalla
cattura schermo, non dalla persistenza in RAM.
In pratica: copie della password possono restare nella memoria del processo finché l'allocatore non le riusa. Non si dichiara quindi una zeroizzazione end-to-end, perché non esiste.
Cosa la mitiga: la memoria del processo non è leggibile da altre app su iOS senza un exploit del kernel, e i campi password vengono svuotati a operazione conclusa. Cosa non la mitiga: un attaccante con esecuzione di codice nel processo, o un dump di memoria su un device compromesso.
Allineato all'upstream, e in contrasto con SPEC §6.3, che vieta di sovrascrivere
in silenzio. La regola è pensata per l'output di un singolo file — dove infatti
si restituisce OUTPUT_EXISTS — mentre per una cartella servirebbe decidere fra
rifiutare, rinominare o chiedere conferma. È una scelta di prodotto ancora
aperta, non una svista: cambiarla solo su iOS produrrebbe una divergenza
silenziosa dal desktop.
Argon2 con profilo Paranoid richiede 512 MiB. Su iOS il superamento del limite
jetsam non è un'eccezione catturabile: il sistema termina il processo. L'app
verifica la memoria disponibile prima di avviare e rifiuta esplicitamente invece
di provarci.
I parametri non vengono mai abbassati in silenzio. Cambiare argon2_mem
cambia la chiave derivata: un downgrade silenzioso produrrebbe un file che
l'utente crede protetto da un profilo e che invece lo è da un altro. Meglio un
errore. In decifratura i parametri arrivano dall'header e non sono negoziabili.
L'hardening (M10) è completo e verificato su device. Questa sezione elenca ciò che è implementato; sotto, ciò che resta fuori e perché — perché un documento di sicurezza che descrive le intenzioni come se fossero fatti è peggio dell'assenza del documento.
Implementato:
| Ambito | Trattamento |
|---|---|
| Registro operazioni | .completeFileProtection |
| Output decifrati di sessioni interrotte | Cancellati all'avvio successivo |
| Memoria prima di avviare | Verificata con os_proc_available_memory(); il profilo viene rifiutato se non ci sta, mai abbassato in silenzio. Misurata su device: il modello predice il consumo reale al MiB |
| Operazione interrotta da una sospensione | L'app chiede a iOS tempo aggiuntivo; alla scadenza annulla invece di lasciarsi sospendere a metà scrittura, così l'output parziale viene rimosso e non resta un .ecf troncato |
| File di lavoro e temporanei | .completeUnlessOpen sulla cartella, ereditata dai file che vi crea il core. Non .complete: un'operazione in corso mentre lo schermo si spegne fallirebbe a metà |
| Miniatura di sistema in secondo piano | Coperta da una schermata neutra a partire da .inactive, cioè prima che iOS la scatti: non vi compaiono nomi di file |
| Memoria in decifratura | Verificata sui parametri dell'header prima di derivare la chiave, per decrypt, verify e batch. Il rifiuto spiega che la quantità l'ha decisa chi ha cifrato e non è abbassabile: chi apre non ha scelto nulla |
| Manifesto di privacy | PrivacyInfo.xcprivacy: nessun dato raccolto, nessun tracciamento. Le API a motivazione obbligatoria dichiarate sono quelle realmente usate — UserDefaults, spazio disco, metadati dei file — ricavate cercandole nel codice. check-release-bundle.sh verifica che il manifesto sia nel bundle e continui a dichiarare zero raccolta |
Fuori perimetro finché l'app non si pubblica (M11 — vedi
IMPLEMENTATION_PLAN.md):
| Ambito | Stato |
|---|---|
ITSAppUsesNonExemptEncryption |
Non dichiarata, e per ora non richiesta. È export compliance: serve alla prima submission. Finché non si pubblica, nell'Info.plist resta un commento — una chiave sbagliata lì è peggio di una assente |
Non è una lacuna di sicurezza: è una dichiarazione amministrativa che ha senso solo davanti a una distribuzione.
La suite completa gira su un iPhone 14 Pro (iOS 26.5.2) ed è verde, e le classi di Data Protection sono verificate lì — in simulatore quei test saltano, perché il filesystem non riporta affatto la classe.
Le due verifiche non automatizzabili sono state eseguite a mano sullo stesso device e riferite come superate: VoiceOver acceso sulle cinque schermate (ordine di lettura, progresso udibile, errori annunciati) e app in secondo piano durante un'operazione lunga (la miniatura di sistema mostra la copertura, senza nomi di file). Sono annotate come prove eseguite una volta, non come garanzie che un test ripete a ogni esecuzione.
Il codice errore DEVICE_LOCKED esiste ed è registrato nel log delle
operazioni, ma non ha ancora un trattamento dedicato nell'interfaccia.
SecureFieldper le password; mostrare in chiaro è sempre una scelta esplicita dell'utente, mai lo stato iniziale.SecureFieldimpedisce anche la cattura del contenuto negli screenshot di sistema.- Il campo
messagegrezzo di un errore non viene mai mostrato: è diagnostico e può contenere percorsi. L'utente vede la stringa localizzata mappata dalcode. - Il registro delle operazioni memorizza solo il nome del file, mai il suo percorso, e solo codici di errore stabili — mai il messaggio diagnostico.
- Resta sul dispositivo: non viene mai trasmesso, perché l'app non ha rete.
Il nome originale memorizzato nell'header è scelto da chi ha creato il file e
il core non lo sanifica oltre UTF-8 e lunghezza. Non diventa mai un percorso
senza passare da safe_output_name, che ne tiene solo l'ultimo componente:
URL.appendingPathComponent non neutralizza né le risalite né i percorsi
assoluti.
L'estrazione dei TAR rifiuta le entry con .., i percorsi assoluti e i link
(simbolici o hard) che puntano fuori dalla cartella di destinazione — compreso il
caso in cui il nome dell'entry è pulito ma il link porta altrove.
La compressione dell'archivio si deduce dai magic bytes, mai dal nome: il
nome può mancare del tutto (hide_filename), e dedurla da lì produceva file che
l'app non sapeva riaprire.
| Controllo | Dove |
|---|---|
| Round-trip incrociato col desktop, byte per byte | CrypteraTests/CrossCompatTests |
| Compatibilità coi formati v1–v5 | CrypteraTests/FormatCompatTests |
| Recupero FEC e confine oltre il quale fallisce | CrypteraTests/FecRecoveryTests |
| Manomissione dell'header | CrypteraTests/DecryptFlowTests |
| Nessun framework di rete, nessuna fixture in Release | scripts/check-release-bundle.sh |
| Contrasto dei colori (WCAG 2.1) | CrypteraTests/DesignSystemContrastTests |
Girano tutte in CI a ogni push (.github/workflows/ios.yml).