I det föregående inlägget på Android-datasäkerhet, tittade vi på krypteringsdata via ett användarnamnskoder. Denna handledning kommer att flytta fokusen till referens och nyckellagring. Jag börjar med att införa kontouppgifter och sluta med ett exempel på att skydda data med hjälp av KeyStore.
Ofta, när du arbetar med en tredjeparts tjänst, kommer det att behövas någon form av autentisering. Detta kan vara så enkelt som a /logga in
slutpunkt som accepterar ett användarnamn och lösenord.
Det verkar för det första att en enkel lösning är att bygga ett användargränssnitt som ber användaren att logga in och sedan fånga och lagra sina inloggningsuppgifter. Det här är emellertid inte den bästa praxisen eftersom vår app inte behöver känna till uppgifterna för ett tredje partskonto. Istället kan vi använda Account Manager, som delegerar hanteringen av den känsliga informationen för oss.
Kontochefen är en centraliserad hjälper för användarkontouppgifter, så att din app inte behöver hantera lösenord direkt. Det ger ofta ett tecken i stället för det riktiga användarnamnet och lösenordet som kan användas för att göra verifierade förfrågningar till en tjänst. Ett exempel är när du begär en OAuth2-token.
Ibland lagras all nödvändig information redan på enheten, och andra gånger måste kontohanteraren ringa en server för en uppdaterad token. Du kanske har sett konton avsnittet i enhetens inställningar för olika appar. Vi kan få listan över tillgängliga konton så här:
AccountManager accountManager = AccountManager.get (detta); Konto [] accounts = accountManager.getAccounts ();
Koden kommer att kräva android.permission.GET_ACCOUNTS
lov. Om du letar efter ett visst konto kan du hitta det så här:
AccountManager accountManager = AccountManager.get (detta); Konto [] accounts = accountManager.getAccountsByType ("com.google");
När du har kontot kan en token för kontot hämtas genom att ringa getAuthToken (Konto, String, Paket, Aktivitet, AccountManagerCallback, Handler)
metod. Token kan sedan användas för att göra autentiserade API-förfrågningar till en tjänst. Det här kan vara ett RESTful API där du skickar in en token-parameter under en HTTPS-förfrågan utan att någonsin behöva veta användarens privata kontouppgifter.
Eftersom varje tjänst kommer att ha ett annat sätt att verifiera och lagra privata referenser, tillhandahåller kontohanteraren autentiseringsmoduler för en tredjeparts tjänst som ska genomföras. Medan Android har implementeringar för många populära tjänster, betyder det att du kan skriva din egen autentiserare för att hantera din apps autentisering och behörighetslagring. Detta gör att du kan kontrollera att uppgifterna är krypterade. Tänk på att det också betyder att referenser i kontohanteraren som används av andra tjänster kan lagras i tydlig text, vilket gör dem synliga för alla som har rotat sin enhet.
I stället för enkla referenser finns det tillfällen då du måste hantera en nyckel eller ett certifikat för en individ eller en enhet, till exempel när en tredje part skickar dig en certifikatfil som du behöver behålla. Det vanligaste scenariot är när en app måste autentisera till en privat organisations server.
I nästa handledning ser vi på att använda certifikat för autentisering och säker kommunikation, men jag vill ändå ta upp hur man lagrar dessa objekt under tiden. Nyckelring API var ursprungligen byggt för den mycket specifika användarinstallationen av en privat nyckel eller ett certifikatpar från en PKCS # 12-fil.
Introducerad i Android 4.0 (API-nivå 14) behandlar nyckelring API med nyckelhantering. Specifikt fungerar det med PrivateKey
och X509Certificate
objekt och ger en säkrare behållare än att använda appens datalagring. Det beror på att behörigheter för privata nycklar bara tillåter att din egen app får åtkomst till nycklarna och endast efter användarbehörighet. Det betyder att en låsskärm måste ställas in på enheten innan du kan använda referenslagringsenheten. Objekten i nyckelring kan också vara bundna till att säkra hårdvara, om det är tillgängligt.
Koden för att installera ett certifikat är följande:
Avsikten med avsikt = KeyChain.createInstallIntent (); byte [] p12Bytes = // ... läs från filen, till exempel example.pfx eller example.p12 ... intent.putExtra (KeyChain.EXTRA_PKCS12, p12Bytes); startActivity (intent);
Användaren blir uppmanad till ett lösenord för att komma åt den privata nyckeln och ett alternativ för att namnge certifikatet. För att hämta nyckeln presenterar följande kod ett användargränssnitt som låter användaren välja från listan med installerade nycklar.
KeyChain.choosePrivateKeyAlias (detta, den här nya strängen [] "RSA", null, null, -1, null);
När valet är gjort returneras ett strängaliasnamn i alias (sista sträng alias)
återuppringning där du kan komma åt privatnyckeln eller certifikatkedjan direkt.
public class KeychainTest utökar Aktivitetsverktyg ..., KeyChainAliasCallback // ... @Override public void alias (Final String alias) Log.e ("MyApp", "Alias is" + alias); prova PrivateKey privateKey = KeyChain.getPrivateKey (detta, alias); X509Certificate [] certificateChain = KeyChain.getCertificateChain (detta, alias); fånga ... // ...
Beväpnad med den kunskapen, låt oss nu se hur vi kan använda credential storage för att spara din egen känsliga data.
I den tidigare handledningen såg vi på att skydda data via ett användarnummer. Denna typ av inställning är bra, men appkrav stämmer ofta bort från att användarna loggar in varje gång och kommer ihåg ett extra lösenord.
Det är här KeyStore API kan användas. Sedan API 1 har KeyStore använts av systemet för att lagra WiFi och VPN-uppgifter. Från och med 4.3 (API 18) kan du arbeta med egna appspecifika asymmetriska nycklar, och i Android M (API 23) kan den lagra en AES-symmetrisk nyckel. Så medan API: n inte tillåter lagring av känsliga strängar direkt, kan dessa nycklar lagras och sedan användas för att kryptera strängar.
Fördelen att lagra en nyckel i KeyStore är att den låter tangenterna användas utan att avslöja den nyckelens hemliga innehåll. nyckeldata går inte in i apputrymmet. Kom ihåg att nycklarna är skyddade av behörigheter så att endast din app kan komma åt dem, och de kan dessutom vara säkrade med maskinvara om enheten är kapabel. Detta skapar en behållare som gör det svårare att extrahera nycklar från en enhet.
För det här exemplet kan vi, istället för att generera en AES-nyckel från en användarkoderad lösenord, automatiskt generera en slumpmässig nyckel som kommer att skyddas i KeyStore. Vi kan göra detta genom att skapa en KeyGenerator
instans, inställd på "AndroidKeyStore"
leverantör.
// Skapa en nyckel och lagra den i KeyStore sista 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) // kräver låsskärmen ogiltig om Låsskärmen är inaktiverad //.setUserAuthenticationValidityDurationSeconds(120) // Endast tillgängligt x sekunder från lösenordsautentisering. -1 kräver fingeravtryck - varje gång .setRandomizedEncryptionRequired (true) // olika chiffertext för samma klartext på varje samtal .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey ();
Viktiga delar att titta på här är .setUserAuthenticationRequired (true)
och .setUserAuthenticationValidityDurationSeconds (120)
specifikationer. Dessa kräver att en låsskärm ska ställas in och nyckeln ska låsas tills användaren har autentiserat.
Titta på dokumentationen för .setUserAuthenticationValidityDurationSeconds ()
, Du kommer att se att det betyder att nyckeln bara är tillgänglig ett visst antal sekunder från lösenordsautentisering och det som passerar in -1
kräver fingeravtrycksautentisering varje gång du vill komma åt nyckeln. Aktivering av kravet på autentisering har också till syfte att återkalla nyckeln när användaren tar bort eller ändrar låsskärmen.
Eftersom lagring av en oskyddad nyckel bredvid krypterad data är som att sätta en husnyckel under dörrmatningen, försöker dessa alternativ att skydda nyckeln i vila om en enhet är äventyrad. Ett exempel kan vara en dumpning offline av enheten. Utan att lösenordet är känt för enheten görs den data värdelös.
De .setRandomizedEncryptionRequired (true)
alternativet möjliggör kravet på att det finns tillräckligt med randomisering (en ny slumpmässig IV varje gång) så att om samma data krypteras en andra gång, kommer den krypterade utgåvan fortfarande att vara annorlunda. Detta förhindrar att en angripare får ledtrådar om chiffertexten baserat på utfodring i samma data.
Ett annat alternativ att notera är setUserAuthenticationValidWhileOnBody (Boolean remainsValid)
, som låser nyckeln när enheten har upptäckt den är inte längre på personen.
Nu när nyckeln är lagrad i KeyStore kan vi skapa en metod som krypterar data med hjälp av Chiffer
objekt, med tanke på Hemlig nyckel
. Det kommer att returnera a HashMap
innehållande krypterad data och en randomiserad IV som kommer att behövas för att dekryptera data. Den krypterade data, tillsammans med IV, kan sedan sparas i en fil eller i de delade inställningarna.
privat HashMapkryptera (sista byte [] decryptedBytes) final HashMap map = ny HashMap (); försök // Hämta nyckelfinalen KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); sista SecretKey secretKey = secretKeyEntry.getSecretKey (); // Kryptera data slutlig Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); cipher.init (Cipher.ENCRYPT_MODE, secretKey); sista byte [] ivBytes = cipher.getIV (); sista byte [] encryptedBytes = cipher.doFinal (decryptedBytes); map.put ("iv", ivBytes); map.put ("krypterade", krypterade bitar); fånga (Throwable e) e.printStackTrace (); returkarta;
För dekryptering tillämpas omvänden. De Chiffer
objektet initieras med hjälp av DECRYPT_MODE
konstant och en dekrypterad byte []
array returneras.
privat byte [] dekryptera (sista HashMapkarta) byte [] decryptedBytes = null; försök // Hämta nyckelfinalen KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry ("MyKeyAlias", null); sista SecretKey secretKey = secretKeyEntry.getSecretKey (); // Extrahera info från kortens slutliga byte [] encryptedBytes = map.get ("encrypted"); sista byte [] ivBytes = map.get ("iv"); // Dekryptera data slutlig Cipher cipher = Cipher.getInstance ("AES / GCM / NoPadding"); sista GCMParameterSpec spec = ny GCMParameterSpec (128, ivBytes); cipher.init (Cipher.DECRYPT_MODE, secretKey, spec); dekrypteradeBytes = cipher.doFinal (krypteradeBytes); fånga (Throwable e) e.printStackTrace (); returnera dekrypterade bitar;
Vi kan nu testa vårt exempel!
@TargetApi (Build.VERSION_CODES.M) private void testEncryption () försök // Skapa en nyckel och lagra den i KeyStore sista 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) // kräver låsskärmen ogiltig om Låsskärmen är inaktiverad //.setUserAuthenticationValidityDurationSeconds(120) // Endast tillgängligt x sekunder från lösenordsautentisering. -1 kräver fingeravtryck - varje gång .setRandomizedEncryptionRequired (true) // olika chiffertext för samma klartext på varje samtal .build (); keyGenerator.init (keyGenParameterSpec); keyGenerator.generateKey (); // Test sista HashMapmap = kryptera ("My very sensitive string!". getBytes ("UTF-8")); sista byte [] decryptedBytes = dekryptera (karta); sista sträng decryptedString = ny sträng (dekrypterade bitar, "UTF-8"); Log.e ("MyApp", "Den dekrypterade strängen är" + decryptedString); fånga (Throwable e) e.printStackTrace ();
Det här är en bra lösning för att lagra data för versioner M och högre, men vad händer om din app stöder tidigare versioner? Medan AES symmetriska nycklar inte stöds under M är RSA asymmetriska nycklar. Det betyder att vi kan använda RSA-nycklar och kryptering för att uppnå samma sak.
Huvudskillnaden här är att en asymmetrisk tangentbord innehåller två tangenter, en privat och en allmän nyckel, där den offentliga nyckeln krypterar data och den privata nyckeln dekrypterar den. en KeyPairGeneratorSpec
passeras in i KeyPairGenerator
som initialiseras med KEY_ALGORITHM_RSA
och den "AndroidKeyStore"
leverantör.
private void testPreMEncryption () försök // Skapa en keypair och lagra den i KeyStore KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Kalender start = Calendar.getInstance (); Kalenderänden = Calendar.getInstance (); end.add (Calendar.YEAR, 10); KeyPairGeneratorSpec spec = ny KeyPairGeneratorSpec.Builder (this) .setAlias ("MyKeyAlias") .setSubject (nytt X500Principal ("CN = MyKeyName, O = Android Authority")) .setSerialNumber (new BigInteger (1024, new Random ())). setStartDate (start.getTime ()) .setEndDate (end.getTime ()) .setEncryptionRequired () // på API-nivån 18, krypterad i vila, kräver att låsskärmen ska ställas in, att byta låsskärm tar bort nyckel .build (); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance (KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); keyPairGenerator.initialize (spec); keyPairGenerator.generateKeyPair (); // Krypteringstest sista byte [] encryptedBytes = rsaEncrypt ("Min hemliga sträng!". GetBytes ("UTF-8")); sista byte [] decryptedBytes = rsaDecrypt (encryptedBytes); sista sträng decryptedString = ny sträng (dekrypterade bitar, "UTF-8"); Log.e ("MyApp", "Decrypted string is" + decryptedString); fånga (Throwable e) e.printStackTrace ();
För att kryptera får vi RSAPublicKey
från keypair och använd den med Chiffer
objekt.
public byte [] rsaEncrypt (final byte [] decryptedBytes) byte [] encryptedBytes = null; prova final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); sista RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate (). getPublicKey (); sista Cipher cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.ENCRYPT_MODE, publicKey); slutlig ByteArrayOutputStream outputStream = ny ByteArrayOutputStream (); sista CipherOutputStream cipherOutputStream = nya CipherOutputStream (outputStream, chiffer); cipherOutputStream.write (decryptedBytes); cipherOutputStream.close (); encryptedBytes = outputStream.toByteArray (); fånga (Throwable e) e.printStackTrace (); returnera krypterade bitar;
Dekryptering görs med hjälp av RSAPrivateKey
objekt.
public byte [] rsaDecrypt (sista byte [] encryptedBytes) byte [] decryptedBytes = null; prova final KeyStore keyStore = KeyStore.getInstance ("AndroidKeyStore"); keyStore.load (null); Final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry ("MyKeyAlias", null); sista RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey (); sista Cipher cipher = Cipher.getInstance ("RSA / ECB / PKCS1Padding", "AndroidOpenSSL"); cipher.init (Cipher.DECRYPT_MODE, privatKey); sista CipherInputStream cipherInputStream = nya CipherInputStream (nya ByteArrayInputStream (encryptedBytes), chiffer); sista ArrayListarrayList = ny ArrayList <> (); int nextByte; medan ((nextByte = cipherInputStream.read ())! = -1) arrayList.add ((byte) nextByte); decryptedBytes = new byte [arrayList.size ()]; för (int i = 0; i < decryptedBytes.length; i++) decryptedBytes[i] = arrayList.get(i); catch (Throwable e) e.printStackTrace(); return decryptedBytes;
En sak om RSA är att kryptering är långsammare än i AES. Det här är vanligtvis bra för små mängder information, till exempel när du säkrar delade preferenssträngar. Om du upptäcker att ett prestandaproblem krypterar stora mängder data, kan du istället använda det här exemplet för att kryptera och lagra bara en AES-nyckel. Använd sedan den snabbare AES-kryptering som diskuterades i den tidigare handledningen för resten av dina data. Du kan skapa en ny AES-nyckel och konvertera den till en byte []
array som är kompatibel med detta exempel.
KeyGenerator keyGenerator = KeyGenerator.getInstance ("AES"); keyGenerator.init (256); // AES-256 SecretKey secretKey = keyGenerator.generateKey (); byte [] keyBytes = secretKey.getEncoded ();
För att få nyckeln tillbaka från byte, gör så här:
SecretKey-nyckel = ny SecretKeySpec (keyBytes, 0, keyBytes.length, "AES");
Det var mycket kod! För att hålla alla exemplen enkla har jag utelämnat grundlig undantagshantering. Men kom ihåg att för din produktionskod rekommenderas det inte att helt enkelt fånga allt Throwable
fall i ett fångst uttalande.
Detta avslutar handledningen om att arbeta med inloggningsuppgifter och nycklar. Mycket av förvirringen kring nycklar och lagring har att göra med utvecklingen av Android OS, men du kan välja vilken lösning som ska användas med den API-nivå som din app stöder.
Nu när vi har täckt de bästa metoderna för att säkra data i vila, kommer nästa handledning att fokusera på att säkra data i transit.