The Crypto provider providing the SHA1PRNG algorithm for random number generation was deprecated. This algorithm was sometimes incorrectly used to derive keys. This example provides a helper class and shows how to treat data that was encrypted in the incorrect way and re-encrypt it in a proper way. Bug: 27873296 Change-Id: I92d2fd4ebb07d5823de31f5a199e23b1dba4836e
289 lines
11 KiB
Java
289 lines
11 KiB
Java
/*
|
|
* Copyright (C) 2007 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.example.android.brokenkeyderivation;
|
|
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.os.Bundle;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.widget.EditText;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.GeneralSecurityException;
|
|
import java.security.SecureRandom;
|
|
import java.security.spec.KeySpec;
|
|
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.SecretKeyFactory;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.PBEKeySpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
|
/**
|
|
* Example showing how to decrypt data that was encrypted using SHA1PRNG.
|
|
*
|
|
* The Crypto provider providing the SHA1PRNG algorithm for random number
|
|
* generation is deprecated as of SDK 24.
|
|
*
|
|
* This algorithm was sometimes incorrectly used to derive keys. See
|
|
* <a href="http://android-developers.blogspot.co.uk/2013/02/using-cryptography-to-store-credentials.html">
|
|
* here</a> for details.
|
|
|
|
* This example provides a helper class ({@link InsecureSHA1PRNGKeyDerivator} and shows how to treat
|
|
* data that was encrypted in the incorrect way and re-encrypt it in a proper way,
|
|
* by using a key derivation function.
|
|
*
|
|
* The {@link #onCreate(Bundle)} method retrieves encrypted data twice and displays the results.
|
|
*
|
|
* The mock data is encrypted with an insecure key. The first time it is reencrypted properly and
|
|
* the plain text is returned together with a warning message. The second time, as the data is
|
|
* properly encrypted, the plain text is returned with a congratulations message.
|
|
*/
|
|
public class BrokenKeyDerivationActivity extends Activity {
|
|
/**
|
|
* Method used to derive an <b>insecure</b> key by emulating the SHA1PRNG algorithm from the
|
|
* deprecated Crypto provider.
|
|
*
|
|
* Do not use it to encrypt new data, just to decrypt encrypted data that would be unrecoverable
|
|
* otherwise.
|
|
*/
|
|
private static SecretKey deriveKeyInsecurely(String password, int keySizeInBytes) {
|
|
byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
|
|
return new SecretKeySpec(
|
|
InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(passwordBytes, keySizeInBytes),
|
|
"AES");
|
|
}
|
|
|
|
/**
|
|
* Example use of a key derivation function, derivating a key securely from a password.
|
|
*/
|
|
private SecretKey deriveKeySecurely(String password, int keySizeInBytes) {
|
|
// Use this to derive the key from the password:
|
|
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), retrieveSalt(),
|
|
100 /* iterationCount */, keySizeInBytes * 8 /* key size in bits */);
|
|
try {
|
|
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
|
byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
|
|
return new SecretKeySpec(keyBytes, "AES");
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Deal with exceptions properly!", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve encrypted data using a password. If data is stored with an insecure key, re-encrypt
|
|
* with a secure key.
|
|
*/
|
|
private String retrieveData(String password) {
|
|
String decryptedString;
|
|
|
|
if (isDataStoredWithInsecureKey()) {
|
|
SecretKey insecureKey = deriveKeyInsecurely(password, KEY_SIZE);
|
|
byte[] decryptedData = decryptData(retrieveEncryptedData(), retrieveIv(), insecureKey);
|
|
SecretKey secureKey = deriveKeySecurely(password, KEY_SIZE);
|
|
storeDataEncryptedWithSecureKey(encryptData(decryptedData, retrieveIv(), secureKey));
|
|
decryptedString = "Warning: data was encrypted with insecure key\n"
|
|
+ new String(decryptedData, StandardCharsets.UTF_8);
|
|
} else {
|
|
SecretKey secureKey = deriveKeySecurely(password, KEY_SIZE);
|
|
byte[] decryptedData = decryptData(retrieveEncryptedData(), retrieveIv(), secureKey);
|
|
decryptedString = "Great!: data was encrypted with secure key\n"
|
|
+ new String(decryptedData, StandardCharsets.UTF_8);
|
|
}
|
|
return decryptedString;
|
|
}
|
|
|
|
/*
|
|
***********************************************************************************************
|
|
* The essential point of this example are the three methods above. Everything below this
|
|
* comment just gives a concrete example of usage and defines mock methods.
|
|
***********************************************************************************************
|
|
*/
|
|
|
|
/**
|
|
* Retrieves encrypted data twice and displays the results.
|
|
*
|
|
* The mock data is encrypted with an insecure key (see {@link #cleanRoomStart()}) and so the
|
|
* first time {@link #retrieveData(String)} reencrypts it and returns the plain text with a
|
|
* warning message. The second time, as the data is properly encrypted, the plain text is
|
|
* returned with a congratulations message.
|
|
*/
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
|
|
// Remove any files from previous executions of this app and initialize mock encrypted data.
|
|
// Just so that the application has the same behaviour every time is run. You don't need to
|
|
// do this in your app.
|
|
cleanRoomStart();
|
|
|
|
// Set the layout for this activity. You can find it
|
|
// in res/layout/brokenkeyderivation_activity.xml
|
|
View view = getLayoutInflater().inflate(R.layout.brokenkeyderivation_activity, null);
|
|
setContentView(view);
|
|
|
|
// Find the text editor view inside the layout.
|
|
EditText mEditor = (EditText) findViewById(R.id.text);
|
|
|
|
String password = "unguessable";
|
|
String firstResult = retrieveData(password);
|
|
String secondResult = retrieveData(password);
|
|
|
|
mEditor.setText("First result: " + firstResult + "\nSecond result: " + secondResult);
|
|
|
|
}
|
|
|
|
private static byte[] encryptOrDecrypt(
|
|
byte[] data, SecretKey key, byte[] iv, boolean isEncrypt) {
|
|
try {
|
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING");
|
|
cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key,
|
|
new IvParameterSpec(iv));
|
|
return cipher.doFinal(data);
|
|
} catch (GeneralSecurityException e) {
|
|
throw new RuntimeException("This is unconceivable!", e);
|
|
}
|
|
}
|
|
|
|
private static byte[] encryptData(byte[] data, byte[] iv, SecretKey key) {
|
|
return encryptOrDecrypt(data, key, iv, true);
|
|
}
|
|
|
|
private static byte[] decryptData(byte[] data, byte[] iv, SecretKey key) {
|
|
return encryptOrDecrypt(data, key, iv, false);
|
|
}
|
|
|
|
/**
|
|
* Remove any files from previous executions of this app and initialize mock encrypted data.
|
|
*
|
|
* <p>Just so that the application has the same behaviour every time is run. You don't need to
|
|
* do this in your app.
|
|
*/
|
|
private void cleanRoomStart() {
|
|
removeFile("salt");
|
|
removeFile("iv");
|
|
removeFile(SECURE_ENCRYPTION_INDICATOR_FILE_NAME);
|
|
// Mock initial data
|
|
encryptedData = encryptData(
|
|
"I hope it helped!".getBytes(), retrieveIv(),
|
|
deriveKeyInsecurely("unguessable", KEY_SIZE));
|
|
}
|
|
|
|
/*
|
|
***********************************************************************************************
|
|
* Everything below this comment is a succession of mocks that would rarely interest someone on
|
|
* Earth. They are merely intended to make the example self contained.
|
|
***********************************************************************************************
|
|
*/
|
|
|
|
private boolean isDataStoredWithInsecureKey() {
|
|
// Your app should have a way to tell whether the data has been re-encrypted in a secure
|
|
// fashion, in this mock we use the existence of a file with a certain name to indicate
|
|
// that.
|
|
return !fileExists("encrypted_with_secure_key");
|
|
}
|
|
|
|
private byte[] retrieveIv() {
|
|
byte[] iv = new byte[IV_SIZE];
|
|
// Ideally your data should have been encrypted with a random iv. This creates a random iv
|
|
// if not present, in order to encrypt our mock data.
|
|
readFromFileOrCreateRandom("iv", iv);
|
|
return iv;
|
|
}
|
|
|
|
private byte[] retrieveSalt() {
|
|
// Salt must be at least the same size as the key.
|
|
byte[] salt = new byte[KEY_SIZE];
|
|
// Create a random salt if encrypting for the first time, and save it for future use.
|
|
readFromFileOrCreateRandom("salt", salt);
|
|
return salt;
|
|
}
|
|
|
|
private byte[] encryptedData = null;
|
|
|
|
private byte[] retrieveEncryptedData() {
|
|
return encryptedData;
|
|
}
|
|
|
|
private void storeDataEncryptedWithSecureKey(byte[] encryptedData) {
|
|
// Mock implementation.
|
|
this.encryptedData = encryptedData;
|
|
writeToFile(SECURE_ENCRYPTION_INDICATOR_FILE_NAME, new byte[1]);
|
|
}
|
|
|
|
/**
|
|
* Read from file or return random bytes in the given array.
|
|
*
|
|
* <p>Save to file if file didn't exist.
|
|
*/
|
|
private void readFromFileOrCreateRandom(String fileName, byte[] bytes) {
|
|
if (fileExists(fileName)) {
|
|
readBytesFromFile(fileName, bytes);
|
|
return;
|
|
}
|
|
SecureRandom sr = new SecureRandom();
|
|
sr.nextBytes(bytes);
|
|
writeToFile(fileName, bytes);
|
|
}
|
|
|
|
private boolean fileExists(String fileName) {
|
|
File file = new File(getFilesDir(), fileName);
|
|
return file.exists();
|
|
}
|
|
|
|
private void removeFile(String fileName) {
|
|
File file = new File(getFilesDir(), fileName);
|
|
file.delete();
|
|
}
|
|
|
|
private void writeToFile(String fileName, byte[] bytes) {
|
|
try (FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE)) {
|
|
fos.write(bytes);
|
|
} catch (IOException e) {
|
|
throw new RuntimeException("Couldn't write to " + fileName, e);
|
|
}
|
|
}
|
|
|
|
private void readBytesFromFile(String fileName, byte[] bytes) {
|
|
try (FileInputStream fis = openFileInput(fileName)) {
|
|
int numBytes = 0;
|
|
while (numBytes < bytes.length) {
|
|
int n = fis.read(bytes, numBytes, bytes.length - numBytes);
|
|
if (n <= 0) {
|
|
throw new RuntimeException("Couldn't read from " + fileName);
|
|
}
|
|
numBytes += n;
|
|
}
|
|
} catch (IOException e) {
|
|
throw new RuntimeException("Couldn't read from " + fileName, e);
|
|
}
|
|
}
|
|
|
|
private static final int IV_SIZE = 16;
|
|
private static final int KEY_SIZE = 32;
|
|
private static final String SECURE_ENCRYPTION_INDICATOR_FILE_NAME =
|
|
"encrypted_with_secure_key";
|
|
}
|
|
|