Documentazione/documentazione.md

25 KiB

Protocollo TCN

Descrizione del protocollo

Il protocollo TCN [@TCNCoalitionTCN2020], Temporary Contact Number, è un protocollo di tracciamento dei contatti decentralizzato, ideato dalla TCN Coalition. Quest'ultima è una comunità di persone che durante l'emergenza COVID-19 hanno sviluppato questo supporto per lo sviluppo di applicazioni per notificare l'esposizione al contagio. Il protocollo non richiede, informazioni personali ed è compatibile con autorità sanitarie. I dispositivi degli utenti inviano un identificativo agli utenti vicini tramite Bluetooth e successivamente, se un utente è risultato positivo al contagio, può notificarlo ai suo contatti.

Per aumentare la scalabilità la TCN Coalition ha scelto di non generare randomicamente tcn, ma di generarli in modo deterministico da un seed. In questo modo si riduce la dimensione del report da inviare, in quanto contiene solo le informazione necessarie per calcolare i tcn e non l'elenco intero di tcn. L'utente può caricare diversi report di dimensioni minori in modo da non caricare l'intera cronologia dei contatti.

Chiavi di autenticazione e verifica

L'user agent crea una chiave di autorizzazione rak (report authorization key) e una chiave di verifica rvk (report verification key) attraverso la curva ellittica Ed25519.

A partire dalla rak è possibile ricavare la chiave di contatto iniziale tck_0.

tck_0 ← H_tck(rak)

dove H_tck è una funzione di hash con 256 bit di output che utilizza come separatore di dominio la rappresentazione in byte della stringa H_TCK.

Chiave temporanea di contatto

Ogni report può contenere al massimo 2^{16} chiavi di contatto.

Partendo un tcn se ne può derivare il prossimo tcn come mostrato di seguito:

tck_i ← H_tck(rvk || tck_{i-1})

dove || indica una concatenazione.

Numeri temporanei di contatto

Per ogni tck viene generato un numero di contatto temporaneo come mostrato di seguito:

tcn_i ← H_tcn(le_u16(i) || tck_i)

dove H_tcn è una funzione di Hash con 128 bit di output che utilizza come separatore di dominio la rappresentazione in byte della stringa H_TCN .

Dalla chiave rak è possibile generare la chiave rvk e la chiave tck_0, dalle quali è possibile generare le successive tck e quindi tutti i successivi tcn.

Report

Quando viene richiesto di caricare i dati dell'utente, è possibile eseguire l'upload di un report compatto. Infatti grazie alla derivazione deterministica, è possibile inviare tutti gli identificativi utilizzati nel periodo j1 e j2, caricando unicamente la rvk, tck_{j-1}, e i due indici j1 e j2. Di seguito è illustrato come costruire il report:

report ← rvk || tck_{j1-1} || le_u16(j1) || le_u16(j2) || memo

dove memo è una stringa di byte di lunghezza variabile che va da 2 a 257 byte. Inoltre si utilizza il rak per produrre una firma sig per il report e verrà inviata al server la concatenazione di sig e report.

Poiché la rvk è contenuta all'interno del report, chiunque può verificare l'integrità del report e ricavare i vari.

Implementazione del protocollo per la JMV

Al fine di utilizzare il protocollo precedentemente descritto all'interno dell'applicazione Android, ne è stato sviluppato un'implementazione per la Java Virtual Machine. La gestione della coppia di chiavi derivate dalla curva ellittica Ed25519 è stata affidata alla libreria ed25519-elisabeth [@CryptographycafeEd25519elisabeth2020]. Le chiavi private e pubbliche prodotte sono state wrappate rispettivamente nelle classi ReportAuthorizationKey e ReportVerificationKey. Questa scelta non solo ha permesso di utilizzare nomi dal maggiore significato rispetto al dominio applicativo, ma anche di nascondere l'implementazione della curva Ed25519 in modo da disaccoppiare l'interfaccia della libreria crittografica da quella utilizzata per il protocollo TCN.

