Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Archiviazione sicura dei dati su Android

Scroll to top

() translation by (you can also view the original English article)

Credibilità di un'app oggi altamente dipende dalla modalità di gestione dati privati dell'utente. Lo stack Android ha molte potenti API circostante credenziale e archiviazione delle chiavi, con specifiche caratteristiche disponibili solo in determinate versioni. Questa breve serie partirà con un approccio semplice per arrivare fino e in esecuzione controllando il sistema di archiviazione e come crittografare e memorizzare i dati sensibili tramite un codice di accesso fornito dall'utente. Nel secondo tutorial, vedremo più complessi modi di proteggere le chiavi e le credenziali.

Le nozioni di base

La prima domanda a pensare è la quantità di dati è effettivamente necessario acquisire. Un buon approccio è quello di evitare di memorizzare dati privati, se davvero non devi.

Per i dati che è necessario memorizzare, l'architettura Android è pronto ad aiutare. Dal 6.0 Marshmellow, crittografia completa del disco è attivata per impostazione predefinita, per i dispositivi con la capacità. File e SharedPreferences che vengono salvati dall'app vengono impostate automaticamente con la costante MODE_PRIVATE. Ciò significa che i dati sono accessibili solo dalla tua app.

È una buona idea quella di attaccare a questo difetto. È possibile impostare in modo esplicito quando si salva una preferenza condivisa.

1
SharedPreferences.Editor editor = getSharedPreferences("preferenceName", MODE_PRIVATE).edit();
2
editor.putString("key", "value");
3
editor.commit();

O quando si salva un file.

1
FileOutputStream fos = openFileOutput(filenameString, Context.MODE_PRIVATE);
2
fos.write(data);
3
fos.close();

Evitare di archiviare dati su storage esterno, come i dati quindi sono visibili da altri utenti e applicazioni. Infatti, per evitare di fare più difficile per le persone a copiare il file binario app e i dati, è possibile impedire agli utenti di essere in grado di installare l'app su archiviazione esterna. Aggiunta di android:installLocation con un valore di internalOnly per il file manifesto che compirà.

È inoltre possibile impedire l'applicazione e i relativi dati dal backup. Questo impedisce anche di scaricare il contenuto della directory di dati privati di un'applicazione utilizzando adb backup. A tale scopo, è necessario impostare l'attributo android:allowBackup su false nel file manifesto. Per impostazione predefinita, questo attributo è impostato su true.

Queste sono le migliori pratiche, ma non funzioneranno per un dispositivo radicato o compromesso, e crittografia del disco è utile solo quando il dispositivo è protetto con una schermata di blocco. Questo è dove avere una password app-lato che protegge i dati con crittografia è positivo.

Protezione dei dati di utente con una Password

Nascondere è un'ottima scelta per una libreria di crittografia perché si è installato e funzionante molto velocemente senza dovere preoccuparsi per i dettagli sottostanti. Tuttavia, un exploit mirato per un popolare framework influirà simultaneamente tutte le applicazioni che si basano su di esso.

Inoltre è importante essere ben informato sul funzionano dei sistemi di crittografia al fine di essere in grado di dire se si sta utilizzando un particolare framework in modo sicuro. Così, per questo post ci accingiamo a sporcarci le mani guardando direttamente il provider di crittografia.

AES e derivazione della chiave basata su Password

Utilizziamo lo standard AES consigliato, che crittografa i dati dati una chiave. La stessa chiave utilizzata per crittografare i dati viene utilizzata per decrittografare i dati, che si chiama crittografia simmetrica. Ci sono diverse dimensioni chiave e AES256 (256 bit) è la lunghezza preferita per l'utilizzo con dati sensibili.

Mentre l'esperienza utente della tua app dovrebbe obbligare un utente ad utilizzare un codice di protezione forte, c'è una possibilità che lo stesso passcode sarà scelti anche da un altro utente. Mettere la sicurezza dei nostri dati crittografati nelle mani dell'utente non è sicuro. I nostri dati ha bisogno di essere protetti invece con una chiave che è casuale e abbastanza grande (cioè. che ha abbastanza entropia) deve essere considerato forte. È per questo che mai si consiglia di utilizzare una password direttamente per crittografare dati — che è dove una funzione denominata funzione di derivazione di chiave basata su Password (PBKDF2) entra in gioco.

