Chei, acreditare și stocare pe Android

În postarea precedentă privind securitatea datelor pentru utilizatorii Android, am analizat datele de criptare prin intermediul unui cod de acces furnizat de utilizator. Acest tutorial va îndrepta focalizarea spre acreditare și stocarea cheie. Voi începe prin introducerea acreditărilor contului și se va încheia cu un exemplu de protejare a datelor utilizând KeyStore.

Adesea, atunci când lucrați cu un serviciu terță parte, va exista o anumită formă de autentificare necesară. Acest lucru poate fi la fel de simplu ca a /Logare punct final care acceptă un nume de utilizator și o parolă. 

S-ar părea la început că o soluție simplă este de a construi un UI care cere utilizatorului să se conecteze, apoi captează și stochează datele de conectare. Cu toate acestea, aceasta nu este cea mai bună practică, deoarece aplicația noastră nu trebuie să cunoască acreditările pentru un cont terță parte. În schimb, putem folosi Managerul de cont, care deleagă manipularea informațiilor delicate pentru noi.

Manager Conturi

Managerul de cont este un ajutor centralizat pentru acreditările contului de utilizator, astfel încât aplicația să nu aibă de-a face direct cu parolele. Acesta oferă adesea un simbol în locul numelui de utilizator real și a parolei care poate fi folosit pentru a face cereri autentificate unui serviciu. Un exemplu este atunci când solicitați un jeton OAuth2. 

Uneori, toate informațiile necesare sunt deja stocate pe dispozitiv, iar alteori Managerul de cont va trebui să apeleze un server pentru un token reîmprospătat. S-ar putea să fi văzut Conturi secțiune din Setările dispozitivului pentru diverse aplicații. Putem obține acea listă de conturi disponibile, cum ar fi:

AccountManager accountManager = AccountManager.get (acest lucru); Contul [] conturi = accountManager.getAccounts ();

Codul va cere android.permission.GET_ACCOUNTS permisiune. Dacă sunteți în căutarea unui anumit cont, îl puteți găsi astfel:

AccountManager accountManager = AccountManager.get (acest lucru); Contul [] accounts = accountManager.getAccountsByType ("com.google");

Odată ce aveți contul, un token pentru cont poate fi preluat apelând getAuthToken (Cont, String, Bundle, Activitate, AccountManagerCallback, Handler) metodă. Jetonul poate fi apoi utilizat pentru a face cereri autentificate API pentru un serviciu. Acesta ar putea fi un API RESTful în cazul în care introduceți un parametru token în timpul unei solicitări HTTPS, fără a trebui să cunoașteți detaliile contului particular al utilizatorului.

Deoarece fiecare serviciu va avea un mod diferit de autentificare și stocare a acreditărilor private, Managerul de cont oferă module de autentificare pentru implementarea unui serviciu terță parte. În timp ce Android are implementări pentru multe servicii populare, înseamnă că puteți să vă scrieți propriul autentificator pentru a gestiona autentificarea contului și stocarea acreditărilor din aplicație. Acest lucru vă permite să vă asigurați că acreditările sunt criptate. Rețineți că aceasta înseamnă, de asemenea, că acreditările din Managerul de cont folosite de alte servicii pot fi stocate în text clar, făcându-le vizibile pentru oricine care și-a înrădăcit dispozitivul.

În loc de acreditări simple, există momente când va trebui să faceți față unei chei sau a unui certificat pentru o persoană fizică sau pentru o entitate - de exemplu, atunci când o terță parte vă trimite un fișier de certificat pe care trebuie să îl păstrați. Cel mai frecvent scenariu este când o aplicație trebuie să se autentifice pe serverul unei organizații private. 

În tutorialul următor, vom analiza utilizarea certificatelor de autentificare și a comunicațiilor sigure, dar tot vreau să mă refer la modul de păstrare a acestor elemente între timp. API-ul Keychain a fost inițial construit pentru acea utilizare foarte specifică - instalând o pereche de chei private sau o pereche de certificate dintr-un fișier PKCS # 12.

Keychain-ul