Come illustrato nella @sec:tck, a partire dalla chiave di autorizzazione è possibile ricavare la TemporaryContactKey iniziale1. Ciò può essere fatto traverso la funzione baseTemporaryContactKey(), la cui implementazione è riportata nel @lst:tck-0. Invece, partendo da una chiave di contatto generica, è possibile ricavare la successiva chiave di contatto attraverso il metodo nextTemporaryContactKey() riportato nel listato @lst:next-tck.

fun baseTemporaryContactKey(): TemporaryContactKey {
  val hmac = MessageDigest.getInstance("SHA-256").apply {
    update(Const.H_TCK_DOMAIN_SEPARATOR)
    update(key.toByteArray())
}

  return TemporaryContactKey.createFromByteArray(
    hmac.digest(),
    0
  )
}
fun nextTemporaryContactKey(
  rvk: ReportVerificationKey
): TemporaryContactKey {
  val hmac = MessageDigest.getInstance("SHA-256").apply {
    update(Const.H_TCK_DOMAIN_SEPARATOR)
    update(rvk.toByteArray())
    update(key)
  }

  return TemporaryContactKey(
    hmac.digest(),
    index.inc()
  )
}

Sempre a partire dalla tck è possibile ricavare il numero di contatto temporaneo (tcn) e da esso l'UUID utilizzato all'interno dei beacon bluetooth. Questa operazione può essere eseguita mediante la funzione deriveTemporaryContactNumber() la cui implementazione è stata riportata nel @lst:derive-tcn.

fun deriveTemporaryContactNumber(): TemporaryContactNumber {
  val hmac = MessageDigest.getInstance("SHA-256").apply {
    update(Const.H_TCN_DOMAIN_SEPARATOR)
    update(index.toLeByteArray())
    update(key)
  }

  return TemporaryContactNumber(
    hmac.digest().sliceArray(0 until 16),
    index
  )
}

Un'altra componente fondamentale dell'implementazione del protocollo TCN è la classe Report, infatti tramite essa è possibile generare il report firmato che poi sarà inviato al server. Inoltre questa classe mette a disposizione una serie di funzioni di utilità come generateContactNumbers() e toReportData() che facilitano l'estrazione delle informazioni contenute all'interno del report. Infine la classe report fornisce anche un metodo statico, readReportDataFromByteArray(), attraverso il quale altre componenti applicative sono in grado di recuperare le informazioni di un report contenute all'interno di un array di bytes. L'implementazione di questo metodo è riportata nel listato @lst:read-report.

fun readReportDataFromByteArray(bytes: ByteArray): ReportData {
  val buffer = ByteBuffer.wrap(bytes).apply {
    order(ByteOrder.LITTLE_ENDIAN)
  }

  val rvk = ReportVerificationKey
    .createFromByteArray(buffer.read(32))
  val tckBytes = buffer.read(32)
  val from = buffer.short
  val until = buffer.short
  val memoType = buffer.get()
  val memoData = String(buffer.read(buffer.get().toInt()))

  return ReportData(
    rvk,
    TemporaryContactKey
      .createFromByteArray(tckBytes, from.dec()),
    from,
    until,
    memoData
  )
}

Applicazione

L'applicazione permette di tracciare i contatti degli utenti attraverso l'impiego del Bluetooth Low Energy (BLE). In particolare lo smartphone di ogni utente si comporta sia da trasmittente di beacon bluetooth che da ricevente. In questo modo quando due utenti entrano nel raggio di azione del bluetooth il contatto verrà memorizzato sui rispettivi dispositivi.

L'applicazione prevede differenti modalità di funzionamento, ognuna delle quali garantisce un diverso livello di privacy. Nella modalità di funzionamento A ogni qual volta si verifica un contatto l'applicazione si occupa di notificare immediatamente l'evento al server in modo tale che esso possa essere aggiunto al database remoto. Questa modalità è quella meno privacy friendly in quanto la comunicazione avviene in real-time e all'interno del messaggio scambiato viene riportato sia l'UUID dell'utente sia quello della persona incontrata.

