[英]How Can I Use the Android KeyStore to securely store arbitrary strings?
我希望能夠在 Android KeyStore 中安全地存儲一些敏感字符串。 我從服務器獲取字符串,但我有一個用例需要我保留它們。 KeyStore 將只允許從與分配給我的應用程序的 UID 相同的 UID 進行訪問,並且它將使用設備主密碼對數據進行加密,因此我的理解是我不必進行任何額外的加密來保護我的數據。 我的問題是,我缺少有關如何寫入數據的信息。 只要省略對 KeyStore.store(null) 的調用,我下面的代碼就可以完美運行。 那個代碼失敗了,只要我把數據放到 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);
我的前提是我可以使用 AndroidKeyStore 來保護任意數據塊,並將它們稱為“密鑰”。 然而,我越深入研究,就越清楚地發現 KeyStore API 與安全相關的對象深深地糾纏在一起:證書、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));
}
您可能已經注意到,使用 Android Keystore 處理不同的 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);
我已經重新設計了 Patrick Brennan 接受的答案。 在 Android 9 上,它產生了 NoSuchAlgorithmException。 已棄用的 KeyPairGeneratorSpec 已替換為 KeyPairGenerator。 還有一些工作需要解決有關填充的異常。
代碼注釋了所做的更改:“***”
@RequiresApi(api = Build.VERSION_CODES.M)
public static void storeExistingKey(Context context) {
final String TAG = "KEY-UTIL";
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
String alias = "key11";
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);
// *** Replaced deprecated KeyPairGeneratorSpec with KeyPairGenerator
KeyPairGenerator spec = KeyPairGenerator.getInstance(
// *** Specified algorithm here
// *** Specified: Purpose of key here
KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
spec.initialize(new KeyGenParameterSpec.Builder(
alias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) // RSA/ECB/PKCS1Padding
.setKeySize(2048)
// *** Replaced: setStartDate
.setKeyValidityStart(notBefore.getTime())
// *** Replaced: setEndDate
.setKeyValidityEnd(notAfter.getTime())
// *** Replaced: setSubject
.setCertificateSubject(new X500Principal("CN=test"))
// *** Replaced: setSerialNumber
.setCertificateSerialNumber(BigInteger.ONE)
.build());
KeyPair keyPair = spec.generateKeyPair();
Log.i(TAG, keyPair.toString());
}
int nAfter = keyStore.size();
Log.v(TAG, "Before = " + nBefore + " After = " + nAfter);
// Retrieve the keys
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
PrivateKey privateKey = privateKeyEntry.getPrivateKey();
PublicKey publicKey = 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 = context.getApplicationInfo().dataDir;
String filesDirectory = context.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);
// *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround
Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");
inCipher.init(Cipher.ENCRYPT_MODE, publicKey);
// *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround
Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");
outCipher.init(Cipher.DECRYPT_MODE, privateKey);
CipherOutputStream cipherOutputStream =
new CipherOutputStream(
new FileOutputStream(encryptedDataFilePath), inCipher);
// *** Replaced string literal with StandardCharsets.UTF_8
cipherOutputStream.write(plainText.getBytes(StandardCharsets.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++;
}
// *** Replaced string literal with StandardCharsets.UTF_8
String roundTrippedString = new String(roundTrippedBytes, 0, index, StandardCharsets.UTF_8);
Log.v(TAG, "round tripped string = " + roundTrippedString);
} catch (NoSuchAlgorithmException | UnsupportedOperationException | InvalidKeyException | NoSuchPaddingException | UnrecoverableEntryException | NoSuchProviderException | KeyStoreException | CertificateException | IOException e | InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
注意:“AndroidKeyStoreBCWorkaround”允許代碼跨不同的 API 工作。
如果有人可以評論此更新解決方案中的任何缺點,我將不勝感激。 否則,如果有更多加密知識的人有信心更新帕特里克的答案,那么我將刪除這個答案。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.