Prezentat în Android 4.0 (API Level 14), API-ul Keychain se ocupă de gestionarea cheilor. În mod specific, funcționează cu PrivateKey și X509Certificate obiecte și oferă un container mai sigur decât utilizarea spațiului de stocare al aplicației. Aceasta deoarece permisiunile pentru cheile private permit numai aplicației dvs. să acceseze cheile și numai după autorizarea utilizatorului. Aceasta înseamnă că pe dispozitiv trebuie să fie instalat un ecran de blocare înainte de a putea utiliza stocarea acreditărilor. De asemenea, obiectele din breloc pot fi obligate să asigure hardware-ul, dacă este disponibil. 

Codul pentru instalarea unui certificat este următorul:

Intenția intenției = KeyChain.createInstallIntent (); byte [] p12Bytes = // ... citiți din fișier, cum ar fi example.pfx sau example.p12 ... intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (intenție);

Utilizatorul va fi solicitat pentru o parolă pentru a accesa cheia privată și o opțiune pentru denumirea certificatului. Pentru a prelua cheia, următorul cod prezintă un interfață utilizator care permite utilizatorului să aleagă din lista de taste instalate.

KeyChain.choosePrivateKeyAlias ​​(acest nou string [] "RSA", null, null, -1, null);

Odată ce alegerea este făcută, un nume alias de șir este returnat în alias (alias String final) apel invers unde puteți accesa direct cheia privată sau lanțul de certificate.

clasei publice KeychainTest se extinde Activitatea implementează ..., KeyChainAliasCallback // ... @Override alias public void (ultimul alias String) Log.e ("MyApp", "Alias ​​este" + alias); încercați PrivateKey privateKey = KeyChain.getPrivateKey (acest alias); X509Certificate [] certificateChain = KeyChain.getCertificateChain (acest alias);  captură…  //… 

Înarmat cu aceste cunoștințe, să vedem acum modul în care putem folosi spațiul de stocare pentru acreditare pentru a vă salva propriile date sensibile.

KeyStore

În tutorialul anterior, am analizat protecția datelor prin intermediul unui cod de acces furnizat de utilizator. Acest tip de configurare este bun, dar cerințele aplicației se îndepărtează adesea de faptul că utilizatorii se loghează de fiecare dată și își aduc aminte de un cod de acces suplimentar. 

Aici se poate utiliza API-ul KeyStore. De la API 1, KeyStore a fost utilizat de sistem pentru a stoca acreditările WiFi și VPN. Începând cu 4.3 (API 18), vă permite să lucrați cu propriile taste asimetrice specifice aplicației, iar în Android M (API 23) puteți stoca o cheie simetrică AES. Deci, în timp ce API nu permite stocarea directă a șirurilor sensibile, aceste chei pot fi stocate și apoi folosite pentru a cripta șiruri de caractere. 

Beneficiul pentru stocarea unei chei în KeyStore este că permite funcționarea cheilor fără a expune conținutul secret al acelei chei; datele cheie nu intră în spațiul aplicației. Amintiți-vă că cheile sunt protejate de permisiuni, astfel încât numai aplicația dvs. să le poată accesa și că acestea pot fi în plus protejate prin hardware dacă dispozitivul este capabil. Acest lucru creează un container care face mai dificilă extragerea cheilor de la un dispozitiv. 

Generați o nouă cheie aleatorie

Pentru acest exemplu, în loc să generăm o cheie AES din codul de acces furnizat de utilizator, putem genera automat o cheie aleatoare care va fi protejată în KeyStore. Putem face acest lucru prin crearea unui KeyGenerator exemplu, setat la "AndroidKeyStore" furnizor.

// Generați o cheie și păstrați-o în KeyGetatorul final KeyGenerator keyGenerator = 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) // necesită ecran de blocare, dacă este invalidată ecranul de blocare este dezactivat //.setUserAuthenticationValidityDurationSeconds(120) // disponibil numai de secunde de autentificare cu parolă. -1 necesită degetul imprimat - de fiecare dată .setRandomizedEncryptionRequired (true) // diferiți texte cipher pentru același plaintext pe fiecare call .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();

Particulele importante pe care le puteți vedea aici sunt .setUserAuthenticationRequired (true) și .setUserAuthenticationValidityDurationSeconds (120) specificații. Acestea necesită setarea unui ecran de blocare și cheia care trebuie blocată până la autentificarea utilizatorului. 