La modalità B prevede lo scambio delle stesse informazioni previste per la modalità precedente, ma solo se richiesto dalle autorità sanitarie. In questo modo non solo si evita che i dati siano catturati dal server in real-time, ma si espongono le informazioni dell'utente solo quando queste sono strettamente necessarie. Sia in questa modalità, che nella precedente si è scelto di non ruotare gli UUID identificativi degli utenti in modo da facilitare la generazione del grafo sul server. Questa soluzione può mettere a repentaglio la privacy degli utenti ed essere sfruttata da avversari per ottenere informazioni sulle abitudini degli utilizzatori2.

L'ultima modalità, la C, è quella che tutela maggiormente la privacy degli utilizzatori attraverso due accorgimenti:

  • Rotazione degli UUID
  • Matching locale

La generazione degli UUID avviene attraverso una derivazione deterministica come visto nella @sec:tcn-protocol, in modo tale da avere lo stesso livello di privacy di una soluzione randomica, ma con una migliore scalabilità. Mentre il matching locale permette di condividere il minor numero di informazioni possibili e solo quando questo è strettamente necessario. Infatti in questa modalità l'applicazione carica le informazioni sul server solo in seguito alla richiesta delle autorità sanitarie. Inoltre a differenza delle prime due modalità è previsto l'upload unicamente degli UUID che il dispositivo ha assunto nel tempo, in questo modo il server non è in grado di conoscere o ricavare i contatti avuti dall'utente.

API covid di Apple e Google

La prima scelta progettuale che abbiamo dovuto affrontare ha riguardato l'utilizzo o meno dell'API messa a disposizione da Apple [@AppleGooglePartner] e Google [@NotificheDiEsposizione]. Infatti i due giganti californiani hanno sviluppato in modo congiunto un protocollo che permettesse di tracciare i contatti degli utenti dei due sistemi operativi mobili più diffusi. Questo protocollo ricorda per molte caratteristiche il protocollo TCN illustrato precedentemente. Entrambi i protocolli ricavano gli identificativi (rolling proximity identifier nel protocollo di Apple e Google) in modo deterministico a partire da delle chiavi (temporary exposure keys). I due protocolli differiscono nel numero di identificativi ricavati da ciascuna chiave; come visto nella @sec:tcn il protocollo TCN ricava un unico tcn per chiave, mentre l'altro protocollo permette di ricavare un numero imprecisato di identificativi. Un'ulteriore differenza risiede nella derivazioni delle chiavi, che nel protocollo TCN è deterministica, mentre in quello di Apple e Google non è specificato.

Oltre al protocollo, le due società, hanno fornito agli sviluppatori un API che permettesse alle applicazioni di di utilizzare il protocollo descritto nel paragrafo precedente. Sfortunatamente l'API non ci avrebbe permesso di implementare tutte e tre le modalità di funzionamento, ma solo l'ultima che si basa su un matching locale. Questa caratteristica, unita alle regole stringenti di utilizzo imposte della libreria3 ci ha condotto alla scelta di implementare le varie funzionalità in maniera indipendente.

Bluetooth

L'interazione tra l'hardware bluetooth del dispositivo e l'applicazione è stata gestita attraverso l'impiego della libreria Android Beacon Library [@AndroidBeaconLibrary] che permette di gestire più facilmente le operazioni con beacon bluetooth. Inoltre per rendere l'applicazione più funzionale, e quindi garantirne il funzionamento anche in background o a schermo spento è stato utilizzato un foreground service [@ServicesOverview], che consente di mantenere in primo piano le operazioni di trasmissione e scansione anche quando l'applicazione non lo è. Data la natura variegata di Android, le diverse implementazioni del sistema operativo adoperate dai vari produttori non si comportano sempre nello stesso modo, motivo per il quale alcuni dispositivi tenderanno a terminare, o mettere in pausa ugualmente l'applicazione4. Potendo opera unicamente nello spazio utente non è stato possibile superare questi limiti.

Tutte le funzionalità legate al bluetooth sono state incapsulate all'interno della classe BluetoothManager. L'interfaccia di questa classe espone due metodi, startService() e stopService() che consentono di avviare e stoppare sia la scansione che la trasmissione del beacon. Poiché queste operazioni vanno ad interagire con le funzionalità del sistema operativo, l'istanza di questa classe deve essere collegata ad un oggetto di tipo Context. Si è scelto di collegare l'oggetto BluetoothManager all'application e non ad una Activity in quanto i servizi devono essere utilizzati anche quando non sono presenti activity in foreground. Per questo motivo è stata sviluppata anche una classe BluetoothApplication che va ad estendere le funzionalità di Application e fornisce a sua volta due metodi di start e stop che vanno a richiamare quelli esposti da BluetoothManager in modo tale da rendere possibile il controllo dei servizi legati al bluetooth anche da altre componenti dell'applicazione.

