Lagre data trygt på Android

En apps troverdighet i dag avhenger mye av hvordan brukerens private data administreres. Android-stakken har mange kraftige APIer som omgir legitimasjon og nøkkellagring, med spesifikke funksjoner som bare er tilgjengelige i enkelte versjoner. 

Denne korte serien vil starte med en enkel tilnærming for å komme seg opp ved å se på lagringssystemet og hvordan å kryptere og lagre sensitive data via et passord som er levert av brukeren. I den andre opplæringen vil vi se på mer komplekse måter å beskytte nøkler og legitimasjon på.

Det grunnleggende

Det første spørsmålet å tenke på er hvor mye data du faktisk trenger å skaffe seg. En god tilnærming er å unngå å lagre private data hvis du ikke virkelig trenger det.

For data som du må lagre, er Android-arkitekturen klar til å hjelpe. Siden 6.0 Marshmallow er fulldiskkryptering aktivert som standard for enheter med mulighet. Filer og SharedPreferences som lagres av appen, settes automatisk inn med MODE_PRIVATE konstant. Dette betyr at dataene kun kan nås av din egen app. 

Det er en god ide å holde fast ved denne standarden. Du kan angi det eksplisitt når du lagrer en felles preferanse.

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

Eller når du lagrer en fil.

FileOutputStream fos = openFileOutput (filnavnString, kontekst.MODE_PRIVATE); fos.write (data); fos.close ();

Unngå å lagre data på ekstern lagring, da dataene er synlige av andre apper og brukere. Faktisk, for å gjøre det vanskeligere for folk å kopiere appens binære data og data, kan du forhindre at brukere kan installere appen på ekstern lagring. legge android: install med en verdi på internalOnly til manifestfilen vil oppnå det.

Du kan også forhindre at appen og dataene blir sikkerhetskopiert. Dette forhindrer også at innholdet i en apps private datakatalog lastes ned med Adb backup. For å gjøre det, sett inn android: allowBackup tilskrive falsk i manifestfilen. Som standard er denne attributtet satt til ekte.

Disse er beste praksis, men de fungerer ikke for en kompromittert eller rotfestet enhet, og diskkryptering er bare nyttig når enheten er sikret med en låseskjerm. Dette er hvor å ha et app-side passord som beskytter dataene med kryptering, er fordelaktig.

Sikre brukerdata med et passord

Conceal er et godt valg for et krypteringsbibliotek fordi det får deg til å løpe veldig fort uten å måtte bekymre deg for de underliggende detaljene. En utnyttelse rettet mot et populært rammeverk vil imidlertid samtidig påvirke alle appene som er avhengige av det. 

Det er også viktig å være kunnskapsrik om hvordan krypteringssystemer fungerer for å kunne fortelle om du bruker et bestemt rammeverk sikkert. Så, for dette innlegget, kommer vi til å få hendene våre skitne ved å se på krypteringsleverandøren direkte. 

AES og Password-Based Key Derivation

Vi bruker den anbefalte AES-standarden, som krypterer data som er gitt en nøkkel. Den samme nøkkelen som brukes til å kryptere data, brukes til å dekryptere dataene, som kalles symmetrisk kryptering. Det er forskjellige nøkkelstørrelser, og AES256 (256 bits) er den foretrukne lengden for bruk med sensitive data.

Selv om brukeropplevelsen av appen din burde tvinge en bruker til å bruke et sterkt passord, er det en sjanse for at samme passord også blir valgt av en annen bruker. Det er ikke trygt å sette sikkerhet for våre krypterte data i brukerens hender. Våre data må sikres i stedet med en nøkkel Det er tilfeldig og stort nok (dvs. det har nok entropi) å bli ansett som sterk. Derfor anbefales det aldri å bruke et passord direkte for å kryptere data - det er her en funksjon kalles Passordbasert nøkkelavledningsfunksjon (PBKDF2) kommer inn i spill. 

PBKDF2 oppnår a nøkkel fra en passord ved hashing det mange ganger med et salt. Dette kalles nøkkelstrekning. Saltet er bare en tilfeldig rekkefølge av data og gjør den avledede nøkkelen unik selv om det samme passordet ble brukt av noen andre. 

La oss starte med å generere det saltet. 

SecureRandom random = new SecureRandom (); bytesalt [] = nytt byte [256]; random.nextBytes (salt);

De SecureRandom klassen garanterer at den genererte produksjonen blir vanskelig å forutsi - det er en "kryptografisk sterk tilfeldig talgenerator". Vi kan nå sette salt og passord inn i et passordbasert krypteringsobjekt: PBEKeySpec. Objektets konstruktør tar også et iterasjonstallskjema, noe som gjør nøkkelen sterkere. Dette skyldes at økende antall iterasjoner utvider den tiden det ville ta å operere på et sett med nøkler under et brutalt kraftangrep. De PBEKeySpec så blir det passert inn i SecretKeyFactory, som til slutt genererer nøkkelen som en byte [] array. Vi vil vikle den råen byte [] array inn i a SecretKeySpec gjenstand.

