Nøkler, legitimasjon og lagring på Android

I det forrige innlegget på Android-datasikkerhet, så vi på krypterende data via et passord som ble levert av brukeren. Denne opplæringen vil skifte fokus til legitimasjon og nøkkeloppbevaring. Jeg begynner med å introdusere kontolegitimasjon og avslutte med et eksempel på å beskytte data ved hjelp av KeyStore.

Ofte, når du jobber med en tredjepartstjeneste, vil det være nødvendig med noen form for godkjenning. Dette kan være så enkelt som a /Logg Inn sluttpunkt som aksepterer et brukernavn og passord. 

Det virker som om en enkel løsning er å bygge et brukergrensesnitt som ber brukeren om å logge inn, og deretter ta opp og lagre sine påloggingsinformasjon. Dette er imidlertid ikke den beste praksisen fordi vår app ikke trenger å kjenne legitimasjonene for en tredjepartskonto. I stedet kan vi bruke kontoadministratoren, som delegerer håndtering av den sensitive informasjonen for oss.

Kontoadministrator

Kontoadministratoren er en sentralisert hjelper for brukerkonto legitimasjon, slik at appen din ikke trenger å håndtere passord direkte. Det gir ofte et token i stedet for det virkelige brukernavnet og passordet som kan brukes til å gjøre godkjente forespørsler til en tjeneste. Et eksempel er når du ber om et OAuth2-token. 

Noen ganger er all nødvendig informasjon allerede lagret på enheten, og andre ganger må kontoadministratoren ringe til en server for et oppdatert token. Du har kanskje sett kontoer delen i enhetens Innstillinger for ulike apper. Vi kan få den listen over tilgjengelige kontoer som dette:

AccountManager accountManager = AccountManager.get (dette); Konto [] accounts = accountManager.getAccounts ();

Koden vil kreve android.permission.GET_ACCOUNTS tillatelse. Hvis du leter etter en bestemt konto, kan du finne det slik:

AccountManager accountManager = AccountManager.get (dette); Konto [] accounts = accountManager.getAccountsByType ("com.google");

Når du har kontoen, kan et token for kontoen hentes ved å ringe getAuthToken (Konto, String, Bundle, Aktivitet, AccountManagerCallback, Handler) metode. Token kan da brukes til å gjøre godkjente API-forespørsler til en tjeneste. Dette kan være en RESTful API der du passerer i en token-parameter under en HTTPS-forespørsel, uten å måtte vite brukerens private kontoopplysninger.

Fordi hver tjeneste har en annen måte å godkjenne og lagre private legitimasjonsbeskrivelser, tilbyr Kontoadministrator godkjenningsmoduler for en tredjepartstjeneste som skal implementeres. Mens Android har implementeringer for mange populære tjenester, betyr det at du kan skrive din egen autentiserer for å håndtere appens kontoautentisering og legitimasjonslagring. Dette gjør at du kan kontrollere at legitimasjonene er kryptert. Husk dette betyr også at legitimasjon i kontoadministratoren som brukes av andre tjenester, kan lagres i klar tekst, noe som gjør dem synlige for alle som har rotfestet enheten sin.

I stedet for enkle legitimasjonsbeskrivelser er det tidspunkter når du må håndtere en nøkkel eller et sertifikat for en person eller enhet, for eksempel når en tredjepart sender deg en sertifikatfil som du må beholde. Det vanligste scenariet er når en app må godkjenne til en privat organisasjonsserver. 

I den neste opplæringen vil vi se på bruk av sertifikater for godkjenning og sikker kommunikasjon, men jeg vil fortsatt adressere hvordan du lagrer disse elementene i mellomtiden. Nøkkelring-API ble opprinnelig bygget for den svært spesifikke bruken av å installere en privatnøkkel eller sertifikatpar fra en PKCS # 12-fil.

Nøkkelring

Introdusert i Android 4.0 (API-nivå 14) omhandler nøkkelring-APIen nøkkeladministrasjon. Spesielt virker det med Privat og X509Certificate objekter og gir en sikrere container enn å bruke appens datalagring. Det er fordi tillatelser for private nøkler bare tillater at din egen app får tilgang til nøklene, og bare etter brukerautorisasjon. Dette betyr at en låseskjerm må settes opp på enheten før du kan bruke legitimasjonslagringen. Objektene i nøkkelringen kan også være bundet til å sikre maskinvare, hvis tilgjengelig. 

