2014-12-05 108 views
29

我希望能夠安全地在Android KeyStore中存儲一些敏感的字符串。我從服務器獲取字符串,但我有一個用例需要我堅持下去。 KeyStore將只允許使用與分配給我的應用程序相同的UID進行訪問,並且它將使用設備主密碼對數據進行加密,所以我的理解是,我不必進行任何額外的加密來保護我的數據。我的麻煩是,我錯過了關於如何寫入數據的內容。下面的代碼完美地工作,只要對KeyStore.store(null)的調用被省略。該代碼失敗,並且只要我在將數據放入KeyStore後無法存儲數據,那麼我無法保存它。如何使用Android KeyStore安全地存儲任意字符串?

我想我錯過了一些關於KeyStore API的知識,但我不知道是什麼。任何幫助感謝!

String metaKey = "ourSecretKey"; 
String encodedKey = "this is supposed to be a secret"; 
byte[] encodedKeyBytes = new byte[(int)encodedKey.length()]; 
encodedKeyBytes = encodedKey.getBytes("UTF-8"); 
KeyStoreParameter ksp = null; 

//String algorithm = "DES"; 
String algorithm = "DESede"; 
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm); 
SecretKeySpec secretKeySpec = new SecretKeySpec(encodedKeyBytes, algorithm); 
SecretKey secretKey = secretKeyFactory.generateSecret(secretKeySpec); 

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 

keyStore.load(null); 

KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey); 
keyStore.setEntry(metaKey, secretKeyEntry, ksp); 

keyStore.store(null); 

String recoveredSecret = ""; 
if (keyStore.containsAlias(metaKey)) { 
    KeyStore.SecretKeyEntry recoveredEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry(metaKey, ksp); 
    byte[] bytes = recoveredEntry.getSecretKey().getEncoded(); 
    for (byte b : bytes) { 
     recoveredSecret += (char)b; 
    } 
} 
Log.v(TAG, "recovered " + recoveredSecret); 

回答

48

我開始的前提是我可以使用AndroidKeyStore來保護任意數據塊,並將它們稱爲「鍵」。然而,深入研究這個問題後,KeyStore API與安全相關的對象:Certificates,KeySpecs,Providers等深深地糾纏在一起。它不是爲存儲任意數據而設計的,我沒有看到一個簡單的將其彎曲到此目的的路徑。

但是,AndroidKeyStore可以用來幫助我保護我的敏感數據。我可以使用它來管理加密密鑰,我將用它來加密本地應用程序的數據。通過使用AndroidKeyStore,CipherOutputStream和CipherInputStream的組合,我們可以:

  • 生成,安全存儲,並在設備上獲取加密密鑰
  • 加密任意數據,並(將其保存在設備上的應用程序的目錄,在那裏它將受到文件系統權限的進一步保護)
  • 訪問和解密數據以備後用。

下面是一些示例代碼,演示瞭如何實現這一點。