PDKDF2 deriva una chiave da una password hash molte volte sopra con un sale. Questo è chiamato chiave di stretching. Il sale è una sequenza casuale di dati e rende unica la chiave derivata anche se la stessa password è stata utilizzata da qualcun altro. Lascia inizio generando quello sale.

1
SecureRandom random = new SecureRandom();
2
byte salt[] = new byte[256];
3
random.nextBytes(salt);

La classe SecureRandom garantisce che l'output generato sarà difficile prevedere — è un "Generatore numeri casuali crittograficamente sicuro". Ora possiamo mettere il sale e la password in un oggetto di crittografia basata su password: PBEKeySpec. Il costruttore dell'oggetto prende anche un modulo di conteggio di iterazione fare la chiave più forte. Infatti, aumentando il numero di iterazioni espande il tempo che ci sarebbe voluto per operare su un set di chiavi durante un attacco a forza bruta. Il PBEKeySpec viene quindi passato SecretKeyFactory, che infine genera la chiave come una matrice di byte[]. Potremo avvolgere array di byte[] non elaborati in un oggetto SecretKeySpec.

1
char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array

2
PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations

3
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
4
byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
5
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");

Si noti che la password viene passata come un array di char[] e la classe PBEKeySpec memorizza come un array di char[] pure. array di Char[] sono usati solitamente per le funzioni di crittografia perché mentre la classe String non è modificabile, un array di char[] contenente informazioni riservate possono essere sovrascritti — eliminando in tal modo i dati sensibili completamente da phyc del dispositivo RAM.

Vettori di inizializzazione

Ora siamo pronti per crittografare i dati, ma ne abbiamo un'altra cosa da fare. Ci sono diverse modalità di crittografia AES, ma useremo quella consigliata: (CBC) cipher block chaining. Questo opera su nostri dati un blocco alla volta. La cosa grandiosa di questa modalità è che ogni blocco di dati non crittografato successivo è che XOR ha avuto con il precedente blocco cifrato per rendere la crittografia più forte. Tuttavia, significa che il primo blocco non è mai unico, proprio come tutti gli altri!

Se un messaggio da crittografare dovesse iniziare lo stesso un altro messaggio da crittografare, l'output di inizio crittografato sarebbe lo stesso, e che darebbe un utente malintenzionato un indizio per capire che cosa potrebbe essere il messaggio. La soluzione consiste nell'utilizzare un vettore di inizializzazione (IV).

Un IV è solo un blocco di byte casuali che sarà XOR ha avuto con il primo blocco di dati utente. Poiché ogni blocco è dipende da tutti i blocchi elaborati fino a quel punto, l'intero messaggio verrà crittografato in modo univoco — identici messaggi crittografati con la chiave stessa non produrrà risultati identici. Consente di creare un IV ora.

1
SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom

2
byte[] iv = new byte[16];
3
ivRandom.nextBytes(iv);
4
IvParameterSpec ivSpec = new IvParameterSpec(iv);

Una nota riguardo SecureRandom. Sulle versioni 4.3 e sotto, il Java Cryptography Architecture aveva una vulnerabilità a causa di improprio inizializzazione del generatore numero pseudocasuale sottostante (PRNG). Se la destinazione è versioni 4.3 e sotto, è disponibile una correzione.

Crittografia dei dati

Armati con un IvParameterSpec, ora possiamo fare la crittografia effettiva.

1
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
2
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
3
byte[] encrypted = cipher.doFinal(plainTextBytes);

Qui si passa nella stringa "AES/CBC/PKCS7Padding". Questo specifica la crittografia AES con cypher block chaining. L'ultima parte di questa stringa si riferisce PKCS7, che è uno standard consolidato per imbottitura dati che non rientrano perfettamente nella dimensione del blocco. (Blocchi sono a 128 bit, e imbottitura avviene prima della crittografia).