Koden for å installere et sertifikat er som følger:

Intent Intent = KeyChain.createInstallIntent (); byte [] p12Bytes = // ... les fra fil, for eksempel example.pfx eller example.p12 ... intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (hensikt);

Brukeren blir bedt om å oppgi et passord for å få tilgang til den private nøkkelen og et alternativ for å navngi sertifikatet. For å hente nøkkelen presenterer følgende kode et brukergrensesnitt som lar brukeren velge fra listen over installerte nøkler.

KeyChain.choosePrivateKeyAlias ​​(dette, denne nye strengen [] "RSA", null, null, -1, null);

Når valget er gjort, returneres et streng aliasnavn i alias (siste streng alias) tilbakeringing hvor du kan få tilgang til den private nøkkelen eller sertifikatkjeden direkte.

offentlig klasse KeychainTest utvider Aktivitetsverktøy ..., KeyChainAliasCallback // ... @Override public void alias (siste String-alias) Log.e ("MyApp", "Alias ​​is" + alias); prøv PrivateKey privateKey = KeyChain.getPrivateKey (dette aliaset); X509Certificate [] certificateChain = KeyChain.getCertificateChain (dette aliaset);  å fange…  //… 

Bevæpnet med den kunnskapen, la oss nå se hvordan vi kan bruke legitimasjonslagringen for å lagre dine egne sensitive data.

KeyStore

I den forrige veiledningen så vi på å beskytte data via et passord som ble levert av brukeren. Denne typen oppsett er bra, men appkrav styrer ofte bort fra at brukerne logger på hver gang og husker et ekstra passord. 

Det er her KeyStore API kan brukes. Siden API 1 har KeyStore blitt brukt av systemet til å lagre WiFi og VPN legitimasjon. Fra 4.3 (API 18) kan du arbeide med dine egne appspesifikke asymmetriske nøkler, og i Android M (API 23) kan den lagre en AES symmetrisk nøkkel. Så mens API ikke tillater lagring av sensitive strenger direkte, kan disse nøklene lagres og brukes til å kryptere strenger. 

Fordelen med å lagre en nøkkel i KeyStore er at den tillater at nøkler blir betjent uten å avsløre det hemmelige innholdet til nøkkelen; viktige data kommer ikke inn i apprommet. Husk at tastene er beskyttet av tillatelser, slik at bare appen din kan få tilgang til dem, og de kan i tillegg være sikre maskinvare-støttet hvis enheten er i stand. Dette skaper en beholder som gjør det vanskeligere å trekke ut nøkler fra en enhet. 

Generer en ny tilfeldig nøkkel

For dette eksempelet, i stedet for å generere en AES-nøkkel fra et passord som er levert av brukeren, kan vi automatisk generere en tilfeldig nøkkel som vil bli beskyttet i KeyStore. Vi kan gjøre dette ved å opprette en keygenerator eksempel, satt til "AndroidKeyStore" forsørger.

// Generer en nøkkel og lagre den i KeyStore siste KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); endelig KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // krever låseskjermen, ugyldig hvis Lås skjermen er deaktivert //.setUserAuthenticationValidityDurationSeconds(120) // Kun tilgjengelig x sekunder fra passordautentisering. -1 krever fingeravtrykk - hver gang .setRandomizedEncryptionRequired (true) // forskjellig chiffertekst for samme tekst på hver samtale .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();

Viktige deler å se på her er .setUserAuthenticationRequired (sann) og .setUserAuthenticationValidityDurationSeconds (120) spesifikasjoner. Disse krever en låsskjerm som skal settes opp og nøkkelen skal låses til brukeren har godkjent. 

Ser på dokumentasjonen for .setUserAuthenticationValidityDurationSeconds (), du vil se at det betyr at nøkkelen bare er tilgjengelig et visst antall sekunder fra passordautentisering, og det som passerer inn -1 krever fingeravtrykksautentisering hver gang du vil ha tilgang til nøkkelen. Aktivering av kravet til godkjenning medfører også tilbakekalling av nøkkelen når brukeren fjerner eller endrer låseskjermbildet. 

