Sleutels, legitimatiegegevens en opslag op Android

In het vorige bericht over de beveiliging van Android-gebruikersgegevens hebben we gekeken naar de codering van gegevens via een door de gebruiker opgegeven toegangscode. In deze zelfstudie wordt de focus verschoven naar referentie- en sleutelopslag. Ik zal beginnen met het invoeren van accountreferenties en eindigen met een voorbeeld van het beschermen van gegevens met behulp van de KeyStore.

Vaak is er bij het werken met een service van derden enige vorm van authenticatie vereist. Dit kan zo simpel zijn als een /Log in eindpunt dat een gebruikersnaam en wachtwoord accepteert. 

In eerste instantie lijkt het eenvoudig om een ​​gebruikersinterface samen te stellen die de gebruiker vraagt ​​om in te loggen en vervolgens zijn inloggegevens vast te leggen en op te slaan. Dit is echter niet de beste methode, omdat onze app de inloggegevens voor een account van derden niet hoeft te weten. In plaats daarvan kunnen we de Account Manager gebruiken, die de verwerking van die gevoelige informatie voor ons delegeert.

Account Manager

De accountmanager is een gecentraliseerde hulp voor gebruikersaccountreferenties, zodat uw app niet direct te maken heeft met wachtwoorden. Het biedt vaak een token in plaats van de echte gebruikersnaam en het wachtwoord dat kan worden gebruikt om geverifieerde aanvragen voor een dienst te maken. Een voorbeeld is het aanvragen van een OAuth2-token. 

Soms is alle vereiste informatie al opgeslagen op het apparaat en soms moet de accountbeheerder een server bellen voor een vernieuwd token. Misschien heb je de accounts in de Instellingen van uw apparaat voor verschillende apps. We kunnen die lijst met beschikbare accounts als volgt opvragen:

AccountManager accountManager = AccountManager.get (dit); Account [] accounts = accountManager.getAccounts ();

De code vereist de android.permission.GET_ACCOUNTS toestemming. Als u op zoek bent naar een specifiek account, kunt u dit op de volgende manier vinden:

AccountManager accountManager = AccountManager.get (dit); Account [] accounts = accountManager.getAccountsByType ("com.google");

Zodra u het account hebt, kan een token voor het account worden opgehaald door het getAuthToken (Account, String, Bundel, Activiteit, AccountManagerCallback, Handler) methode. Het token kan vervolgens worden gebruikt om geverifieerde API-aanvragen voor een service te maken. Dit kan een RESTful API zijn waarbij u een token-parameter doorgeeft tijdens een HTTPS-aanvraag, zonder ooit de persoonlijke accountdetails van de gebruiker te hoeven weten.

Omdat elke service een andere manier heeft om de privéreferenties te verifiëren en op te slaan, biedt de accountbeheerder verificatiemodules voor een externe service om te implementeren. Hoewel Android implementaties heeft voor veel populaire services, betekent dit dat je je eigen authenticator kunt schrijven om de accountauthenticatie en legitimatiegegevensopslag van je app te verwerken. Hiermee kunt u ervoor zorgen dat de referenties gecodeerd zijn. Houd er rekening mee dat dit ook betekent dat inloggegevens in de Account Manager die door andere services worden gebruikt, in duidelijke tekst kunnen worden opgeslagen, zodat ze zichtbaar zijn voor iedereen die zijn apparaat heeft geroot.

In plaats van eenvoudige referenties, zijn er tijden dat u een sleutel of een certificaat voor een persoon of entiteit moet afhandelen, bijvoorbeeld wanneer een externe partij u een certificaatbestand stuurt dat u moet bewaren. Het meest voorkomende scenario is wanneer een app moet worden geverifieerd bij de server van een particuliere organisatie. 

In de volgende zelfstudie zullen we kijken naar het gebruik van certificaten voor verificatie en beveiligde communicatie, maar ik wil nog steeds bespreken hoe deze items in de tussentijd moeten worden opgeslagen. De Keychain API is oorspronkelijk gebouwd voor dat zeer specifieke gebruik - het installeren van een privésleutel of certificaatpaar van een PKCS # 12-bestand.

De sleutelhanger

Geïntroduceerd in Android 4.0 (API Level 14), behandelt de Keychain API key management. Concreet werkt het met Prive sleutel en X509Certificate objecten en biedt een veiliger container dan het gebruik van de gegevensopslag van uw app. Dat komt omdat machtigingen voor privésleutels alleen toestaan ​​dat uw eigen app toegang heeft tot de sleutels en alleen na toestemming van de gebruiker. Dit betekent dat er een vergrendelingsscherm moet worden ingesteld op het apparaat voordat u gebruik kunt maken van de legitimatiegegevensopslag. Ook kunnen de objecten in de sleutelhanger gebonden zijn aan beveiligde hardware, indien beschikbaar. 