Privind documentația pentru .setUserAuthenticationValidityDurationSeconds (), veți vedea că aceasta înseamnă că cheia este disponibilă numai pentru un anumit număr de secunde de la autentificarea prin parolă și că trecerea -1 necesită autentificare prin amprentă de fiecare dată când doriți să accesați cheia. Activarea cerinței de autentificare are de asemenea efectul de a revoca cheia atunci când utilizatorul îndepărtează sau modifică ecranul de blocare. 

Deoarece stocarea unei chei neprotejate de-a lungul datelor criptate este ca și cum ați pune o cheie de sub casă, aceste opțiuni încearcă să protejeze cheia în repaus în cazul în care un dispozitiv este compromis. Un exemplu ar putea fi un dump de date offline al dispozitivului. Fără parola cunoscută pentru dispozitiv, aceste date devin inutile.

.setRandomizedEncryptionRequired (true) opțiunea permite necesitatea unei randomizări suficiente (o dată la fiecare nouă întâlnire), astfel încât, dacă aceleași date sunt criptate a doua oară, această ieșire criptată va fi în continuare diferită. Acest lucru împiedică un atacator să obțină indicii despre textul cifrat pe baza alimentării în aceleași date. 

O altă opțiune de observat este setUserAuthenticationValidWhileOnBody (rădăcină booleanăValid), care blochează cheia o dată ce dispozitivul a detectat că nu mai este pe persoană.

Criptarea datelor

Acum, când cheia este stocată în KeyStore, putem crea o metodă care criptează datele utilizând Cifru obiect, având în vedere Cheie secreta. Se va întoarce a HashMap conținând datele criptate și un IV randomizat care va fi necesar pentru a decripta datele. Datele criptate, alături de IV, pot fi apoi salvate într-un fișier sau în preferințele partajate.

privat HashMap cripta (ultimul octet [] decryptedBytes) final HashMap map = nou HashMap(); încercați // Obțineți cheie finală KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); finală KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); finală SecretKey secretKey = secretKeyEntry.getSecretKey (); // Criptarea datelor finale Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); ultimul octet [] ivBytes = cipher.getIV (); ultimul octet [] encryptedBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("criptat", encryptedBytes);  captură (aruncabilă e) e.printStackTrace ();  întoarcere hartă; 

Decriptarea la o array de octeți

Pentru decriptare se aplică inversa. Cifru obiect este inițializat folosind DECRYPT_MODE constanta si decriptata byte [] matricea este returnată.

byte privat [] decripta (final HashMap harta) byte [] decryptedBytes = null; încercați // Obțineți cheie finală KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); finală KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); finală SecretKey secretKey = secretKeyEntry.getSecretKey (); // extrageți informații din harta ultimului byte [] encryptedBytes = map.get ("criptat"); ultimul octet [] ivBytes = map.get ("iv"); // Decodifică datele finale Cipher Cipher = Cipher.getInstance ("AES / GCM / NoPadding"); finală GCMParameterSpec spec = nou GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (encryptedBytes);  captură (aruncabilă e) e.printStackTrace ();  return decryptedBytes; 

Testarea exemplului

Acum putem testa exemplul nostru!

@TargetApi (Build.VERSION_CODES.M) test privat voidEncryption () try // Generează o cheie și stochează-o în KeyGenerator cheie cheie KeyGenerator = 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) // necesită ecran de blocare, dacă este invalidată ecranul de blocare este dezactivat //.setUserAuthenticationValidityDurationSeconds(120) // disponibil numai de secunde de autentificare cu parolă. -1 necesită degetul imprimat - de fiecare dată .setRandomizedEncryptionRequired (true) // diferiți texte cipher pentru același plaintext pe fiecare call .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Testați HashMap final map = criptat ("Șirul meu foarte sensibil!". getBytes ("UTF-8")); ultimul byte [] decryptedBytes = decripta (harta); final String decryptedString = String nou (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Șirul decriptat este" + decryptedString ");  captură (aruncabilă e) e.printStackTrace (); 

Utilizarea cheilor asimetrice RSA pentru dispozitivele vechi

Aceasta este o soluție bună pentru stocarea datelor pentru versiunile M și peste, dar dacă aplicația acceptă versiuni anterioare? În timp ce tastele simetrice AES nu sunt acceptate sub M, tastele asimetrice RSA sunt. Asta înseamnă că putem folosi cheile RSA și criptarea pentru a realiza același lucru. 