try { 
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 
    keyStore.load(null); 

    String alias = "key3"; 

    int nBefore = keyStore.size(); 

    // Create the keys if necessary 
    if (!keyStore.containsAlias(alias)) { 

     Calendar notBefore = Calendar.getInstance(); 
     Calendar notAfter = Calendar.getInstance(); 
     notAfter.add(Calendar.YEAR, 1); 
     KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this) 
      .setAlias(alias) 
      .setKeyType("RSA") 
      .setKeySize(2048) 
      .setSubject(new X500Principal("CN=test")) 
      .setSerialNumber(BigInteger.ONE) 
      .setStartDate(notBefore.getTime()) 
      .setEndDate(notAfter.getTime()) 
      .build(); 
     KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); 
     generator.initialize(spec); 

     KeyPair keyPair = generator.generateKeyPair(); 
    } 
    int nAfter = keyStore.size(); 
    Log.v(TAG, "Before = " + nBefore + " After = " + nAfter); 

    // Retrieve the keys 
    KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null); 
    RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); 
    RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey(); 

    Log.v(TAG, "private key = " + privateKey.toString()); 
    Log.v(TAG, "public key = " + publicKey.toString()); 

    // Encrypt the text 
    String plainText = "This text is supposed to be a secret!"; 
    String dataDirectory = getApplicationInfo().dataDir; 
    String filesDirectory = getFilesDir().getAbsolutePath(); 
    String encryptedDataFilePath = filesDirectory + File.separator + "keep_yer_secrets_here"; 

    Log.v(TAG, "plainText = " + plainText); 
    Log.v(TAG, "dataDirectory = " + dataDirectory); 
    Log.v(TAG, "filesDirectory = " + filesDirectory); 
    Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath); 

    Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); 
    inCipher.init(Cipher.ENCRYPT_MODE, publicKey); 

    Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); 
    outCipher.init(Cipher.DECRYPT_MODE, privateKey); 

    CipherOutputStream cipherOutputStream = 
     new CipherOutputStream(
      new FileOutputStream(encryptedDataFilePath), inCipher); 
    cipherOutputStream.write(plainText.getBytes("UTF-8")); 
    cipherOutputStream.close(); 

    CipherInputStream cipherInputStream = 
     new CipherInputStream(new FileInputStream(encryptedDataFilePath), 
      outCipher); 
    byte [] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data 

    int index = 0; 
    int nextByte; 
    while ((nextByte = cipherInputStream.read()) != -1) { 
     roundTrippedBytes[index] = (byte)nextByte; 
     index++; 
    } 
    String roundTrippedString = new String(roundTrippedBytes, 0, index, "UTF-8"); 
    Log.v(TAG, "round tripped string = " + roundTrippedString); 

} catch (NoSuchAlgorithmException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (NoSuchProviderException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (InvalidAlgorithmParameterException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (KeyStoreException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (CertificateException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (IOException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (UnrecoverableEntryException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (NoSuchPaddingException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (InvalidKeyException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (BadPaddingException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (IllegalBlockSizeException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} catch (UnsupportedOperationException e) { 
    Log.e(TAG, Log.getStackTraceString(e)); 
} 
+2

setKeyType需要API 19,而KeyPairGeneratorSpec是API 18如何支持API 18? – 2015-10-18 07:09:49

+7

偉大的職位。幫了我很多。我必須在'Cipher.getInstance(String transformation,Provider provider)'調用中移除提供者才能使其工作,即調用'Cipher.getInstance(String transformation)'。否則,您將得到一個'java.security.InvalidKeyException',並帶有'需要RSA私鑰或公鑰'消息。 – 2016-01-28 18:11:29

+0

只是覺得我會把這個文檔記錄下來,即使我在官方文檔的任何地方都找不到它。 **注意關鍵別名不能太長。**我沒有測試過長度,但是我的頭撞在牆上翻轉所有的開關,直到我發現我的密鑰別名字符串太大... – 2017-01-14 04:52:39

3

您可能已經注意到使用Android密鑰庫處理不同API級別時出現問題。

Scytale是一個開源的庫,它爲Android密鑰庫提供了一個方便的包裝,因此您不需要寫入鍋爐板,並且可以直接進入加密/解密。

示例代碼:

// Create and save key 
Store store = new Store(getApplicationContext()); 
if (!store.hasKey("test")) { 
    SecretKey key = store.generateSymmetricKey("test", null); 
} 
... 

// Get key 
SecretKey key = store.getSymmetricKey("test", null); 

// Encrypt/Decrypt data 
Crypto crypto = new Crypto(Options.TRANSFORMATION_SYMMETRIC); 
String text = "Sample text"; 

String encryptedData = crypto.encrypt(text, key); 
Log.i("Scytale", "Encrypted data: " + encryptedData); 

String decryptedData = crypto.decrypt(encryptedData, key); 
Log.i("Scytale", "Decrypted data: " + decryptedData); 
相關問題