Fordi lagring av en ubeskyttet nøkkel ved siden av krypterte data er som å sette en husnøkkel under dørmatten, forsøker disse alternativene å beskytte nøkkelen i ro i tilfelle en enhet er kompromittert. Et eksempel kan være en data-dump fra nettverket på enheten. Uten passordet er kjent for enheten, blir dataene ubrukelig.

De .setRandomizedEncryptionRequired (sann) alternativet muliggjør kravet om at det er nok randomisering (en ny tilfeldig IV hver gang) slik at hvis de samme dataene krypteres en gang til, vil den krypterte utgangen fortsatt være forskjellig. Dette forhindrer en angriper på å få ledetråder om krypteringsteksten basert på fôring i de samme dataene. 

Et annet alternativ å merke seg er setUserAuthenticationValidWhileOnBody (boolean remainsValid), som låser nøkkelen når enheten har oppdaget den, er ikke lenger på personen.

Krypteringsdata

Nå som nøkkelen er lagret i KeyStore, kan vi lage en metode som krypterer data ved hjelp av cipher objekt, gitt SecretKey. Det kommer tilbake a HashMap inneholder de krypterte dataene og en randomisert IV som vil være nødvendig for å dekryptere dataene. Den krypterte data, sammen med IV, kan deretter lagres i en fil eller i de delte preferansene.

privat HashMap kryptere (final byte [] decryptedBytes) siste HashMap map = ny HashMap(); prøv // Få nøkkelfinalen KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); siste KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); siste SecretKey secretKey = secretKeyEntry.getSecretKey (); // Krypter data endelig Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); siste byte [] ivBytes = cipher.getIV (); siste byte [] encryptedBytes = cipher.doFinal (dekrypterteBytes); map.put ("iv", ivBytes); map.put ("encrypted", encryptedBytes);  catch (Throwable e) e.printStackTrace ();  returkort; 

Dekryptere til en byte-array

For dekryptering brukes reversen. De cipher objektet initialiseres ved hjelp av DECRYPT_MODE konstant og en dekryptert byte [] array returneres.

privat byte [] dekryptere (siste HashMap kart) byte [] decryptedBytes = null; prøv // Få nøkkelfinalen KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); siste KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); siste SecretKey secretKey = secretKeyEntry.getSecretKey (); // Utdrag info fra kart endelig byte [] encryptedBytes = map.get ("kryptert"); siste byte [] ivBytes = map.get ("iv"); // Dekrypter datafinalen Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); siste GCMParameterSpec spec = nytt GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); decryptedBytes = cipher.doFinal (encryptedBytes);  catch (Throwable e) e.printStackTrace ();  returnere dekrypterteBytes; 

Teste eksempelet

Vi kan nå teste vårt eksempel!

@TargetApi (Build.VERSION_CODES.M) private void testEncryption () prøv // Generer en nøkkel og lagre den i KeyStore-nøkkel KeyGenerator keyGenerator = KeyGenerator.getInstance (KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); endelig KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder ( "MyKeyAlias", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes (KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings (KeyProperties.ENCRYPTION_PADDING_NONE) //.setUserAuthenticationRequired(true) // krever låseskjermen, ugyldig hvis Lås skjermen er deaktivert //.setUserAuthenticationValidityDurationSeconds(120) // Kun tilgjengelig x sekunder fra passordautentisering. -1 krever fingeravtrykk - hver gang .setRandomizedEncryptionRequired (true) // forskjellig chiffertekst for samme tekst på hver samtale .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Test siste HashMap map = krypter ("My very sensitive string!". getBytes ("UTF-8")); siste byte [] decryptedBytes = dekryptere (kart); siste streng decryptedString = ny String (dekrypterteBytes, "UTF-8"); Log.e ("MyApp", "Den dekrypterte strengen er" + decryptedString);  catch (Throwable e) e.printStackTrace (); 

Bruke RSA Asymmetric Keys for Older Devices

Dette er en god løsning for å lagre data for versjoner M og høyere, men hva om appen din støtter tidligere versjoner? Mens AES symmetriske nøkler ikke støttes under M, er RSA asymmetriske nøkler. Det betyr at vi kan bruke RSA-nøkler og kryptering for å oppnå det samme. 