char [] passwordChar = passwordString.toCharArray (); // Slå passord til char [] array PBEKeySpec pbKeySpec = ny PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iterasjoner SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = ny SecretKeySpec (keyBytes, "AES");

Merk at passordet er bestått som en char [] array og PBEKeySpec klassen lagrer det som en char [] array også. char [] Arrays brukes vanligvis til kryptering funksjoner fordi mens string klassen er uforanderlig, a char [] array som inneholder sensitiv informasjon kan overskrives, og dermed fjerner du sensitive data helt fra enhetens minne.

Initialiseringsvektorer

Vi er nå klar til å kryptere dataene, men vi har en ting å gjøre. Det finnes forskjellige krypteringsmoduser med AES, men vi bruker den anbefalte en: krypteringsblokkering (CBC). Dette opererer på våre data ett kvartal om gangen. Den gode tingen om denne modusen er at hver neste ukrypterte blokk med data er XOR'd med den forrige krypterte blokk for å gjøre krypteringen sterkere. Det betyr imidlertid at den første blokken aldri er like unik som alle de andre! 

Hvis en melding som skal krypteres skulle starte med det samme som en annen melding som skal krypteres, vil den begynnelsen krypterte utdata være den samme, og det vil gi en angriper en anelse om å finne ut hva meldingen kan være. Løsningen er å bruke en initialiseringsvektor (IV). 

En IV er bare en blokk med tilfeldige byte som vil bli XOR'd med den første blokken med brukerdata. Siden hver blokk avhenger av alle blokkene som er behandlet frem til det punktet, vil hele meldingen bli kryptert unikt identiske meldinger som krypteres med samme nøkkel, vil ikke produsere like resultater. 

La oss lage en IV nå.

SecureRandom ivRandom = nytt SecureRandom (); // ikke caching tidligere seeded forekomst av SecureRandom byte [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nytt IvParameterSpec (iv);

Et notat om SecureRandom. I versjon 4.3 og under hadde Java kryptografiarkitekturen et sårbarhet på grunn av feil initialisering av den underliggende pseudorandom-tallgeneratoren (PRNG). Hvis du målretter mot versjon 4.3 og under, er en løsning tilgjengelig.

Kryptering av dataene

Bevæpnet med en IvParameterSpec, Vi kan nå gjøre den faktiske kryptering.

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

Her passerer vi i strengen "AES / CBC / PKCS7Padding". Dette spesifiserer AES-kryptering med cypher-blokkering. Den siste delen av denne strengen refererer til PKCS7, som er en etablert standard for polstringsdata som ikke passer perfekt inn i blokkstørrelsen. (Blokker er 128 bits, og polstring er ferdig før kryptering.)

For å fullføre vårt eksempel, vil vi sette denne koden i en krypteringsmetode som vil pakke resultatet inn i a HashMap inneholder de krypterte dataene sammen med salt- og initialiseringsvektoren som er nødvendig for dekryptering.

privat HashMap encryptBytes (byte [] plainTextBytes, String passwordString) HashMap map = ny HashMap(); prøv // tilfeldig salt for neste trinn SecureRandom random = new SecureRandom (); bytesalt [] = nytt byte [256]; random.nextBytes (salt); // PBKDF2 - hent nøkkelen fra passordet, ikke bruk passord direkte char [] passwordChar = passwordString.toCharArray (); // Slå passord til char [] array PBEKeySpec pbKeySpec = ny PBEKeySpec (passwordChar, salt, 1324, 256); // 1324 iterasjoner SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = ny SecretKeySpec (keyBytes, "AES"); // Opprett initialiseringsvektor for AES SecureRandom ivRandom = nytt SecureRandom (); // ikke caching tidligere seeded forekomst av SecureRandom byte [] iv = new byte [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nytt IvParameterSpec (iv); // Krypter Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte [] encrypted = cipher.doFinal (plainTextBytes); map.put ("salt", salt); map.put ("iv", iv); map.put ("kryptert", kryptert);  fangst (Unntak e) Log.e ("MYAPP", "kryptering unntak", e);  returkort; 

Dekrypteringsmetoden

Du trenger bare å lagre IV og salt med dataene dine. Mens salter og IVs regnes som offentlige, sørg for at de ikke sekvensielt økes eller gjenbrukes. For å dekryptere dataene, er alt vi trenger å gjøre, å endre modusen i cipher konstruktør fra ENCRYPT_MODE til DECRYPT_MODE

Dekrypteringsmetoden vil ta en HashMap som inneholder samme nødvendige opplysninger (kryptert data, salt og IV) og returnerer en dekryptert byte [] array, gitt riktig passord. Dekrypteringsmetoden vil regenerere krypteringsnøkkelen fra passordet. Nøkkelen skal aldri lagres!

