Files
android_development/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationActivity.java
Sergio Giro 477c90ca1a KeyDerivationFunction: example about treating data encrypted via SHA1PRNG
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
2016-06-07 16:05:06 +01:00

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";
}