De geloofwaardigheid van een app van vandaag is in hoge mate afhankelijk van hoe de privégegevens van de gebruiker worden beheerd. De Android-stack heeft veel krachtige API's rond referenties en sleutelopslag, met specifieke functies die alleen beschikbaar zijn in bepaalde versies.
Deze korte serie begint met een eenvoudige manier om aan de slag te gaan door te kijken naar het opslagsysteem en hoe gevoelige gegevens te coderen en op te slaan via een door de gebruiker verstrekte toegangscode. In de tweede zelfstudie zullen we kijken naar meer complexe manieren om sleutels en referenties te beschermen.
De eerste vraag om na te denken is hoeveel gegevens u daadwerkelijk moet verzamelen. Een goede aanpak is om te voorkomen dat u privégegevens opslaat als dat niet echt nodig is.
Voor gegevens die u moet opslaan, is de Android-architectuur klaar om te helpen. Sinds 6.0 Marshmallow is full-disk-codering standaard ingeschakeld voor apparaten met de mogelijkheid. Bestanden en Gedeelde voorkeuren
die worden opgeslagen door de app worden automatisch ingesteld met de MODE_PRIVATE
constante. Dit betekent dat de gegevens alleen toegankelijk zijn voor uw eigen app.
Het is een goed idee om je aan deze standaard te houden. U kunt dit expliciet instellen wanneer u een gedeelde voorkeur opslaat.
SharedPreferences.Editor-editor = getSharedPreferences ("preferenceName", MODE_PRIVATE) .edit (); editor.putString ("sleutel", "waarde"); editor.commit ();
Of bij het opslaan van een bestand.
FileOutputStream fos = openFileOutput (filenameString, Context.MODE_PRIVATE); fos.write (data); fos.close ();
Vermijd het opslaan van gegevens op externe opslag, omdat de gegevens dan zichtbaar zijn voor andere apps en gebruikers. Om het voor mensen moeilijker te maken om de binaire gegevens en gegevens van uw app te kopiëren, kunt u voorkomen dat gebruikers de app kunnen installeren op externe opslag. Het toevoegen android: installLocation
met een waarde van internalOnly
naar het manifestbestand zal dat bereiken.
U kunt ook voorkomen dat er een back-up van de app en de bijbehorende gegevens wordt gemaakt. Hiermee wordt ook voorkomen dat de inhoud van de privégegevensdirectory van een app wordt gedownload via adb-back-up
. Om dit te doen, stelt u de android: allowBackup
attribuut aan vals
in het manifestbestand. Dit kenmerk is standaard ingesteld op waar
.
Dit zijn praktische tips, maar ze werken niet voor een aangetast of geroot apparaat en schijfversleuteling is alleen nuttig als het apparaat is beveiligd met een vergrendelscherm. Dit is waar het hebben van een app-side-wachtwoord dat zijn gegevens beschermt met codering voordelig is.
Conceal is een uitstekende keuze voor een coderingsbibliotheek, omdat u hierdoor snel aan de slag kunt en geen zorgen hoeft te maken over de onderliggende details. Een exploit die is getarget op een populair framework, heeft echter tegelijkertijd invloed op alle apps die erop zijn gebaseerd.
Het is ook belangrijk om goed geïnformeerd te zijn over de manier waarop versleutelingssystemen werken om te kunnen zien of u een bepaald raamwerk veilig gebruikt. Dus voor deze post gaan we onze handen vies maken door direct naar de cryptografieleverancier te kijken.
We zullen de aanbevolen AES-standaard gebruiken, die gegevens versleuteld met een sleutel codeert. Dezelfde sleutel die wordt gebruikt om de gegevens te coderen, wordt gebruikt om de gegevens te ontsleutelen, wat symmetrische codering wordt genoemd. Er zijn verschillende sleutelgroottes en AES256 (256 bits) heeft de voorkeur voor gebruik met gevoelige gegevens.
Hoewel de gebruikerservaring van uw app een gebruiker moet dwingen om een sterke toegangscode te gebruiken, bestaat de kans dat dezelfde toegangscode ook door een andere gebruiker wordt gekozen. De beveiliging van onze versleutelde gegevens in handen van de gebruiker is niet veilig. Onze gegevens moeten in plaats daarvan worden beveiligd met een sleutel dat is willekeurig en groot genoeg (d.w.z. dat heeft voldoende entropie) om als sterk te worden beschouwd. Daarom wordt het nooit aangeraden om een wachtwoord rechtstreeks te gebruiken om gegevens te versleutelen, dat is waar een functie naar verwijst Wachtwoordgebaseerde Key Derivation-functie (PBKDF2) komt om de hoek kijken.
PBKDF2 is afgeleid a sleutel van een wachtwoord door hem vele malen met een zout te verspoelen. Dit wordt key stretching genoemd. Het zout is slechts een willekeurige reeks gegevens en maakt de afgeleide sleutel uniek, zelfs als hetzelfde wachtwoord door iemand anders is gebruikt.
Laten we beginnen met het genereren van dat zout.
SecureRandom random = nieuwe SecureRandom (); byte zout [] = nieuwe byte [256]; random.nextBytes (zout);
De SecureRandom
klasse garandeert dat de gegenereerde uitvoer moeilijk te voorspellen is - het is een "cryptografisch sterke willekeurige getallengenerator". We kunnen het zout en het wachtwoord nu in een coderingsobject op wachtwoordniveau plaatsen: PBEKeySpec
. De constructor van het object neemt ook een iteratietelling, waardoor de sleutel sterker wordt. Dit komt omdat het verhogen van het aantal iteraties de tijd verlengt die nodig is om een set sleutels te gebruiken tijdens een brute force-aanval. De PBEKeySpec
dan wordt doorgegeven aan de SecretKeyFactory
, die uiteindelijk de sleutel als een genereert byte[]
matrix. We zullen dat rauw verpakken byte[]
array in een SecretKeySpec
voorwerp.
char [] passwordChar = passwordString.toCharArray (); // Schakel wachtwoord in char [] array PBEKeySpec pbKeySpec = nieuwe PBEKeySpec (wachtwoordChar, salt, 1324, 256); // 1324 iteraties SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nieuwe SecretKeySpec (keyBytes, "AES");
Merk op dat het wachtwoord wordt doorgegeven als een char []
array en de PBEKeySpec
klasse slaat het op als een char []
array ook. char []
arrays worden meestal gebruikt voor coderingsfuncties, omdat terwijl Draad
klasse is onveranderlijk, a char []
een array met gevoelige informatie kan worden overschreven, waardoor de gevoelige gegevens volledig uit het geheugen van het apparaat worden verwijderd.
We zijn nu klaar om de gegevens te versleutelen, maar we hebben nog één ding te doen. Er zijn verschillende coderingsmethoden met AES, maar we zullen de aanbevolen versleutelingsmethode gebruiken: cipher block chaining (CBC). Dit werkt één blok tegelijk op onze gegevens. Het mooie aan deze modus is dat elk volgende niet-gecodeerde gegevensblok XOR bevat met het vorige gecodeerde blok om de codering sterker te maken. Dat betekent echter dat het eerste blok nooit zo uniek is als alle andere blokken!
Als een te versleutelen bericht hetzelfde zou beginnen als een ander te coderen bericht, zou de begin gecodeerde uitvoer hetzelfde zijn, en zou een aanvaller een aanwijzing kunnen geven om uit te zoeken wat het bericht zou kunnen zijn. De oplossing is om een initialisatievector te gebruiken (IV).
Een IV is slechts een blok van willekeurige bytes die XOR'd worden met het eerste blok gebruikersgegevens. Aangezien elk blok afhankelijk is van alle blokken die tot dat moment zijn verwerkt, wordt het hele bericht uniek gecodeerd, identieke berichten die zijn gecodeerd met dezelfde sleutel, produceren geen identieke resultaten.
Laten we nu een IV maken.
SecureRandom ivRandom = nieuwe SecureRandom (); // niet cachen van eerder geplaatste instantie van SecureRandom byte [] iv = nieuwe byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nieuw IvParameterSpec (iv);
Een opmerking over SecureRandom
. Op versies 4.3 en lager had de Java Cryptography Architecture een kwetsbaarheid als gevolg van onjuiste initialisatie van de onderliggende pseudorandom nummer generator (PRNG). Als u versie 4.3 en lager target, is er een oplossing beschikbaar.
Gewapend met een IvParameterSpec
, we kunnen nu de daadwerkelijke codering doen.
Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] versleuteld = cipher.doFinal (plainTextBytes);
Hier passeren we de string "AES / CBC / PKCS7Padding"
. Dit specificeert AES-codering met cypher block chaining. Het laatste deel van deze reeks verwijst naar PKCS7, wat een gevestigde standaard is voor opvulgegevens die niet perfect in de blokgrootte passen. (Blokken zijn 128 bits en padding wordt vóór codering uitgevoerd.)
Om ons voorbeeld te vervolledigen, zullen we deze code in een coderingsmethode plaatsen die het resultaat in a zal verpakken Hash kaart
die de versleutelde data bevat, samen met de zout- en initialisatievector die nodig is voor decodering.
privé HashMapencrypteBytes (byte [] plainTextBytes, String passwordString) HashMap kaart = nieuwe HashMap (); probeer // Willekeurig zout voor de volgende stap SecureRandom willekeurig = nieuw SecureRandom (); byte zout [] = nieuwe byte [256]; random.nextBytes (zout); // PBKDF2 - ontleen de sleutel aan het wachtwoord, gebruik geen wachtwoorden direct char [] passwordChar = passwordString.toCharArray (); // Schakel wachtwoord in char [] array PBEKeySpec pbKeySpec = nieuwe PBEKeySpec (wachtwoordChar, salt, 1324, 256); // 1324 iteraties SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nieuwe SecretKeySpec (keyBytes, "AES"); // Initialisatievector voor AES SecureRandom maken ivRandom = nieuwe SecureRandom (); // niet cachen van eerder geplaatste instantie van SecureRandom byte [] iv = nieuwe byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nieuw IvParameterSpec (iv); // Versleutel Cijfercijfer = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] versleuteld = cipher.doFinal (plainTextBytes); map.put ("zout", zout); map.put ("iv", iv); map.put ("versleuteld", gecodeerd); catch (Uitzondering e) Log.e ("MYAPP", "coderingsuitzondering", e); terug kaart;
U hoeft de IV en zout alleen op te slaan met uw gegevens. Hoewel zouten en IV's als openbaar worden beschouwd, moet u ervoor zorgen dat ze niet sequentieel worden opgehoogd of opnieuw worden gebruikt. Om de gegevens te decoderen, hoeven we alleen maar de modus in de Cijfer
constructor van ENCRYPT_MODE
naar DECRYPT_MODE
.
De decoderingsmethode duurt een Hash kaart
die dezelfde vereiste informatie bevat (gecodeerde gegevens, zout en IV) en een ontsleutelde teruggave byte[]
array, gegeven het juiste wachtwoord. De decoderingsmethode genereert de coderingssleutel van het wachtwoord. De sleutel mag nooit worden opgeslagen!
private byte [] decryptData (HashMapmap, String wachtwoordString) byte [] decrypted = null; probeer byte salt [] = map.get ("salt"); byte iv [] = map.get ("iv"); byte versleuteld [] = map.get ("gecodeerd"); // regenereer sleutel van wachtwoord char [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = nieuwe PBEKeySpec (wachtwoordChar, salt, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nieuwe SecretKeySpec (keyBytes, "AES"); // Decrypt Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = nieuw IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); gedecodeerd = cipher.doFinal (versleuteld); catch (Uitzondering e) Log.e ("MYAPP", "decryption exception", e); terug gedecodeerd;
Om het voorbeeld simpel te houden, laten we het controleren van fouten achterwege, zodat het Hash kaart
bevat de vereiste sleutel, waardeparen. We kunnen nu onze methoden testen om ervoor te zorgen dat de gegevens na versleuteling correct worden gedecodeerd.
// Encryptietest String string = "Mijn gevoelige string die ik wil coderen"; byte [] bytes = string.getBytes (); Hash kaartmap = encrypteBytes (bytes, "UserSuppliedPassword"); // Decryptietestbyte [] decrypted = decryptData (kaart, "UserSuppliedPassword"); if (decodering! = null) String decryptedString = new String (gedecodeerd); Log.e ("MYAPP", "Decrypted String is:" + decryptedString);
De methoden gebruiken a byte[]
array zodat u willekeurige gegevens kunt versleutelen in plaats van alleen Draad
voorwerpen.
Nu dat we een gecodeerde hebben byte[]
array kunnen we opslaan in opslag.
FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (gecodeerd); fos.close ();
Als u de IV en het zout niet apart wilt opslaan, Hash kaart
is serialiseerbaar met de ObjectInputStream
en ObjectOutputStream
klassen.
FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = nieuwe ObjectOutputStream (fos); oos.writeObject (kaart); oos.close ();
Gedeelde voorkeuren
U kunt ook beveiligde gegevens opslaan in uw apps Gedeelde voorkeuren
.
SharedPreferences.Editor-editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); String keyBase64String = Base64.encodeToString (encryptedKey, Base64.NO_WRAP); StringwaardeBase64String = Base64.encodeToString (versleutelde waarde, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();
Sinds de Gedeelde voorkeuren
is een XML-systeem dat alleen specifieke primitieven en objecten als waarden accepteert, we moeten onze gegevens converteren naar een compatibel formaat zoals a Draad
voorwerp. Met Base64 kunnen we de onbewerkte gegevens omzetten in een Draad
weergave die alleen de tekens bevat die zijn toegestaan door de XML-indeling. Versleutel zowel de sleutel als de waarde, zodat een aanvaller niet kan achterhalen waarvoor een waarde kan zijn.
In het bovenstaande voorbeeld, encryptedKey
en encryptedValue
zijn beide gecodeerd byte[]
arrays terug van onze encryptBytes ()
methode. De IV en zout kunnen worden opgeslagen in het voorkeurenbestand of als een afzonderlijk bestand. Om de versleutelde bytes terug te halen uit de Gedeelde voorkeuren
, we kunnen een Base64-decode toepassen op de opgeslagen Draad
.
SharedPreferences-voorkeuren = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "default"); byte [] encryptenBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);
Nu de opgeslagen gegevens veilig zijn, is het mogelijk dat u een eerdere versie van de app heeft waarvan de gegevens onveilig zijn opgeslagen. Bij een upgrade kunnen de gegevens worden gewist en opnieuw worden gecodeerd. De volgende code veegt over een bestand met behulp van willekeurige gegevens.
In theorie kun je gewoon je gedeelde voorkeuren verwijderen door de /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml en your_prefs_name.bak bestanden en het wissen van de voorkeuren in het geheugen met de volgende code:
getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (). clear (). commit ();
In plaats van te proberen de oude gegevens te wissen en in de hoop dat het werkt, is het beter om het eerst te coderen! Dit geldt in het bijzonder voor solid-state schijven die gegevensschrijvers vaak verspreiden naar verschillende regio's om slijtage te voorkomen. Dat betekent dat zelfs als u een bestand in het bestandssysteem overschrijft, het fysieke solid-state geheugen uw gegevens op de oorspronkelijke locatie op schijf kan bewaren.
public static void secureWipeFile (Bestandsbestand) gooit IOException if (file! = null && file.exists ()) final long length = file.length (); final SecureRandom random = nieuwe SecureRandom (); final RandomAccessFile randomAccessFile = nieuw RandomAccessFile (bestand, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); byte [] data = nieuwe byte [64]; int positie = 0; terwijl (positie < length) random.nextBytes(data); randomAccessFile.write(data); position += data.length; randomAccessFile.close(); file.delete();
Dat omvat onze tutorial over het opslaan van versleutelde data. In dit bericht hebt u geleerd hoe u veilig gevoelige gegevens kunt coderen en decoderen met een door de gebruiker verstrekt wachtwoord. Het is gemakkelijk om te doen als u weet hoe, maar het is belangrijk om alle praktische tips te volgen om ervoor te zorgen dat de gegevens van uw gebruikers echt veilig zijn.
In het volgende bericht zullen we bekijken hoe we het kunnen gebruiken sleutelopslag
en andere inloggerelateerde API's om items veilig op te slaan. Bekijk ondertussen enkele van onze andere geweldige artikelen over de ontwikkeling van Android-apps.