Credibilitatea unei aplicații depinde în mare măsură de gestionarea datelor private ale utilizatorului. Stack-ul Android are multe API-uri puternice care înconjoară acreditările și spațiul de stocare cheie, cu caracteristici specifice disponibile numai în anumite versiuni.
Această serie scurtă va începe cu o abordare simplă de a intra în funcțiune, analizând sistemul de stocare și modul de criptare și stocare a datelor sensibile printr-un cod de acces furnizat de utilizator. În cel de-al doilea tutorial, vom examina modalități mai complexe de protejare a cheilor și a acreditărilor.
Prima întrebare pe care trebuie să o gândiți este cât de multe date trebuie să achiziționați. O bună abordare este evitarea stocării datelor private, dacă nu trebuie.
Pentru datele pe care trebuie să le stocați, arhitectura Android este gata să vă ajute. Din moment ce 6.0 Marshmallow, criptarea full-disk este activată implicit, pentru dispozitivele cu capacitate. Fișiere și SharedPreferences
care sunt salvate de aplicație sunt setate automat cu MODE_PRIVATE
constant. Aceasta înseamnă că datele pot fi accesate numai de propria aplicație.
Este o idee bună să rămânem la această valoare implicită. Puteți să o setați în mod explicit atunci când salvați o preferință partajată.
Editorul SharedPreferences.Editor = getSharedPreferences ("preferenceName", MODE_PRIVATE) .edit (); editor.putString ("cheie", "valoare"); editor.commit ();
Sau când salvați un fișier.
FileOutputStream fos = openFileOutput (nume fișierString, Context.MODE_PRIVATE); fos.write (date); fos.close ();
Evitați stocarea datelor pe spațiul de stocare extern, deoarece datele sunt vizibile apoi de alte aplicații și utilizatori. De fapt, pentru a împiedica oamenii să copieze aplicația binară și datele dvs., puteți împiedica utilizatorii să poată instala aplicația pe spațiul de stocare extern. adăugare Android: installLocation
cu o valoare de internalOnly
la fișierul manifest va realiza acest lucru.
De asemenea, puteți împiedica ca aplicația și datele acesteia să fie copiate în siguranță. De asemenea, acest lucru împiedică descărcarea conținutului directorului de date privat al unei aplicații adb backup
. Pentru aceasta, setați Android: allowBackup
atribuit lui fals
în fișierul manifest. Implicit, acest atribut este setat la Adevărat
.
Acestea sunt cele mai bune practici, dar nu vor funcționa pentru un dispozitiv compromis sau înrădăcinat, iar criptarea discului este utilă numai atunci când dispozitivul este securizat cu un ecran de blocare. Aici este benefică o parolă care să protejeze datele cu criptare.
Conceal este o alegere excelentă pentru o bibliotecă de criptare, deoarece vă devine și funcționează foarte repede, fără să vă faceți griji cu privire la detaliile care stau la baza. Cu toate acestea, un exploit vizat pentru un cadru popular va afecta simultan toate aplicațiile care se bazează pe acesta.
De asemenea, este important să aveți cunoștință despre modul în care funcționează sistemele de criptare pentru a putea afla dacă folosiți un cadru anume în mod sigur. Deci, pentru acest post, vom mânca mâinile uitându-ne direct la furnizorul de criptografie.
Vom folosi standardul recomandat AES, care criptează datele date unei chei. Aceeași cheie folosită pentru a cripta datele este folosită pentru a decripta datele, ceea ce se numește criptare simetrică. Există diferite dimensiuni de taste, iar AES256 (256 biți) este lungimea preferată pentru utilizarea cu date sensibile.
În timp ce experiența utilizatorului din aplicația dvs. ar trebui să forțeze un utilizator să utilizeze un cod de acces puternic, există șansa ca același cod de parol să fie, de asemenea, ales de alt utilizator. Punerea securității datelor criptate în mâinile utilizatorului nu este sigură. Datele noastre trebuie să fie securizate în schimb cu un cheie care este destul de aleator și destul de mare (adică care are suficientă entropie) pentru a fi considerată puternică. De aceea nu este recomandat să folosiți o parolă direct pentru criptarea datelor - adică acolo unde este apelată o funcție Funcția de derivare a cheilor bazate pe parolă (PBKDF2) intră în joc.
PBKDF2 derivă a cheie de la parola prin ștergerea de mai multe ori cu o sare. Aceasta se numește stretching cheie. Sarea este doar o secvență aleatoare de date și face ca tasta derivată să fie unică, chiar dacă aceeași parolă a fost utilizată de altcineva.
Să începem prin generarea acelei sare.
SecureRandom aleator = nou SecureRandom (); byte salt [] = nou octet [256]; random.nextBytes (sare);
SecureRandom
garantează că producția generată va fi greu de prezis - este un "generator de numere aleatoare cu un număr criptografic puternic". Acum putem pune sarea și parola într-un obiect de criptare bazat pe parolă: PBEKeySpec
. Constructorul obiectului are de asemenea un număr de iterație, făcând cheia mai puternică. Acest lucru se datorează faptului că creșterea numărului de iterații extinde timpul necesar pentru a funcționa pe un set de chei în timpul unui atac de forță brute. PBEKeySpec
apoi este trecut în SecretKeyFactory
, care generează în final cheia ca a byte []
matrice. Vom înfășura bruta byte []
array într-un SecretKeySpec
obiect.
char [] parolaChar = parolaString.toCharArray (); // Schimbați parola în char [] array PBEKeySpec pbKeySpec = nou PBEKeySpec (passwordChar, sare, 1324, 256); // 1324 iterații SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); octet [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nou SecretKeySpec (keyBytes, "AES");
Rețineți că parola este trecută ca a carboniza[]
array, și PBEKeySpec
clasa îl stochează ca pe un carboniza[]
array, de asemenea. carboniza[]
array-urile sunt de obicei folosite pentru funcțiile de criptare, deoarece în timp ce Şir
clasa este imuabilă, a carboniza[]
array care conține informații sensibile poate fi suprascris - eliminând astfel datele sensibile din memoria dispozitivului.
Suntem gata să criptați datele, dar avem încă un lucru de făcut. Există diferite moduri de criptare cu AES, dar vom folosi cel recomandat: blocarea blocului de cifru (CBC). Aceasta funcționează pe datele noastre câte un bloc la un moment dat. Lucrul minunat în legătură cu acest mod este că fiecare următor bloc de date necriptat este XOR'd cu blocul criptat anterior pentru a face criptarea mai puternică. Cu toate acestea, acest lucru înseamnă că primul bloc nu este niciodată la fel de unic ca toate celelalte!
Dacă un mesaj care urmează a fi criptat trebuie să înceapă la fel ca un alt mesaj care urmează să fie criptat, ieșirea criptată de la început ar fi aceeași și ar da un atacator un indiciu pentru a afla ce mesaj ar putea fi. Soluția este de a folosi un vector de inițializare (IV).
Un IV este doar un bloc de octeți aleatorii care vor fi XOR'd cu primul bloc de date de utilizator. Deoarece fiecare bloc depinde de toate blocurile prelucrate până în acel moment, întregul mesaj va fi criptat mesajele unice identice criptate cu aceeași cheie nu vor produce rezultate identice.
Să creăm un IV acum.
SecureRandom ivRandom = nou SecureRandom (); // nu se cachează instanța precedentă a secvenței SecureRandom byte [] iv = nou octet [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nou IvParameterSpec (iv);
O notă despre SecureRandom
. Pe versiunile 4.3 și mai jos, arhitectura de criptografie Java a avut o vulnerabilitate datorită inițializării necorespunzătoare a generatorului de numere pseudo-numere de bază (prng). Dacă direcționați versiunile 4.3 și mai jos, este disponibilă o remediere.
Înarmat cu un IvParameterSpec
, acum putem face criptarea reală.
Cifor cifră = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); octet [] criptat = cipher.doFinal (plainTextBytes);
Aici trecem în șir "AES / CBC / PKCS7Padding"
. Aceasta specifică criptarea AES cu blocarea blocului de cifru. Ultima parte a acestui șir se referă la PKCS7, care este un standard stabilit pentru datele de umplutură care nu se încadrează perfect în dimensiunea blocului. (Blocurile sunt 128 de biți, iar umplutura se face înainte de criptare.)
Pentru a completa exemplul nostru, vom pune acest cod într-o metodă de criptare care va împacheta rezultatul într-un HashMap
conținând datele criptate, împreună cu vectorul de sare și inițializare necesar pentru decriptare.
privat HashMapencryptBytes (octet [] plainTextBytes, parolă String) HashMap map = nou HashMap (); încercați // Sare aleatorie pentru următorul pas SecureRandom random = new SecureRandom (); byte salt [] = nou octet [256]; random.nextBytes (sare); // PBKDF2 - extrage cheia de la parola, nu foloseste parole direct char [] passwordChar = passwordString.toCharArray (); // Schimbați parola în char [] array PBEKeySpec pbKeySpec = nou PBEKeySpec (passwordChar, sare, 1324, 256); // 1324 iterații SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); octet [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nou SecretKeySpec (keyBytes, "AES"); // Crearea vectorului de inițializare pentru AES SecureRandom ivRandom = new SecureRandom (); // nu se cachează instanța precedentă a secvenței SecureRandom byte [] iv = nou octet [16]; ivRandom.nextBytes (iv); IvParameterSpec ivSpec = nou IvParameterSpec (iv); // Cifra de criptare Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); cipher.init (Cipher.ENCRYPT_MODE, keySpec, ivSpec); octet [] criptat = cipher.doFinal (plainTextBytes); map.put ("sare", sare); map.put ("iv", iv); map.put ("criptat", criptat); captură (Excepție e) Log.e ("MYAPP", "excepție de criptare", e); întoarcere hartă;
Trebuie doar să stocați IV și sare cu datele dvs. În timp ce sărurile și IV-urile sunt considerate publice, asigurați-vă că nu sunt incrementate succesiv sau reutilizate. Pentru a decripta datele, tot ce trebuie să facem este să schimbăm modul în Cifru
constructor de la ENCRYPT_MODE
la DECRYPT_MODE
.
Metoda de decriptare va lua a HashMap
care conține aceleași informații necesare (date criptate, sare și IV) și returnează o descriere decriptată byte []
array, având în vedere parola corectă. Metoda de decriptare va genera cheia de criptare din parolă. Cheia nu ar trebui să fie stocată niciodată!
byte privat [] decryptData (HashMaphartă, parolă StringString) byte [] decrypted = null; încercați byte salt [] = map.get ("sare"); octetul iv [] = map.get ("iv"); byte criptat [] = map.get ("criptat"); // regenerați cheia din parola char [] passwordChar = passwordString.toCharArray (); PBEKeySpec pbKeySpec = nou PBEKeySpec (passwordChar, sare, 1324, 256); SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA1"); octet [] keyBytes = secretKeyFactory.generateSecret (pbKeySpec) .getEncoded (); SecretKeySpec keySpec = nou SecretKeySpec (keyBytes, "AES"); // Decrypt Cipher Cipher = Cipher.getInstance ("AES / CBC / PKCS7Padding"); IvParameterSpec ivSpec = nou IvParameterSpec (iv); cipher.init (Cipher.DECRYPT_MODE, keySpec, ivSpec); decodificat = cipher.doFinal (criptat); captură (Excepție e) Log.e ("MYAPP", "excepție decriptare", e); returnare decriptată;
Pentru a păstra exemplul simplu, omitem verificarea erorilor care ar fi sigur că HashMap
conține perechile de chei, valori necesare. Acum putem testa metodele noastre pentru a ne asigura că datele sunt decriptate corect după criptare.
// Test de criptare String string = "Șirul meu sensibil pe care vreau să îl criptez"; byte [] octeți = string.getBytes (); HashMapmap = encryptBytes (octeți, "UserSuppliedPassword"); // Byte de test de decriptare [] decrypted = decryptData (hartă, "UserSuppliedPassword"); dacă (decriptată! = null) String decryptedString = String nou (decriptat); Log.e ("MYAPP", "Codul decriptat este:" + decryptedString);
Metodele utilizează a byte []
array astfel încât să puteți cripta datele arbitrare în loc de numai Şir
obiecte.
Acum că avem un codat byte []
array, îl putem salva în spațiul de stocare.
FileOutputStream fos = openFileOutput ("test.dat", Context.MODE_PRIVATE); fos.write (criptat); fos.close ();
Dacă nu doriți să salvați IV și sarea separat, HashMap
este serializabil cu ObjectInputStream
și ObjectOutputStream
clase.
FileOutputStream fos = openFileOutput ("map.dat", Context.MODE_PRIVATE); ObjectOutputStream oos = ObjectOutputStream nou (fos); oos.writeObject (harta); oos.close ();
SharedPreferences
De asemenea, puteți salva datele sigure în aplicațiile dvs. SharedPreferences
.
Editorul SharedPreferences.Editor = getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit (); String keyBase64String = Base64.encodeToString (codificatKey, Base64.NO_WRAP); String valueBase64String = Base64.encodeToString (encryptedValue, Base64.NO_WRAP); editor.putString (keyBase64String, valueBase64String); editor.commit ();
Din moment ce SharedPreferences
este un sistem XML care acceptă numai primitive și obiecte specifice ca valori, trebuie să convertim datele noastre într-un format compatibil, cum ar fi Şir
obiect. Base64 ne permite să convertim datele brute într-un Şir
reprezentare care conține numai caracterele permise de formatul XML. Criptați atât cheia, cât și valoarea, astfel încât un atacator să nu poată da seama ce ar putea fi o valoare.
În exemplul de mai sus, encryptedKey
și encryptedValue
sunt ambele criptate byte []
arraile s-au întors de la noi encryptBytes ()
metodă. IV și sarea pot fi salvate în fișierul de preferințe sau ca fișier separat. Pentru a returna octeții criptat din SharedPreferences
, putem aplica o decodare Base64 pe memoria stocată Şir
.
SharedPreferences preferințe = getSharedPreferences ("prefs", Context.MODE_PRIVATE); String base64EncryptedString = preferences.getString (keyBase64String, "implicit"); octet [] encryptedBytes = Base64.decode (base64EncryptedString, Base64.NO_WRAP);
Acum, că datele stocate sunt sigure, este posibil să aveți o versiune anterioară a aplicației care a stocat datele nesigure. La o actualizare, datele ar putea fi șterse și re-criptate. Următorul cod șterge un fișier utilizând date aleatorii.
În teorie, poți să ștergi preferințele comune, eliminând /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml și your_prefs_name.bak fișiere și ștergerea preferințelor din memorie cu următorul cod:
getSharedPreferences ("prefs", Context.MODE_PRIVATE) .edit () .mai (); comite ();
Cu toate acestea, în loc să încercați să ștergeți datele vechi și sperând că funcționează, este mai bine să o criptați în primul rând! Acest lucru este valabil mai ales în general pentru driverele de stare solidă care deseori împrăștie date-scrie în diferite regiuni pentru a preveni uzura. Aceasta înseamnă că, chiar dacă suprascrieți un fișier în sistemul de fișiere, memoria fizică solidă poate păstra datele în locația inițială pe disc.
public void static secureWipeFile (fișier fișier) aruncă IOException if (fișier! = null && file.exists ()) fin lung lungime = file.length (); final SecureRandom aleator = nou SecureRandom (); finală RandomAccessFile randomAccessFile = nou RandomAccessFile (fișier, "rws"); randomAccessFile.seek (0); randomAccessFile.getFilePointer (); octet [] date = nou octet [64]; int pozitie = 0; în timp ce (poziția < length) random.nextBytes(data); randomAccessFile.write(data); position += data.length; randomAccessFile.close(); file.delete();
Asta ne întărește tutorialul despre stocarea datelor criptate. În acest post, ați învățat cum să criptați în siguranță și să decriptați datele sensibile cu o parolă furnizată de utilizator. Este ușor de făcut când știți cum, dar este important să urmați toate cele mai bune practici pentru a vă asigura că datele utilizatorilor dvs. sunt cu adevărat sigure.
În următorul post, vom examina modul în care se poate utiliza levierul depozite de chei
și alte API-uri legate de acreditare pentru a stoca în siguranță articolele. Între timp, verificați câteva din celelalte articole extraordinare despre dezvoltarea aplicațiilor Android.