Trasmissione

Il dispositivo dell'utente deve eseguire il broadcast di un beacon bluetooth contenete l'UUID identificativo. Questa operazione è stata svolta attraverso la classe BeaconTransmitter messa a disposizione dalla Android Beacon Library. Inoltre per la modalità di funzionamento C è stato necessario prevedere un meccanismo di rotazione delle chiavi. Questa rotazione viene settata attraverso la funzione rotateTCN() che sfrutta un Handler per programmare la rotazione dell'UUID.

private fun rotateTCN() {
  val advertiseHandler = Handler()
  val changeTCN: Runnable = object : Runnable {
    override fun run() {
      tcnManager.nextTcn()
      startAdvertising()
      advertiseHandler.postDelayed(
        this,
        TCNManager.ELAPSE_BETWEEN_NEW_TCN
      )
    }
  }

  advertiseHandler.postDelayed(
    changeTCN,
    TCNManager.ELAPSE_BETWEEN_NEW_TCN
  )
}

La scelta della frequenza di advertising è stata dettata dai vincoli tracciati dall'API di Android [@AdvertiseSettings]. Infatti la libreria permette di trasmettere un beacon con una frequenza di 1 Hz, 3 Hz o 10 Hz. Fortunatamente questi vincoli non si sono rilevati troppo limitanti infatti la frequenza di un Hertz, quindi un beacon trasmetto ogni secondo, permette di avere una buona trasmissione e di risparmiare batteria. Inoltre in fase di scanning evita che siano registrate più interazioni nello stesso ciclo.

Sempre attraverso l'API di Android è stata settata la potenza di trasmissione del beacon. Anche in questo caso la scelta era limitata a poche alternative:

  • HIGH
  • MEDIUM
  • LOW
  • ULTRA_LOW

Com'è possibile dedurre anche dai nomi dei vari livelli, l'API non fornisce nessuna stima quantitativa5, ma solo delle indicazioni qualitative delle intensità del segnale trasmesso. L'individuazione del livello più adatto è stata svolta per via sperimentale utilizzando cinque dispositivi differenti. I due livelli più alti sono stati immediatamente scartati in quanto permettevano di rilevare i beacon a distanze elevate cosa che avrebbe minato la bontà dell'applicazione. Con il livello ULTRA_LOW si è notato che venivano rilevate unicamente le interazioni inferiori al metro in contesti free space. Poiché l'organizzazione mondiale della sanità raccomanda una distanza di almeno un metro [@AdvicePublicCOVID19] questo livello di trasmissione non consente di rilevare contatti potenzialmente a rischio. Per questo motivo si è scelto di utilizzare il livello LOW che permette di rilevare contatti fino a circa due metri.

Scansione

Le operazioni di scansione sono meno limitate dalle funzionalità dell'API di Android e per questo motivo c'è stata maggiore libertà di scelta. In particolare è stato possibile settare sia l'intervallo temporale che deve intercorrere tra una scansione e la successiva, sia la durata della singola scansione. Si è scelto di far trascorrere un minuto tra una scansione e la prossima e di avere una scansione della durata di un secondo. Per quanto detto già in precedenza in base ai vari dispositivi e alle varie condizioni di funzionamento l'intervallo tra una scansione e la successiva potrebbe essere più ampio rispetto a quello stabilito. Entrambi i parametri sono stati settati tramite una costante in modo tale da poter configurare facilmente il comportamento dell'applicazione.