Per completare il nostro esempio, metteremo questo codice in un metodo di crittografia che confezionerà il risultato in un HashMap contenente i dati crittografati, insieme con il vettore di inizializzazione e di sale necessario per la decrittografia.

1
private HashMap<String, byte[]> encryptBytes(byte[] plainTextBytes, String passwordString)
2
{
3
    HashMap<String, byte[]> map = new HashMap<String, byte[]>();
4
    
5
    try
6
    {
7
        //Random salt for next step

8
        SecureRandom random = new SecureRandom();
9
        byte salt[] = new byte[256];
10
        random.nextBytes(salt);
11
12
        //PBKDF2 - derive the key from the password, don't use passwords directly

13
        char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array

14
        PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations

15
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
16
        byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
17
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
18
19
        //Create initialization vector for AES

20
        SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom

21
        byte[] iv = new byte[16];
22
        ivRandom.nextBytes(iv);
23
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
24
25
        //Encrypt

26
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
27
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
28
        byte[] encrypted = cipher.doFinal(plainTextBytes);
29
30
        map.put("salt", salt);
31
        map.put("iv", iv);
32
        map.put("encrypted", encrypted);
33
    }
34
    catch(Exception e)
35
    {
36
        Log.e("MYAPP", "encryption exception", e);
37
    }
38
39
    return map;
40
}

Il metodo di decrittografia

È sufficiente memorizzare il IV e il sale con i vostri dati. Mentre sali e IVs sono considerati pubblici, assicurarsi che non in sequenza vengono incrementati o riutilizzati. Per decrittografare i dati, tutto quello che dobbiamo fare è cambiare la modalità nel costruttore cifrario da ENCRYPT_MODE a DECRYPT_MODE. Il metodo decrypt porterà un HashMap contenente le stesse informazioni richieste (dati crittografati, sale e IV) e restituire una matrice di byte[] decrittografata, dato la password corretta. Il metodo decrypt verrà rigenerata la chiave di crittografia dalla password. La chiave non deve mai essere archiviata!

1
private byte[] decryptData(HashMap<String, byte[]> map, String passwordString)
2
{
3
    byte[] decrypted = null;
4
    try
5
    {
6
        byte salt[] = map.get("salt");
7
        byte iv[] = map.get("iv");
8
        byte encrypted[] = map.get("encrypted");
9
10
        //regenerate key from password

11
        char[] passwordChar = passwordString.toCharArray();
12
        PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256);
13
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
14
        byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded();
15
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
16
17
        //Decrypt

18
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
19
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
20
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
21
        decrypted = cipher.doFinal(encrypted);
22
    }
23
    catch(Exception e)
24
    {
25
        Log.e("MYAPP", "decryption exception", e);
26
    }
27
28
    return decrypted;
29
}

Prova la crittografia e la decrittografia

Per mantenere l'esempio semplice, noi stiamo omettendo il controllo degli errori che assicurerebbe che la HashMap contiene la chiave necessaria, coppie valore. Ora possiamo testare i nostri metodi per garantire che i dati viene decrittografati correttamente dopo la crittografia.

1
//Encryption test

2
String string = "My sensitive string that I want to encrypt";
3
byte[] bytes = string.getBytes();
4
HashMap<String, byte[]> map = encryptBytes(bytes, "UserSuppliedPassword");
5
6
//Decryption test

7
byte[] decrypted = decryptData(map, "UserSuppliedPassword");
8
if (decrypted != null)
9
{
10
    String decryptedString = new String(decrypted);
11
    Log.e("MYAPP", "Decrypted String is : " + decryptedString);
12
}

I metodi utilizzano una matrice di byte[] in modo che è possibile crittografare dati arbitrari anziché solo gli oggetti String.

Salvataggio dei dati crittografati

Ora che abbiamo un array di byte[] crittografati, possiamo salvarlo per deposito.

1
FileOutputStream fos = openFileOutput("test.dat", Context.MODE_PRIVATE);
2
fos.write(encrypted);
3
fos.close();

Se non volevi salvare il IV e il sale separatamente, HashMap è serializzabile con le classi ObjectInputStream e ObjectOutputStream.

