La credibilità di un'app oggi dipende molto da come vengono gestiti i dati privati dell'utente. Lo stack Android ha molte potenti API che riguardano le credenziali e l'archiviazione delle chiavi, con funzionalità specifiche disponibili solo in determinate versioni.
Questa breve serie inizierà con un semplice approccio per iniziare e funzionare osservando il sistema di storage e come crittografare e archiviare i dati sensibili tramite un passcode fornito dall'utente. Nel secondo tutorial, vedremo modi più complessi di proteggere chiavi e credenziali.
La prima domanda a cui pensare è quanti dati hai effettivamente bisogno di acquisire. Un buon approccio è quello di evitare la memorizzazione di dati privati se non è necessario.
Per i dati che è necessario memorizzare, l'architettura Android è pronta ad aiutare. Dal momento che 6.0 Marshmallow, la crittografia del disco completo è abilitata per impostazione predefinita, per i dispositivi con la capacità. File e SharedPreferences
che vengono salvati dall'app vengono automaticamente impostati con MODE_PRIVATE
costante. Ciò significa che i dati sono accessibili solo dalla tua app.
È una buona idea attenersi a questa impostazione predefinita. È possibile impostarlo in modo esplicito quando si salva una preferenza condivisa.
SharedPreferences.Editor editor = getSharedPreferences ("preferenceName", MODE_PRIVATE) .edit (); editor.putString ("chiave", "valore"); editor.commit ();
O quando si salva un file.
FileOutputStream fos = openFileOutput (filenameString, Context.MODE_PRIVATE); fos.write (dati); fos.close ();
Evita di archiviare i dati su una memoria esterna, poiché i dati sono visibili da altre app e utenti. In effetti, per rendere più difficile per le persone copiare i dati e il binario dell'app, è possibile impedire agli utenti di installare l'app su una memoria esterna. Aggiunta Android: installLocation
con un valore di InternalOnly
al file manifest lo realizzerà.
Puoi anche impedire il backup dell'applicazione e dei suoi dati. Ciò impedisce anche il download dei contenuti della directory dei dati privati di un'app utilizzando adb backup
. Per fare ciò, imposta il Android: allowBackup
attribuire a falso
nel file manifest. Per impostazione predefinita, questo attributo è impostato su vero
.
Queste sono le migliori pratiche, ma non funzioneranno per un dispositivo compromesso o rooted e la crittografia del disco è utile solo quando il dispositivo è protetto da una schermata di blocco. Qui è utile avere una password sul lato app che protegge i dati con la crittografia.
Conceal è un'ottima scelta per una libreria di crittografia perché ti consente di essere subito operativo senza doversi preoccupare dei dettagli sottostanti. Tuttavia, un exploit mirato per un framework popolare influenzerà simultaneamente tutte le app che si basano su di esso.
È anche importante essere informati su come funzionano i sistemi di crittografia per essere in grado di dire se si sta utilizzando un particolare framework in modo sicuro. Quindi, per questo post, ci sporcheremo le mani guardando direttamente il provider di crittografia.
Useremo lo standard AES raccomandato, che crittografa i dati forniti con una chiave. La stessa chiave utilizzata per crittografare i dati viene utilizzata per decrittografare i dati, denominata crittografia simmetrica. Esistono diverse dimensioni di chiave e AES256 (256 bit) è la lunghezza preferita per l'utilizzo con dati sensibili.
Mentre l'esperienza utente della tua app dovrebbe costringere un utente a utilizzare un passcode forte, c'è la possibilità che lo stesso passcode venga scelto anche da un altro utente. Mettere la sicurezza dei nostri dati crittografati nelle mani dell'utente non è sicuro. I nostri dati devono essere protetti invece con a chiave questo è casuale e abbastanza grande (cioè ha abbastanza entropia) da essere considerato forte. Questo è il motivo per cui non è mai consigliabile usare una password direttamente per crittografare i dati, cioè dove viene chiamata una funzione Funzione di derivazione chiave basata su password (PBKDF2) entra in gioco.
PBKDF2 deriva a chiave da un parola d'ordine tagliandolo molte volte con un sale. Questo è chiamato stretching chiave. Il sale è solo una sequenza casuale di dati e rende la chiave derivata univoca anche se la stessa password è stata utilizzata da qualcun altro.
Iniziamo generando quel sale.
SecureRandom random = new SecureRandom (); byte salt [] = new byte [256]; random.nextBytes (sale);
Il SecureRandom
class garantisce che l'output generato sarà difficile da prevedere: è un "generatore di numeri casuali crittograficamente forte". Ora possiamo inserire sale e password in un oggetto di crittografia basato su password: PBEKeySpec
. Il costruttore dell'oggetto prende anche un modulo di conteggio iterativo, rendendo più forte la chiave. Questo perché aumentare il numero di iterazioni espande il tempo necessario per operare su un set di chiavi durante un attacco di forza bruta. Il PBEKeySpec
quindi viene passato nel SecretKeyFactory
, che alla fine genera la chiave come a byte[]
array. Lo avvolgeremo crudo byte[]
array in a SecretKeySpec
oggetto.
char [] passwordChar = passwordString.toCharArray (); // Trasforma la password in char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iterations SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES");
Si noti che la password è passata come un char []
matrice e il PBEKeySpec
la classe lo memorizza come a char []
matrice pure. char []
gli array vengono solitamente utilizzati per le funzioni di crittografia perché mentre il Stringa
la classe è immutabile, a char []
l'array contenente informazioni sensibili può essere sovrascritto, rimuovendo così i dati sensibili interamente dalla memoria del dispositivo.
Ora siamo pronti per crittografare i dati, ma abbiamo ancora una cosa da fare. Esistono diverse modalità di crittografia con AES, ma useremo quella consigliata: cipher block concatenamento (CBC). Questo funziona sui nostri dati un blocco alla volta. Il bello di questa modalità è che ogni successivo blocco di dati non crittografato è XOR'd con il blocco crittografato precedente per rendere più forte la crittografia. Tuttavia, ciò significa che il primo blocco non è mai unico come tutti gli altri!
Se un messaggio da crittografare dovesse iniziare come un altro messaggio da crittografare, l'inizio dell'output crittografato sarebbe lo stesso, e questo darebbe a un aggressore un indizio per capire quale potrebbe essere il messaggio. La soluzione è usare un vettore di inizializzazione (IV).
Un IV è solo un blocco di byte casuali che sarà XOR con il primo blocco di dati utente. Poiché ogni blocco dipende da tutti i blocchi elaborati fino a quel momento, l'intero messaggio verrà crittografato in modo univoco, i messaggi identici crittografati con la stessa chiave non produrranno risultati identici.
Creiamo una IV ora.
SecureRandom ivRandom = new SecureRandom (); // non memorizza nella cache l'istanza seeded precedente del byte SecureRandom [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv);
Una nota su SecureRandom
. Nelle versioni 4.3 e precedenti, l'architettura di crittografia Java aveva una vulnerabilità dovuta all'inadeguata inizializzazione del generatore di numeri pseudocasici sottostante (PRNG). Se scegli come target le versioni 4.3 e precedenti, è disponibile una correzione.
Armato di un IvParameterSpec
, ora possiamo fare la vera crittografia.
Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes);
Qui passiamo nella stringa "AES / CBC / PKCS7Padding"
. Specifica la crittografia AES con il concatenamento del blocco cypher. L'ultima parte di questa stringa si riferisce a PKCS7, che è uno standard stabilito per i dati di riempimento che non si adattano perfettamente alle dimensioni del blocco. (I blocchi sono a 128 bit e il riempimento avviene prima della crittografia).
Per completare il nostro esempio, inseriremo questo codice in un metodo di crittografia che impacchetterà il risultato in a HashMap
contenente i dati crittografati, insieme al vettore di inizializzazione e di sale necessario per la decrittografia.
HashMap privataencryptBytes (byte [] plainTextBytes, String passwordString) HashMap map = new HashMap (); prova // random sale per il prossimo passo SecureRandom random = new SecureRandom (); byte salt [] = new byte [256]; random.nextBytes (sale); // PBKDF2 - ricava la chiave dalla password, non utilizzare le password direttamente char [] passwordChar = passwordString.toCharArray (); // Trasforma la password in char [] array PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iterations SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Crea un vettore di inizializzazione per AES SecureRandom ivRandom = new SecureRandom (); // non memorizza nella cache l'istanza seeded precedente del byte SecureRandom [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = new IvParameterSpec (iv); // Encrypt Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes); map.put ("sale", sale); map.put ("iv", iv); map.put ("crittografato", crittografato); catch (Exception e) Log.e ("MYAPP", "eccezione di crittografia", e); mappa di ritorno;
Hai solo bisogno di memorizzare il IV e il sale con i tuoi dati. Mentre i sali e gli IV sono considerati pubblici, assicurati che non siano incrementati o riutilizzati in sequenza. Per decifrare i dati, tutto ciò che dobbiamo fare è cambiare la modalità in Cifra
costruttore da ENCRYPT_MODE
a DECRYPT_MODE
.
Il metodo di decifrazione richiederà a HashMap
che contiene le stesse informazioni richieste (dati crittografati, sale e IV) e restituisce un decrittografato byte[]
array, data la password corretta. Il metodo decrypt rigenera la chiave di crittografia dalla password. La chiave non dovrebbe mai essere memorizzata!
byte privato [] decryptData (HashMapmap, String passwordString) byte [] decrypted = null; prova byte salt [] = map.get ("salt"); byte iv [] = map.get ("iv"); byte encrypted [] = map.get ("encrypted"); // rigenera chiave da password char [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = new PBEKeySpec (passwordChar, salt, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = new SecretKeySpec (keyBytes, "AES"); // Decrypt Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = new IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); decrypted = cipher.doFinal (criptato); catch (Exception e) Log.e ("MYAPP", "eccezione di decrittazione", e); return decrypted;
Per mantenere l'esempio semplice, omettiamo il controllo degli errori che assicurerebbe il HashMap
contiene la chiave richiesta, coppie di valori. Ora possiamo testare i nostri metodi per garantire che i dati vengano decodificati correttamente dopo la crittografia.
// Test di crittografia String string = "La mia stringa sensibile che voglio crittografare"; byte [] bytes = string.getBytes (); HashMapmap = encryptBytes (byte, "UserSuppliedPassword"); // Decryption test byte [] decrypted = decryptData (map, "UserSuppliedPassword"); if (decrypted! = null) String decryptedString = new String (decrypted); Log.e ("MYAPP", "Decrypted String is:" + decryptedString);
I metodi usano a byte[]
array in modo da poter crittografare dati arbitrari anziché solo Stringa
oggetti.
Ora che abbiamo una crittografia byte[]
array, possiamo salvarlo nella memoria.
FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (criptato); fos.close ();
Se non si desidera salvare la IV e il sale separatamente, HashMap
è serializzabile con il ObjectInputStream
e ObjectOutputStream
classi.
FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject (mappa); oos.close ();
SharedPreferences
Puoi anche salvare i dati protetti nella tua app SharedPreferences
.
SharedPreferences.Editor editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); String keyBase64String = Base64.encodeToString (encryptedKey, Base64.NO_WRAP); String valueBase64String = Base64.encodeToString (encryptedValue, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();
Dal momento che il SharedPreferences
è un sistema XML che accetta solo primitive e oggetti specifici come valori, abbiamo bisogno di convertire i nostri dati in un formato compatibile come a Stringa
oggetto. Base64 ci consente di convertire i dati grezzi in a Stringa
rappresentazione che contiene solo i caratteri consentiti dal formato XML. Cripta sia la chiave che il valore in modo che un utente malintenzionato non sia in grado di capire a cosa potrebbe essere un valore.
Nell'esempio sopra, encryptedKey
e encryptedValue
sono entrambi crittografati byte[]
gli array sono tornati dal nostro encryptBytes ()
metodo. L'IV e il sale possono essere salvati nel file delle preferenze o come file separato. Per recuperare i byte crittografati dal SharedPreferences
, possiamo applicare una decodifica Base64 sull'archivio Stringa
.
Preferenze SharedPreferences = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "default"); byte [] encryptedBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);
Ora che i dati archiviati sono sicuri, è possibile che tu abbia una versione precedente dell'app con i dati memorizzati in modo non sicuro. In un aggiornamento, i dati potrebbero essere cancellati e ricodificati. Il seguente codice passa su un file utilizzando dati casuali.
In teoria, puoi semplicemente eliminare le tue preferenze condivise rimuovendo il /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml e your_prefs_name.bak file e cancellando le preferenze in memoria con il seguente codice:
getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (). clear (). commit ();
Tuttavia, invece di tentare di cancellare i vecchi dati e sperare che funzioni, è meglio criptarlo in primo luogo! Ciò è particolarmente vero in generale per le unità a stato solido che spesso distribuiscono le scritture di dati in regioni diverse per evitare l'usura. Ciò significa che anche se si sovrascrive un file nel filesystem, la memoria fisica a stato solido potrebbe conservare i dati nella posizione originale sul disco.
public static void secureWipeFile (File file) genera IOException if (file! = null && file.exists ()) final long length = file.length (); finale SecureRandom random = new SecureRandom (); final RandomAccessFile randomAccessFile = new RandomAccessFile (file, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); byte [] data = new byte [64]; int position = 0; mentre (posizione < length) random.nextBytes(data); randomAccessFile.write(data); position += data.length; randomAccessFile.close(); file.delete();
Questo avvolge il nostro tutorial sulla memorizzazione dei dati crittografati. In questo post, hai imparato come crittografare e decifrare in modo sicuro i dati sensibili con una password fornita dall'utente. È facile farlo quando sai come, ma è importante seguire tutte le best practice per garantire che i dati dei tuoi utenti siano veramente sicuri.
Nel prossimo post, daremo un'occhiata a come sfruttare il KeyStore
e altre API relative alle credenziali per archiviare gli oggetti in modo sicuro. Nel frattempo, dai uno sguardo ad altri nostri fantastici articoli sullo sviluppo di app per Android.