Hovedforskjellen her er at et asymmetrisk tastatur inneholder to nøkler, en privat og en offentlig nøkkel, hvor den offentlige nøkkelen krypterer dataene og den private nøkkelen dekrypterer den. EN KeyPairGeneratorSpec er sendt inn i KeyPairGenerator som initialiseres med KEY_ALGORITHM_RSA og "AndroidKeyStore" forsørger.

private void testPreMEncryption () prøv // Generer et keypair og lagre det i KeyStore KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Kalender start = Calendar.getInstance (); Kalender slutten = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = ny KeyPairGeneratorSpec.Builder (this) .setAlias ​​("MyKeyAlias") .setSubject (ny X500Principal ("CN = MyKeyName, O = Android Authority")) .setSerialNumber (new BigInteger (1024, new Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // på API-nivå 18, kryptert i hvilemodus, krever låseskjerm som skal settes opp, endring av låseskjerm fjerner nøkkel .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Krypteringstest siste byte [] encryptedBytes = rsaEncrypt ("Min hemmelige streng!". GetBytes ("UTF-8")); siste byte [] decryptedBytes = rsaDecrypt (encryptedBytes); siste streng decryptedString = ny String (dekrypterteBytes, "UTF-8"); Log.e ("MyApp", "Decrypted string er" + decryptedString);  catch (Throwable e) e.printStackTrace (); 

For å kryptere, får vi RSAPublicKey fra keypair og bruk den med cipher gjenstand. 

offentlig byte [] rsaEncrypt (final byte [] decryptedBytes) byte [] encryptedBytes = null; prøv siste KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); siste KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); siste RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate (). getPublicKey (); siste Cipher cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); Endelig ByteArrayOutputStream outputStream = ny ByteArrayOutputStream (); siste CipherOutputStream cipherOutputStream = ny CipherOutputStream (outputStream, chiffer); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray ();  catch (Throwable e) e.printStackTrace ();  returnere krypterteBytes; 

Dekryptering gjøres ved bruk av RSAPrivateKey gjenstand.

offentlig byte [] rsaDecrypt (siste byte [] encryptedBytes) byte [] decryptedBytes = null; prøv siste KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); siste KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); siste RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey (); siste Cipher cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privateKey); siste CipherInputStream cipherInputStream = ny CipherInputStream (ny ByteArrayInputStream (encryptedBytes), kryptering); siste ArrayList arrayList = ny ArrayList <> (); int nextByte; mens ((nextByte = cipherInputStream.read ())! = -1) arrayList.add (byte) nextByte);  decryptedBytes = new byte [arrayList.size ()]; for (int i = 0; i < decryptedBytes.length; i++)  decryptedBytes[i] = arrayList.get(i);   catch (Throwable e)  e.printStackTrace();  return decryptedBytes; 

En ting om RSA er at kryptering er tregere enn det er i AES. Dette er vanligvis bra for små mengder informasjon, for eksempel når du sikrer felles preferansestrenger. Hvis du finner det et ytelsesproblem som krypterer store mengder data, kan du i stedet bruke dette eksemplet til å kryptere og lagre bare en AES-nøkkel. Deretter bruker du raskere AES-kryptering som ble diskutert i den forrige veiledningen for resten av dataene dine. Du kan generere en ny AES-nøkkel og konvertere den til en byte [] array som er kompatibelt med dette eksemplet.

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

For å få nøkkelen tilbake fra bytesene, gjør dette:

SecretKey-nøkkel = ny SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");

Det var mye kode! For å holde alle eksemplene enkle har jeg utelatt grundig unntakshåndtering. Men husk at for din produksjonskode, anbefales det ikke å bare fange alt Throwable saker i en fangstoppgave.

Konklusjon

Dette fullfører opplæringen om å jobbe med legitimasjon og nøkler. Mye av forvirringen rundt nøkler og lagring har å gjøre med utviklingen av Android OS, men du kan velge hvilken løsning du skal bruke gitt API-nivået din app støtter. 

Nå som vi har dekket de beste metodene for å sikre data i ro, vil neste opplæring fokusere på å sikre data i transitt.