De code om een ​​certificaat te installeren is als volgt:

Intent intent = KeyChain.createInstallIntent (); byte [] p12Bytes = // ... gelezen van bestand, zoals example.pfx of example.p12 ... intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (intent);

De gebruiker wordt om een ​​wachtwoord gevraagd voor toegang tot de persoonlijke sleutel en een optie om het certificaat een naam te geven. Om de sleutel op te halen, bevat de volgende code een gebruikersinterface waarmee de gebruiker kan kiezen uit de lijst met geïnstalleerde sleutels.

KeyChain.choosePrivateKeyAlias ​​(this, this, new String [] "RSA", null, null, -1, null);

Nadat de keuze is gemaakt, wordt een string alias naam geretourneerd in de alias (laatste tekenreeksalias) terugbellen waarbij u rechtstreeks toegang hebt tot de privésleutel of certificaatketen.

public class KeychainTest breidt Activiteitswerktuigen uit ..., KeyChainAliasCallback // ... @Override public void alias (laatste String alias) Log.e ("MyApp", "Alias ​​is" + alias); probeer PrivateKey privateKey = KeyChain.getPrivateKey (dit, alias); X509Certificaat [] certificateChain = KeyChain.getCertificateChain (dit, alias);  vangst ... // ...

Gewapend met die kennis, laten we nu zien hoe we de referentieopslag kunnen gebruiken om uw eigen gevoelige gegevens op te slaan.

De KeyStore

In de vorige zelfstudie hebben we gekeken naar de bescherming van gegevens via een door de gebruiker geleverde toegangscode. Dit soort instellingen is goed, maar appvereisten zorgen er vaak voor dat gebruikers zich steeds opnieuw aanmelden en onthouden een extra toegangscode. 

Dat is waar de KeyStore API kan worden gebruikt. Sinds API 1 is de KeyStore door het systeem gebruikt om WiFi- en VPN-inloggegevens op te slaan. Vanaf 4.3 (API 18) kunt u met uw eigen app-specifieke asymmetrische sleutels werken en in Android M (API 23) een AES-symmetrische sleutel opslaan. Dus hoewel de API het niet toestaat gevoelige strings rechtstreeks op te slaan, kunnen deze sleutels worden opgeslagen en vervolgens worden gebruikt om strings te coderen. 

Het voordeel van het opslaan van een sleutel in de KeyStore is dat hiermee toetsen kunnen worden gebruikt zonder de geheime inhoud van die sleutel bloot te leggen; sleutelgegevens komen niet in de app-ruimte. Houd er rekening mee dat sleutels worden beschermd door machtigingen, zodat alleen uw app toegang tot deze sleutels kan krijgen en ze kunnen bovendien via een beveiligde computer worden beveiligd als het apparaat daartoe in staat is. Hierdoor wordt een container gemaakt die het moeilijker maakt om sleutels van een apparaat te extraheren. 

Genereer een nieuwe willekeurige sleutel

Voor dit voorbeeld kunnen we in plaats van een AES-sleutel genereren via een door de gebruiker opgegeven wachtwoord, automatisch een willekeurige sleutel genereren die in de KeyStore wordt beveiligd. We kunnen dit doen door een keygenerator bijvoorbeeld ingesteld op de "AndroidKeyStore" leverancier.

// Genereer een sleutel en sla deze op in de KeyStore laatste KeyGenerator-sleutelGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // vereist vergrendelingsscherm, ongeldig als vergrendelscherm is uitgeschakeld //.setUserAuthenticationValidityDurationSeconds(120) // alleen beschikbaar x seconden na wachtwoordverificatie. -1 vereist vingerafdruk - elke keer .setRandomizedEncryptionRequired (true) // verschillende ciphertext voor dezelfde leesbare tekst bij elke aanroep .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();

Belangrijke onderdelen om naar te kijken zijn de .setUserAuthenticationRequired (true) en .setUserAuthenticationValidityDurationSeconds (120) specificaties. Hiervoor moet een vergrendelscherm worden ingesteld en moet de sleutel worden vergrendeld totdat de gebruiker is geverifieerd. 