privat byte [] decryptData (HashMap kart, String PasswordString) byte [] decrypted = null; prøv byte salt [] = map.get ("salt"); byte iv [] = map.get ("iv"); byte kryptert [] = map.get ("kryptert"); // regenerere nøkkel fra passordkarakteristikk [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = ny PBEKeySpec (passwordChar, salt, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); byte [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = ny SecretKeySpec (keyBytes, "AES"); // Dekrypter Cipher cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = nytt IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); dekryptert = cipher.doFinal (kryptert);  fangst (Unntak e) Log.e ("MYAPP", "dekryptering unntak", e);  returnere dekryptert; 

Testing av kryptering og dekryptering

For å holde eksemplet enkelt, unnlater vi feilkontroll som vil sørge for at HashMap inneholder nødvendig nøkkel, verdi par. Vi kan nå teste våre metoder for å sikre at dataene dekrypteres riktig etter kryptering.

// Krypteringstest String string = "Min sensitive streng som jeg vil kryptere"; byte [] bytes = string.getBytes (); HashMap map = encryptBytes (bytes, "UserSuppliedPassword"); // Dekrypteringstest byte [] dekryptert = decryptData (kart, "UserSuppliedPassword"); hvis (dekryptert! = null) String decryptedString = ny String (dekryptert); Log.e ("MYAPP", "Decrypted String er:" + decryptedString); 

Metodene bruker a byte [] array slik at du kan kryptere vilkårlig data i stedet for bare string objekter. 

Lagring av krypterte data

Nå som vi har en kryptert byte [] array, kan vi lagre det til lagring.

FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (kryptert); fos.close ();

Hvis du ikke vil lagre IV og salt separat, HashMap er serialiserbar med Object og ObjectOutputStream klasser.

FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = nytt ObjectOutputStream (fos); oos.writeObject (kart); oos.close ();

Lagrer sikre data til SharedPreferences

Du kan også lagre sikre data i appen din SharedPreferences.

SharedPreferences.Editor editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); StringsnøkkelBase64String = Base64.encodeToString (kryptertKey, Base64.NO_WRAP); String valueBase64String = Base64.encodeToString (kryptertValue, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();

Siden SharedPreferences er et XML-system som bare aksepterer bestemte primitiver og objekter som verdier, må vi konvertere dataene våre til et kompatibelt format, for eksempel en string gjenstand. Base64 lar oss konvertere de rå dataene til en string representasjon som bare inneholder tegnene som tillates av XML-formatet. Krypter både nøkkelen og verdien slik at en angriper ikke kan finne ut hva en verdi kan være for. 

I eksemplet ovenfor, encryptedKey og encryptedValue er begge kryptert byte [] arrays returnerte fra vår encryptBytes () metode. IV og salt kan lagres i preferansefilen eller som en egen fil. For å få tilbake de krypterte bytes fra SharedPreferences, vi kan bruke et Base64-dekoder på den lagrede string.

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

Sletting av usikre data fra gamle versjoner

Nå som de lagrede dataene er sikre, kan det være tilfelle at du har en tidligere versjon av appen som lagret dataene usikkert. Ved en oppgradering kan dataene bli slettet og omkryptert. Følgende kode tørker over en fil ved hjelp av tilfeldige data. 

I teorien kan du bare slette dine delte preferanser ved å fjerne /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml og your_prefs_name.bak filer og rydde inninnstillingene i minnet med følgende kode:

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

Men i stedet for å forsøke å tørke de gamle dataene og håper at det fungerer, er det bedre å kryptere det i utgangspunktet! Dette gjelder spesielt generelt for solid state-stasjoner som ofte sprer ut dataskriver til forskjellige regioner for å hindre slitasje. Det betyr at selv om du overskriver en fil i filsystemet, kan det fysiske solid state-minnet lagre dataene dine i sin opprinnelige plassering på disken.

offentlig statisk tomt secureWipeFile (filfil) kaster IOException if (fil! = null && file.exists ()) endelig lang lengde = file.length (); endelig SecureRandom random = new SecureRandom (); endelig RandomAccessFile randomAccessFile = ny RandomAccessFile (fil, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); byte [] data = ny byte [64]; int posisjon = 0; mens (posisjon < length)  random.nextBytes(data); randomAccessFile.write(data); position += data.length;  randomAccessFile.close(); file.delete();  

Konklusjon

Det bryter opp vår veiledning om lagring av krypterte data. I dette innlegget lærte du hvordan du trygt krypterer og dekrypterer sensitive data med et brukerleverandørpassord. Det er lett å gjøre når du vet hvordan, men det er viktig å følge alle de beste metodene for å sikre at brukerne dine data er helt sikre.

I neste innlegg vil vi se på hvordan du kan utnytte keystore og andre legitimasjonsrelaterte APIer for å lagre gjenstander trygt. I mellomtiden kan du sjekke ut noen av våre andre gode artikler om Android App-utvikling.