Principala diferență aici este că o cheie cheie asimetrică conține două chei, o cheie privată și una publică, unde cheia publică criptează datele, iar cheia privată o decriptează. A KeyPairGeneratorSpec este trecut în KeyPairGenerator care este inițializat cu KEY_ALGORITHM_RSA si "AndroidKeyStore" furnizor.

private void testPreMEncryption () try // Generați o pereche cheie și păstrați-o în KeyStore KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Calendar start = Calendar.getInstance (); Sfârșitul calendarului = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder (acest) .setAlias ​​("MyKeyAlias") .setSubject (noul X500Principal ("CN = MyKeyName, O = autoritatea Android")) .SerialNumber (noul BigInteger (1024, new Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // la nivelul 18 API, criptat în repaus, necesită configurarea ecranului de blocare, schimbarea ecranului de blocare elimină tasta .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Testul de criptare final byte [] encryptedBytes = rsaEncrypt ("Șirul meu secret!". GetBytes ("UTF-8")); ultimul octet [] decryptedBytes = rsaDecrypt (encryptedBytes); final String decryptedString = String nou (decryptedBytes, "UTF-8"); Log.e ("MyApp", "Șirul decriptat este" + decryptedString ");  captură (aruncabilă e) e.printStackTrace (); 

Pentru a cripta, obținem RSAPublicKey de la perechea cheie și folosiți-o cu Cifru obiect. 

byte public [] rsaEncrypt (final octet [] decryptedBytes) byte [] encryptedBytes = null; încercați final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); finala KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); finală RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate (). getPublicKey (); cifra finală Cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); final ByteArrayOutputStream outputStream = nou ByteArrayOutputStream (); finit CipherOutputStream cipherOutputStream = nou CipherOutputStream (outputStream, cifru); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray ();  captură (aruncabilă e) e.printStackTrace ();  retur encryptedBytes; 

Decriptarea se face folosind RSAPrivateKey obiect.

byte public [] rsaDecrypt (final octet [] encryptedBytes) byte [] decryptedBytes = null; încercați final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); finala KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); ultimul RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey (); cifra finală Cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); finală CipherInputStream cipherInputStream = nou CipherInputStream (new ByteArrayInputStream (encryptedBytes), cifru); final ArrayList arrayList = noul ArrayList <> (); int nextByte; în timp ce ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((byte) nextByte);  decryptedBytes = nou octet [arrayList.size ()]; pentru (int i = 0; i < decryptedBytes.length; i++)  decryptedBytes[i] = arrayList.get(i);   catch (Throwable e)  e.printStackTrace();  return decryptedBytes; 

Un lucru despre RSA este că criptarea este mai lentă decât în ​​AES. Acest lucru este, de obicei, bine pentru cantități mici de informații, cum ar fi atunci când securiziți șiruri de preferințe partajate. Dacă găsiți că există o problemă de performanță care criptează cantități mari de date, cu toate acestea, puteți folosi acest exemplu pentru a cripta și a stoca doar o cheie AES. Apoi, utilizați această criptare AES mai rapidă, care a fost discutată în tutorialul anterior pentru restul datelor. Aveți posibilitatea să generați o nouă cheie AES și să o convertiți în a byte [] matrice compatibilă cu acest exemplu.

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

Pentru a obține cheia înapoi de la octeți, procedați astfel:

SecretKey cheie = nou SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");

A fost o mulțime de cod! Pentru a păstra toate exemplele simple, am renunțat la tratarea excepțională. Dar țineți minte că pentru codul dvs. de producție nu este recomandat pur și simplu să prindeți toate Dispensabil cazuri într-o declarație de captură.

Concluzie

Acest lucru completează tutorialul privind lucrul cu acreditările și cheile. O mare parte din confuzia din jurul tastelor și a spațiului de stocare are legătură cu evoluția sistemului de operare Android, însă puteți alege ce soluție să utilizați, având în vedere nivelul API pe care aplicația dvs. îl suportă. 

Acum, că am acoperit cele mai bune practici pentru securizarea datelor în repaus, tutorialul următor se va concentra pe securizarea datelor în tranzit. 

Cod