Kijkend naar de documentatie voor .setUserAuthenticationValidityDurationSeconds (), u zult zien dat dit betekent dat de sleutel slechts een bepaald aantal seconden beschikbaar is vanaf wachtwoordverificatie en die binnenkomt -1 vereist vingerafdrukverificatie telkens wanneer u toegang wilt tot de sleutel. Het inschakelen van de vereiste voor authenticatie heeft ook tot gevolg dat de sleutel wordt ingetrokken wanneer de gebruiker het vergrendelingsscherm verwijdert of wijzigt. 

Omdat het opslaan van een onbeveiligde sleutel naast de gecodeerde gegevens net zoiets is als het plaatsen van een huissleutel onder de deurmat, proberen deze opties de sleutel in rust te beschermen in het geval een apparaat is gecompromitteerd. Een voorbeeld kan een offline gegevensverzameling van het apparaat zijn. Zonder dat het wachtwoord bekend is voor het apparaat, worden die gegevens onbruikbaar gemaakt.

De .setRandomizedEncryptionRequired (true) optie stelt de vereiste in dat er voldoende randomisatie is (telkens een nieuwe willekeurige IV), zodat als dezelfde gegevens een tweede keer worden versleuteld, de gecodeerde uitvoer nog steeds anders zal zijn. Dit voorkomt dat een aanvaller aanwijzingen krijgt over de cijfertekst op basis van invoer in dezelfde gegevens. 

Een andere optie om op te merken is setUserAuthenticationValidWhileOnBody (boolean remainsValid), die de sleutel vergrendelt zodra het apparaat heeft gedetecteerd dat het niet langer op de persoon rust.

Gegevens versleutelen

Nu de sleutel in de KeyStore is opgeslagen, kunnen we een methode maken die gegevens versleutelt met behulp van de Cijfer object, gezien de Geheime sleutel. Het zal terugkeren a Hash kaart met de gecodeerde gegevens en een willekeurige IV die nodig is om de gegevens te decoderen. De gecodeerde gegevens, samen met de IV, kunnen vervolgens worden opgeslagen in een bestand of in de gedeelde voorkeuren.

privé HashMap coderen (laatste byte [] decryptedBytes) final HashMap kaart = nieuwe HashMap(); probeer // Verkrijg de sleutel tot slot KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Versleutel gegevens definitief Cijfercijfer = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); laatste byte [] ivBytes = cipher.getIV (); laatste byte [] gecodeerdBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("versleuteld", gecodeerd bits);  catch (Throwable e) e.printStackTrace ();  terug kaart; 

Decoderen naar een byte-array

Voor decodering wordt het omgekeerde toegepast. De Cijfer object wordt geïnitialiseerd met behulp van de DECRYPT_MODE constant en gedecodeerd byte[] array wordt geretourneerd.

private byte [] decrypt (final HashMap kaart) byte [] decryptedBytes = null; probeer // Verkrijg de sleutel tot slot KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); final SecretKey secretKey = secretKeyEntry.getSecretKey (); // Extraheer informatie van kaart laatste byte [] encryptionedBytes = map.get ("versleuteld"); laatste byte [] ivBytes = map.get ("iv"); // Ontcijferen van gegevens definitief Cijfercijfer = Cipher.getInstance ("AES / GCM / NoPadding"); finale GCMParameterSpec spec = nieuwe GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (versleutelde bits);  catch (Throwable e) e.printStackTrace ();  return decryptedBytes; 

Het voorbeeld testen

We kunnen ons voorbeeld nu testen!

@TargetApi (Build.VERSION_CODES.M) private void testEncryption () try // Genereer een sleutel en sla deze op in de KeyStore laatste KeyGenerator-sleutelGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ("MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // vereist vergrendelingsscherm, ongeldig als vergrendelscherm is uitgeschakeld //.setUserAuthenticationValidityDurationSeconds(120) // alleen beschikbaar x seconden na wachtwoordverificatie. -1 vereist vingerafdruk - elke keer .setRandomizedEncryptionRequired (true) // verschillende ciphertext voor dezelfde leesbare tekst bij elke aanroep .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Test de definitieve HashMap map = versleutelen ("My very sensitive string!". getBytes ("UTF-8")); laatste byte [] decryptedBytes = decoderen (kaart); final String decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "De ontsleutelde reeks is" + decryptedString);  catch (Throwable e) e.printStackTrace (); 

RSA-asymmetrische sleutels voor oudere apparaten gebruiken

Dit is een goede oplossing om gegevens op te slaan voor versies M en hoger, maar wat als uw app eerdere versies ondersteunt? Hoewel AES-symmetrische sleutels niet worden ondersteund onder M, zijn RSA-asymmetrische sleutels dat wel. Dat betekent dat we RSA-sleutels en -codering kunnen gebruiken om hetzelfde te bereiken. 