Quando l'applicazione rivela un beacon nelle vicinanze esso viene trasmesso ad un'ulteriore componente applicativa tramite l'impiego del LocalBroadcastManager @BroadcastsOverview. Questa componente non consuma direttamente il beacon, ma ha il compito di smistarlo ad ulteriori componenti in base alla modalità di funzionamento dell'applicazione. Il codice necessario a smistare i dati di contatto è stato riportato nel @lst:contact-receiver. Come si può notare nella linea 2, la prima operazione consiste nel recupero dei dati di contatto dall'Intent, mentre dalla linea 7 si seleziona la funzione da invocare in base alla modalità di funzionamento.

// ...
val contactData = intent?.getSerializableExtra(CONTACT_DATA_KEY)
  as ContactData?

val mode = getMod(context)

val onMode = when (mode) {
  Mode.MOD_A -> this::contactOnModeA
  Mode.MOD_B -> this::contactOnModeB
  Mode.MOD_C -> this::contactOnModeC
}

onMode(context, contactData)

Nel caso della modalità A il beacon viene trasmesso alla classe NetworkReceiver che si occupa di trasmettere il contatto al server remoto. Mentre nel caso delle modalità B e C il beacon viene consumato dalla classe StoreReceiver la quale si occupa della memorizzazione permanente del contatto all'interno di un database locale.

Stima della distanza

In base all'intensità dell segnale (rssi) misurato dal dispositivo ricevente è possibile ottenere una stima della distanza che intercorre tra chi invia il beacon e chi lo riceve attraverso l'@eq:distanza. Per poter calcolare la distanza è necessario conoscere anche il valore di n e TxPower. n è una costante che generalmente assume valori compresi tra uno e quattro e ci permette di modellare i diversi ambienti in cui si può operare. Generalmente s'impone n pari a due quando si ipotizza di lavorare in ambienti free space.