1
FileOutputStream fos = openFileOutput("map.dat", Context.MODE_PRIVATE);
2
ObjectOutputStream oos = new ObjectOutputStream(fos);
3
oos.writeObject(map);
4
oos.close();

Salvataggio sicuro dei dati a SharedPreferences

È anche possibile salvare dati sicuri per SharedPreferences della tua app.

1
SharedPreferences.Editor editor = getSharedPreferences("prefs", Context.MODE_PRIVATE).edit();
2
String keyBase64String = Base64.encodeToString(encryptedKey, Base64.NO_WRAP);
3
String valueBase64String = Base64.encodeToString(encryptedValue, Base64.NO_WRAP);
4
editor.putString(keyBase64String, valueBase64String);
5
editor.commit();

Poiché il SharedPreferences è un sistema XML che accetta solo specifici primitive e oggetti come valori, abbiamo bisogno di convertire i nostri dati in un formato compatibile, ad esempio un oggetto String. Base64 permette di convertire i dati non elaborati in una rappresentazione di stringa che contiene solo i caratteri consentiti dal formato XML. Crittografare la chiave e il valore in modo un utente malintenzionato non può capire che un valore potrebbe essere per. Nell'esempio precedente, encryptedKey ed encryptedValue sono entrambe matrici di byte[] crittografati restituite dal nostro metodo di encryptBytes(). Il IV e il sale possono essere salvati nel file di preferenze o come file separato. Per ottenere indietro i byte crittografati dalla SharedPreferences, possiamo applicare un Base64 decodificare la stringa memorizzata.

1
SharedPreferences preferences = getSharedPreferences("prefs", Context.MODE_PRIVATE);
2
String base64EncryptedString = preferences.getString(keyBase64String, "default");
3
byte[] encryptedBytes = Base64.decode(base64EncryptedString, Base64.NO_WRAP);

Dati che puliscono insicuri da vecchie versioni

Ora che i dati memorizzati sono sicuri, può essere il caso che avete una versione precedente dell'app che aveva i dati memorizzati in modo non sicuro. Un aggiornamento, i dati potrebbero essere spazzato via e re-encrypted. Il seguente codice salviette sopra un file utilizzando dati casuali.

In teoria, si possono semplicemente eliminare le preferenze di condivise rimuovendo i file /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml e your_prefs_name.bak, e cancellare le preferenze di in memoria con il seguente codice:

1
getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit();

Tuttavia, anziché tentare di pulire i dati precedenti e la speranza che funziona, è meglio cifrare esso in primo luogo! Questo è particolarmente vero in generale per unità a stato solido che spesso si diffondono fuori dati-scrive a regioni diverse per prevenire l'usura. Ciò significa che anche se si sovrascrive un file nel filesystem, la memoria a stato solido fisica potrebbe conservare i dati nella posizione originale sul disco.

1
public static void secureWipeFile(File file) throws IOException
2
{
3
    if (file != null && file.exists())
4
    {
5
        final long length = file.length();
6
        final SecureRandom random = new SecureRandom();
7
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rws");
8
        randomAccessFile.seek(0);
9
        randomAccessFile.getFilePointer();
10
        byte[] data = new byte[64];
11
        int position = 0;
12
        while (position < length)
13
        {
14
            random.nextBytes(data);
15
            randomAccessFile.write(data);
16
            position += data.length;
17
        }
18
        randomAccessFile.close();
19
        file.delete();
20
    }
21
}

Conclusione

Che avvolge il nostro tutorial sulla memorizzazione di dati crittografati. In questo post, hai imparato in modo sicuro crittografare e decrittografare i dati sensibili con una password fornita dall'utente. È facile da fare quando si sa come, ma è importante seguire tutte le procedure consigliate per garantire i dati degli utenti sono veramente sicuri.

Nel prossimo post, prenderemo un'occhiata a come sfruttare il KeyStore e altre API correlate a credenziali per archiviare gli elementi in modo sicuro. Nel frattempo, Scopri alcuni dei nostri altri grandi articoli sullo sviluppo di app per Android.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.