Het belangrijkste verschil hierbij is dat een asymmetrische sleutelpaar twee sleutels bevat, een private en een publieke sleutel, waarbij de openbare sleutel de gegevens versleutelt en de persoonlijke sleutel deze decodeert. EEN KeyPairGeneratorSpec wordt doorgegeven aan de KeyPairGenerator dat is geïnitialiseerd met KEY_ALGORITHM_RSA en de "AndroidKeyStore" leverancier.

private void testPreMEncryption () try // Genereer een sleutelpaar en sla deze op in de KeyStore KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Kalenderstart = Calendar.getInstance (); Kalender einde = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder (this) .setAlias ​​("MyKeyAlias") .setSubject (new X500Principal ("CN = MyKeyName, O = Android Authority")) .setSerialNumber (new BigInteger (1024, new Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // op API-niveau 18, gecodeerd in rust, vereist vergrendelingsscherm om te worden ingesteld, wijzigt vergrendelingsscherm de sleutel .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Encryptietest laatste byte [] encryptedBytes = rsaEncrypt ("My secret string!". GetBytes ("UTF-8")); laatste byte [] decryptedBytes = rsaDecrypt (versleutelde bits); final String decryptedString = new String (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Decrypted string is" + decryptedString);  catch (Throwable e) e.printStackTrace (); 

Om te coderen, krijgen we de RSAPublicKey van de keypair en gebruik het met de Cijfer voorwerp. 

public byte [] rsaEncrypt (final byte [] decryptedBytes) byte [] versleuteldBytes = null; probeer 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 (); laatste Cijfercijfer = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream (); final CipherOutputStream cipherOutputStream = new CipherOutputStream (outputStream, cipher); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptionedBytes = outputStream.toByteArray ();  catch (Throwable e) e.printStackTrace ();  return encryptedBytes; 

Decryptie wordt gedaan met behulp van de RSAPrivateKey voorwerp.

openbare byte [] rsaDecrypt (laatste byte [] versleutelde bits) byte [] decryptedBytes = null; probeer 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 (); laatste Cijfercijfer = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); final CipherInputStream cipherInputStream = nieuwe CipherInputStream (nieuwe ByteArrayInputStream (encryptioned bytes), cipher); laatste ArrayList arrayList = new ArrayList <> (); int nextByte; while ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((byte) nextByte);  decryptedBytes = nieuwe byte [arrayList.size ()]; voor (int i = 0; i < decryptedBytes.length; i++)  decryptedBytes[i] = arrayList.get(i);   catch (Throwable e)  e.printStackTrace();  return decryptedBytes; 

Eén ding over RSA is dat de codering langzamer is dan in AES. Dit is meestal goed voor kleine hoeveelheden informatie, bijvoorbeeld wanneer u gedeelde voorkeursreeksen beveiligt. Als u echter constateert dat er een prestatieprobleem is met het versleutelen van grote hoeveelheden gegevens, kunt u in plaats daarvan dit voorbeeld gebruiken om alleen een AES-sleutel te coderen en op te slaan. Gebruik vervolgens die snellere AES-codering die in de vorige zelfstudie is besproken voor de rest van uw gegevens. U kunt een nieuwe AES-sleutel genereren en deze converteren naar a byte[] array die compatibel is met dit voorbeeld.

KeyGenerator-sleutelGenerator = KeyGenerator.getInstance ("AES"); keyGenerator.init (256); // AES-256 SecretKey secretKey = keyGenerator.generateKey (); byte [] keyBytes = secretKey.getEncoded ();

Om de sleutel terug te krijgen van de bytes, doe dit:

SecretKey-sleutel = nieuwe SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");

Dat was veel code! Om alle voorbeelden eenvoudig te houden, heb ik een grondige uitzonderingsbehandeling achterwege gelaten. Maar onthoud dat het voor uw productiecode niet wordt aanbevolen om alles te vangen Throwable gevallen in één vangstverklaring.

Conclusie

Hiermee is de zelfstudie over werken met referenties en sleutels voltooid. Veel van de verwarring rond sleutels en opslag heeft te maken met de evolutie van het Android-besturingssysteem, maar je kunt kiezen welke oplossing je gebruikt gezien het API-niveau dat je app ondersteunt. 

Nu we de best practices voor het veilig stellen van gegevens hebben besproken, zal de volgende zelfstudie zich richten op het beveiligen van gegevens tijdens het transport.