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