Nel post precedente sulla sicurezza dei dati degli utenti Android, abbiamo esaminato la crittografia dei dati tramite un passcode fornito dall'utente. Questo tutorial sposterà l'attenzione sulle credenziali e sulla memorizzazione delle chiavi. Inizierò introducendo le credenziali dell'account e terminando con un esempio di protezione dei dati utilizzando KeyStore.
Spesso, quando si lavora con un servizio di terze parti, sarà necessaria una qualche forma di autenticazione. Questo può essere semplice come un /accesso
endpoint che accetta nome utente e password.
Inizialmente sembrerebbe che una soluzione semplice sia quella di costruire un'interfaccia utente che chiede all'utente di accedere e quindi acquisire e memorizzare le proprie credenziali di accesso. Tuttavia, questa non è la migliore pratica perché la nostra app non dovrebbe aver bisogno di conoscere le credenziali per un account di terze parti. Invece, possiamo utilizzare l'account manager, che delega la gestione di tali informazioni sensibili per noi.
Account Manager è un helper centralizzato per le credenziali dell'account utente in modo che l'app non debba gestire direttamente le password. Spesso fornisce un token al posto del nome utente e della password reali che possono essere utilizzati per fare richieste autenticate a un servizio. Un esempio è quando si richiede un token OAuth2.
A volte, tutte le informazioni richieste sono già memorizzate sul dispositivo e altre volte il gestore account dovrà chiamare un server per un token aggiornato. Potresti aver visto il conti sezione nelle Impostazioni del tuo dispositivo per varie app. Possiamo ottenere quell'elenco di account disponibili come questo:
AccountManager accountManager = AccountManager.get (this); Account [] account = accountManager.getAccounts ();
Il codice richiederà il android.permission.GET_ACCOUNTS
autorizzazione. Se stai cercando un account specifico, puoi trovarlo in questo modo:
AccountManager accountManager = AccountManager.get (this); Account [] account = accountManager.getAccountsByType ("com.google");
Una volta ottenuto l'account, è possibile recuperare un token per l'account chiamando il getAuthToken (Account, String, Bundle, Activity, AccountManagerCallback, Handler)
metodo. Il token può quindi essere utilizzato per fare richieste API autenticate a un servizio. Questa potrebbe essere un'API RESTful in cui si passa un parametro token durante una richiesta HTTPS, senza dover mai conoscere i dettagli dell'account privato dell'utente.
Poiché ciascun servizio avrà un diverso modo di autenticare e archiviare le credenziali private, l'Account Manager fornisce moduli di autenticazione per un servizio di terze parti da implementare. Mentre Android ha implementazioni per molti servizi popolari, significa che puoi scrivere il tuo autenticatore per gestire l'autenticazione dell'account e l'archiviazione delle credenziali della tua app. Questo ti consente di assicurarti che le credenziali siano crittografate. Tieni presente che ciò significa anche che le credenziali nel gestore account utilizzate da altri servizi possono essere archiviate in testo non crittografato, rendendole visibili a chiunque abbia effettuato il root del proprio dispositivo.
Invece di semplici credenziali, ci sono momenti in cui dovrai gestire una chiave o un certificato per un individuo o entità, ad esempio quando una terza parte ti invia un file di certificato che devi conservare. Lo scenario più comune è quando un'app deve autenticarsi sul server di un'organizzazione privata.
Nel prossimo tutorial, vedremo come utilizzare i certificati per l'autenticazione e le comunicazioni sicure, ma voglio comunque chiarire come conservare questi elementi nel frattempo. L'API Keychain è stata originariamente creata per un uso molto specifico: l'installazione di una chiave privata o una coppia di certificati da un file PKCS # 12.
Introdotto in Android 4.0 (livello API 14), l'API Keychain si occupa della gestione delle chiavi. In particolare, funziona con PrivateKey
e X509Certificate
oggetti e fornisce un contenitore più sicuro rispetto all'utilizzo della memorizzazione dei dati dell'app. Questo perché le autorizzazioni per le chiavi private consentono alla tua app di accedere solo alle chiavi e solo dopo l'autorizzazione dell'utente. Ciò significa che è necessario configurare una schermata di blocco sul dispositivo prima di poter utilizzare la memoria credenziali. Inoltre, gli oggetti nel portachiavi potrebbero essere associati all'hardware sicuro, se disponibile.
Il codice per installare un certificato è il seguente:
Intent intent = KeyChain.createInstallIntent (); byte [] p12Bytes = // ... letti da file, come example.pfx o example.p12 ... intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (intento);
All'utente verrà richiesta una password per accedere alla chiave privata e un'opzione per assegnare un nome al certificato. Per recuperare la chiave, il codice seguente presenta un'interfaccia utente che consente all'utente di scegliere dall'elenco delle chiavi installate.
KeyChain.choosePrivateKeyAlias (this, this, new String [] "RSA", null, null, -1, null);
Una volta effettuata la scelta, viene restituito un nome alias di stringa nel file alias (alias stringa finale)
callback in cui è possibile accedere direttamente alla chiave privata o alla catena di certificati.
public class KeychainTest estende Activity implements ..., KeyChainAliasCallback // ... @Override alias void pubblico (alias stringa finale) Log.e ("MyApp", "Alias is" + alias); prova PrivateKey privateKey = KeyChain.getPrivateKey (questo, alias); X509Certificate [] certificateChain = KeyChain.getCertificateChain (questo, alias); catturare… //…
Armati di questa conoscenza, vediamo ora come possiamo utilizzare la memorizzazione delle credenziali per salvare i tuoi dati sensibili.
Nel tutorial precedente, abbiamo esaminato la protezione dei dati tramite un passcode fornito dall'utente. Questo tipo di installazione è buona, ma i requisiti delle app si allontanano spesso dal momento in cui gli utenti effettuano l'accesso ogni volta e ricordano un passcode aggiuntivo.
È qui che è possibile utilizzare l'API KeyStore. Dall'API 1, il KeyStore è stato utilizzato dal sistema per memorizzare credenziali WiFi e VPN. A partire dalla versione 4.3 (API 18), consente di lavorare con le proprie chiavi asimmetriche specifiche dell'app e, in Android M (API 23), può memorizzare una chiave simmetrica AES. Pertanto, mentre l'API non consente di memorizzare direttamente stringhe sensibili, queste chiavi possono essere archiviate e quindi utilizzate per crittografare le stringhe.
Il vantaggio di memorizzare una chiave nel KeyStore è che consente di utilizzare le chiavi senza esporre il contenuto segreto di quella chiave; i dati chiave non entrano nello spazio dell'app. Ricorda che le chiavi sono protette da permessi in modo che solo la tua app possa accedervi, e possono inoltre essere protette da hardware sicuro se il dispositivo è in grado. Questo crea un contenitore che rende più difficile estrarre le chiavi da un dispositivo.
Per questo esempio, invece di generare una chiave AES da un passcode fornito dall'utente, possiamo generare automaticamente una chiave casuale che sarà protetta nel KeyStore. Possiamo farlo creando un KeyGenerator
istanza, impostata su "AndroidKeyStore"
fornitore.
// Genera una chiave e la memorizza nel KeyStore finale KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); finale KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // richiede schermata di blocco, invalidata se la schermata di blocco è disabilitata //.setUserAuthenticationValidityDurationSeconds(120) // disponibile solo x secondi dall'autenticazione della password. -1 richiede impronte digitali - ogni volta. SetRandomizedEncryptionRequired (true) // testo cifrato diverso per lo stesso testo in chiaro su ogni call .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();
Parti importanti da guardare qui sono le .setUserAuthenticationRequired (vero)
e .setUserAuthenticationValidityDurationSeconds (120)
specifiche. Questi richiedono la configurazione di una schermata di blocco e il blocco della chiave fino all'autenticazione dell'utente.
Guardando la documentazione per .setUserAuthenticationValidityDurationSeconds ()
, vedrai che significa che la chiave è disponibile solo un certo numero di secondi dall'autenticazione della password, e quella che passa -1
richiede l'autenticazione dell'impronta digitale ogni volta che si desidera accedere alla chiave. L'attivazione del requisito per l'autenticazione ha anche l'effetto di revocare la chiave quando l'utente rimuove o modifica la schermata di blocco.
Poiché l'archiviazione di una chiave non protetta insieme ai dati crittografati è come mettere una chiave di casa sotto lo zerbino, queste opzioni tentano di proteggere la chiave a riposo nel caso in cui un dispositivo venga compromesso. Un esempio potrebbe essere un dump di dati offline del dispositivo. Senza che la password sia nota per il dispositivo, tali dati sono resi inutili.
Il .setRandomizedEncryptionRequired (vero)
l'opzione abilita il requisito che ci sia abbastanza randomizzazione (una nuova IV casuale ogni volta) in modo che se gli stessi dati vengano crittografati una seconda volta, l'output crittografato sarà comunque diverso. Ciò impedisce a un utente malintenzionato di ottenere indizi sul testo cifrato basato sull'alimentazione degli stessi dati.
Un'altra opzione da notare è setUserAuthenticationValidWhileOnBody (boolean remainsValid)
, che blocca la chiave una volta che il dispositivo ha rilevato che non è più sulla persona.
Ora che la chiave è memorizzata nel KeyStore, possiamo creare un metodo che codifica i dati usando il Cifra
oggetto, dato il Chiave segreta
. Restituirà a HashMap
contenente i dati crittografati e un IV randomizzato che sarà necessario per decodificare i dati. I dati crittografati, insieme all'IV, possono quindi essere salvati in un file o nelle preferenze condivise.
HashMap privataencrypt (final byte [] decryptedBytes) final HashMap map = new HashMap (); prova // Ottieni la chiave finale KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); secretKey finale secretKey = secretKeyEntry.getSecretKey (); // Crittografa dati finale Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); byte finale [] ivBytes = cipher.getIV (); byte finale [] encryptedBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("crittografato", criptatoByte); catch (Throwable e) e.printStackTrace (); mappa di ritorno;
Per la decodifica, viene applicato il contrario. Il Cifra
l'oggetto è inizializzato usando il DECRYPT_MODE
costante e decrittografato byte[]
array viene restituito.
byte privato [] decripta (HashMap finalemap) byte [] decryptedBytes = null; prova // Ottieni la chiave finale KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); secretKey finale secretKey = secretKeyEntry.getSecretKey (); // Estrai informazioni dalla mappa byte finale [] encryptedBytes = map.get ("encrypted"); byte finale [] ivBytes = map.get ("iv"); // Decrypt data Cipher cipher finale = Cipher.getInstance ("AES / GCM / NoPadding"); finale GCMParameterSpec spec = new GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (encryptedBytes); catch (Throwable e) e.printStackTrace (); return decryptedBytes;
Ora possiamo testare il nostro esempio!
@TargetApi (Build.VERSION_CODES.M) private void testEncryption () try // Genera una chiave e la memorizza nel KeyStore finale keyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); finale KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // richiede schermata di blocco, invalidata se la schermata di blocco è disabilitata //.setUserAuthenticationValidityDurationSeconds(120) // disponibile solo x secondi dall'autenticazione della password. -1 richiede impronte digitali - ogni volta. SetRandomizedEncryptionRequired (true) // testo cifrato diverso per lo stesso testo in chiaro su ogni call .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Prova HashMap finalemap = encrypt ("La mia stringa molto sensibile!". getBytes ("UTF-8")); byte finale [] decryptedBytes = decrypt (map); stringa finale decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "La stringa decrittografata è" + decryptedString); catch (Throwable e) e.printStackTrace ();
Questa è una buona soluzione per archiviare i dati per le versioni M e successive, ma cosa succede se la tua app supporta versioni precedenti? Mentre le chiavi simmetriche AES non sono supportate sotto M, le chiavi asimmetriche RSA sono. Ciò significa che possiamo usare le chiavi RSA e la crittografia per realizzare la stessa cosa.
La differenza principale qui è che una coppia di chiavi asimmetriche contiene due chiavi, una privata e una chiave pubblica, in cui la chiave pubblica crittografa i dati e la chiave privata la decrittografa. UN KeyPairGeneratorSpec
è passato nel KeyPairGenerator
che è inizializzato con KEY_ALGORITHM_RSA
e il "AndroidKeyStore"
fornitore.
private void testPreMEncryption () try // Generare una coppia di chiavi e archiviarla in KeyStore KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Calendar start = Calendar.getInstance (); Calendar end = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder (this) .setAlias ("MyKeyAlias") .setSubject (nuovo X500Principal ("CN = MyKeyName, O = Android Authority")) .setSerialNumber (new BigInteger (1024, new Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // su livello API 18, crittografato a riposo, richiede la schermata di blocco da impostare, la modifica dello schermo di blocco rimuove la chiave .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Test di crittografia byte finale [] encryptedBytes = rsaEncrypt ("My secret string!". GetBytes ("UTF-8")); byte finale [] decryptedBytes = rsaDecrypt (encryptedBytes); stringa finale decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Decrypted string is" + decryptedString); catch (Throwable e) e.printStackTrace ();
Per crittografare, otteniamo il RSAPublicKey
dalla keypair e usarlo con il Cifra
oggetto.
public byte [] rsaEncrypt (final byte [] decryptedBytes) byte [] encryptedBytes = null; try final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate (). getPublicKey (); cipher cifrato finale = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); finale ByteArrayOutputStream outputStream = new ByteArrayOutputStream (); cipherOutputStream finale cipherOutputStream = new CipherOutputStream (outputStream, cipher); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray (); catch (Throwable e) e.printStackTrace (); return encryptedBytes;
La decrittazione viene eseguita utilizzando il RSAPrivateKey
oggetto.
public byte [] rsaDecrypt (final byte [] encryptedBytes) byte [] decryptedBytes = null; try final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey (); cipher cifrato finale = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); cipherInputStream finale cipherInputStream = nuovo CipherInputStream (new ByteArrayInputStream (encryptedBytes), cipher); ArrayList finalearrayList = new ArrayList <> (); int nextByte; while ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((byte) nextByte); decryptedBytes = new byte [arrayList.size ()]; per (int i = 0; i < decryptedBytes.length; i++) decryptedBytes[i] = arrayList.get(i); catch (Throwable e) e.printStackTrace(); return decryptedBytes;
Una cosa su RSA è che la crittografia è più lenta di quanto non sia in AES. Di solito, ciò è normale per piccole quantità di informazioni, ad esempio quando si proteggono le stringhe di preferenza condivise. Se si riscontra un problema di prestazioni durante la crittografia di grandi quantità di dati, è tuttavia possibile utilizzare questo esempio per crittografare e archiviare solo una chiave AES. Quindi, usa quella crittografia AES più veloce che è stata discussa nel tutorial precedente per il resto dei tuoi dati. È possibile generare una nuova chiave AES e convertirla in a byte[]
array compatibile con questo esempio.
KeyGenerator keyGenerator = KeyGenerator.getInstance ("AES"); keyGenerator.init (256); // AES-256 SecretKey secretKey = keyGenerator.generateKey (); byte [] keyBytes = secretKey.getEncoded ();
Per recuperare la chiave dai byte, fai questo:
Chiave SecretKey = new SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");
Quello era un sacco di codice! Per mantenere tutti gli esempi semplici, ho omesso la gestione completa delle eccezioni. Ma ricorda che per il tuo codice di produzione, non è consigliabile semplicemente prendere tutto Throwable
casi in una dichiarazione di cattura.
Questo completa il tutorial su come lavorare con credenziali e chiavi. Gran parte della confusione relativa alle chiavi e all'archiviazione ha a che fare con l'evoluzione del sistema operativo Android, ma puoi scegliere quale soluzione utilizzare dato il livello API supportato dalla tua app.
Ora che abbiamo coperto le migliori pratiche per proteggere i dati a riposo, il prossimo tutorial si concentrerà sulla protezione dei dati in transito.