d = 10^{\frac{TxPower - rssi}{10 \cdot n}}
$$ {#eq:distanza}

$TxPower$ è la potenza di trasmissione nominale che si misurerebbe alla distanza di un metro dalla sorgente del segnale.
Il valore di $TxPower$ deve essere precedentemente ricavato per ogni emettitore e deve essere inviato all'interno del beacon bluetooth.
Lavorando con dispositivi eterogenei tra di loro non è stato possibile calcolare in modo esatto questo valore ma si è scelto di utilizzare un valore che mediamente si adattasse a tutti i dispositivi utilizzati in fase di test.

## UI



## Memorizzazione

In base alla modalità di funzionamento l'applicazione deve memorizzare diversi tipi di dati.
La gestione della persistenza è stata realizzata attraverso la libreria ***Room*** [@RoomPersistenceLibrary], una componente di *Jetpack* [@AndroidJetpackAndroid] la suite di librerie supportate da *Google*.
*Room* fornisce un layer astratto che permette di operare più facilmente con il database *SQLite* sottostante.

La memorizzazione dei contatti è avvenuta tramite lo schema riportato nel @lst:contact-data.
Questi dati vengono conservati solo nella modalità di funzionamento *B* e *C* poiché nella modalità *A* il contatto viene comunicato immediatamente al server per cui non è necessaria una memorizzazione locale.

``` {.markdown #lst:contact-data caption="Schema utilizzato per la memorizzazione dei dati di contatto."}
- `id`: Int [PrimaryKey | AutoGenerate]
- `uuidReceiver`: String
- `uuidSender`: String
- `rssi`: Int
- `txPower`: Int
- `timestamp`: Long
```

Nella modalità *C* è necessario memorizzare anche le tck utilizzate nel corso del tempo (si veda la @sec:tck per maggiori dettagli).
Oltre alla memorizzazione della tck, tramite un array di byte, è necessario memorizzare anche l'indice associato ad essa e il timestamp di primo utilizzo.
Lo schema della tabella utilizzata per la memorizzazione di queste informazioni è riportato nel @lst:tck-data.

``` {.markdown #lst:tck-data caption="Schema utilizzato per la memorizzazione delle tck."}
- `index`: Short [PrimaryKey]
- `timestamp`: Long,
- `tck`: ByteArray
```

Questi dati persistenti sono stati acceduti mediante l'utilizzo di due *Data Access Object* (DAO).
Le interfacce dei DAO utilizzate sono riportate nel @lst:dao.

``` {.kotlin #lst:dao caption="Interfacce dei Data Access Objects."}
@Dao
interface ContactDataDao {
   @Query("SELECT * FROM contact_data")
   suspend fun getAllContactData(): List<ContactData>

   @Insert
   suspend fun insert(cn: ContactData)
}

@Dao
interface TCNDataDao {
    @Query("SELECT * FROM tcn_data")
    suspend fun getAllTCNData(): List<TCNData>

    @Query("SELECT * FROM tcn_data WHERE `index` == :index")
    suspend fun getByIndex(index: Short): TCNData

    @Insert
    suspend fun insert(tcnData: TCNData)
}
```

## Rete

La comunicazione con il server avviene mediante un brocker MQTT fornito da un altro gruppo di studenti.
Come implementazione del client MQTT si è scelto di utilizzare *Paho* [@EclipsePahoMQTT], un client realizzato da Eclipse.
Questa libreria oltre a fornire un client MQTT per la JVM fornisce anche un *service* per Android che permette di sollevare lo sviluppatore da alcuni dettagli implementativi.

L'applicazione, all'interno dell'architettura, svolge il ruolo di *publisher* e si occupa della pubblicazione di due tipologie di messaggi:

- ***Messaggi di contatto***:
  utilizzati sia nella modalità *A* che nella *B*, permettono di notificare al server un contatto tra due utenti.
  Nel caso della modalità *A* viene svolto un invio in *real-time*, mentre nella configurazione *B* l'invio avviene solo dopo aver eseguito l'*upload*.
- ***Messaggi di report***:
  utilizzati esclusivamente nella modalità *C*.
  Questi messaggi trasportano come *payload* il report TCN discusso nella @sec:report e vengono inviati solo quando l'*upload* è richiesto dall'utente.

Anche in questo caso si è scelto di *wrappare* la libreria utilizzata all'interno di una classe sviluppata in proprio.
Poiché l'unica funzionalità di nostro interesse è la *publish* è stato necessario scrivere un'unica funzione statica che si occupa di eseguire quest'operazione.
Questa funzione, riportata nel @lst:mqtt-publish, permette di specificare il topic e il contenuto del messaggio da pubblicare.
Inoltre attraverso una *lambda expression* è possibile specificare come comportarsi in caso di errori.

``` {.kotlin #lst:mqtt-publish caption="Funzione wrap che consente la publicazione di un messaggio MQTT."}
fun publish(
    context: Context,
    topic: String,
    payload: ByteArray,
    onFailure: () -> Unit = LOG_ERROR
) {
  val clientId = MqttClient.generateClientId()
  val client = MqttAndroidClient(
    context.applicationContext,
    BROKER_URL,
    clientId
  )

  client.connect().actionCallback = object: IMqttActionListener {
    override fun onSuccess(asyncActionToken: IMqttToken?) {
      val msg = MqttMessage(payload).apply {
        qos = 2
      }

      client.publish(topic, msg).apply {
        actionCallback = Disconnect(client)
      }

    }

    override fun onFailure(
      asyncActionToken: IMqttToken?,
      exception: Throwable?
    ) {
      onFailure()
    }
  }
}
```

# Riferimenti 

  1. La chiave temporanea iniziale viene ricavata a partire dalla sola rak e da essa non è generato nessun numero temporaneo di contatto. ↩︎

  2. Per esempio una catena di negozi attraverso l'impiego di uno scanner bluetooth potrebbe ricostruire la fedeltà degli utenti, conoscere i settori del negozio preferiti ecc. ↩︎

  3. Le due società californiane consentono lo sviluppo di applicazioni che fanno uso della loro API solo ad enti governativi e impongo un numero massimo di applicazioni per nazione pari ad uno. Dalla documentazione ufficiale non è ben chiaro se questi vincoli riguardino solo la pubblicazione sugli store o anche lo sviluppo. ↩︎

  4. Molti produttori Android per aumentare la durata della batteria dei propri dispositivi tendono a stoppare e ridurre le funzionalità delle applicazioni. Maggiori dettagli possono essere trovati al seguente link \url{https://dontkillmyapp.com}. ↩︎

  5. D'altronde, data la natura non omogenea dei vari dispositivi Android, una stima quantitativa sarebbe stata impossibile da ottenere. ↩︎