1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.brokenkeyderivation; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.os.Bundle; 22 import android.view.View; 23 import android.view.WindowManager; 24 import android.widget.EditText; 25 26 import java.io.File; 27 import java.io.FileInputStream; 28 import java.io.FileOutputStream; 29 import java.io.IOException; 30 import java.nio.charset.StandardCharsets; 31 import java.security.GeneralSecurityException; 32 import java.security.SecureRandom; 33 import java.security.spec.KeySpec; 34 35 import javax.crypto.Cipher; 36 import javax.crypto.SecretKey; 37 import javax.crypto.SecretKeyFactory; 38 import javax.crypto.spec.IvParameterSpec; 39 import javax.crypto.spec.PBEKeySpec; 40 import javax.crypto.spec.SecretKeySpec; 41 42 43 /** 44 * Example showing how to decrypt data that was encrypted using SHA1PRNG. 45 * 46 * The Crypto provider providing the SHA1PRNG algorithm for random number 47 * generation is deprecated as of SDK 24. 48 * 49 * This algorithm was sometimes incorrectly used to derive keys. See 50 * <a href="http://android-developers.blogspot.co.uk/2013/02/using-cryptography-to-store-credentials.html"> 51 * here</a> for details. 52 53 * This example provides a helper class ({@link InsecureSHA1PRNGKeyDerivator} and shows how to treat 54 * data that was encrypted in the incorrect way and re-encrypt it in a proper way, 55 * by using a key derivation function. 56 * 57 * The {@link #onCreate(Bundle)} method retrieves encrypted data twice and displays the results. 58 * 59 * The mock data is encrypted with an insecure key. The first time it is reencrypted properly and 60 * the plain text is returned together with a warning message. The second time, as the data is 61 * properly encrypted, the plain text is returned with a congratulations message. 62 */ 63 public class BrokenKeyDerivationActivity extends Activity { 64 /** 65 * Method used to derive an <b>insecure</b> key by emulating the SHA1PRNG algorithm from the 66 * deprecated Crypto provider. 67 * 68 * Do not use it to encrypt new data, just to decrypt encrypted data that would be unrecoverable 69 * otherwise. 70 */ 71 private static SecretKey deriveKeyInsecurely(String password, int keySizeInBytes) { 72 byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 73 return new SecretKeySpec( 74 InsecureSHA1PRNGKeyDerivator.deriveInsecureKey(passwordBytes, keySizeInBytes), 75 "AES"); 76 } 77 78 /** 79 * Example use of a key derivation function, derivating a key securely from a password. 80 */ 81 private SecretKey deriveKeySecurely(String password, int keySizeInBytes) { 82 // Use this to derive the key from the password: 83 KeySpec keySpec = new PBEKeySpec(password.toCharArray(), retrieveSalt(), 84 100 /* iterationCount */, keySizeInBytes * 8 /* key size in bits */); 85 try { 86 SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); 87 byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded(); 88 return new SecretKeySpec(keyBytes, "AES"); 89 } catch (Exception e) { 90 throw new RuntimeException("Deal with exceptions properly!", e); 91 } 92 } 93 94 /** 95 * Retrieve encrypted data using a password. If data is stored with an insecure key, re-encrypt 96 * with a secure key. 97 */ 98 private String retrieveData(String password) { 99 String decryptedString; 100 101 if (isDataStoredWithInsecureKey()) { 102 SecretKey insecureKey = deriveKeyInsecurely(password, KEY_SIZE); 103 byte[] decryptedData = decryptData(retrieveEncryptedData(), retrieveIv(), insecureKey); 104 SecretKey secureKey = deriveKeySecurely(password, KEY_SIZE); 105 storeDataEncryptedWithSecureKey(encryptData(decryptedData, retrieveIv(), secureKey)); 106 decryptedString = "Warning: data was encrypted with insecure key\n" 107 + new String(decryptedData, StandardCharsets.UTF_8); 108 } else { 109 SecretKey secureKey = deriveKeySecurely(password, KEY_SIZE); 110 byte[] decryptedData = decryptData(retrieveEncryptedData(), retrieveIv(), secureKey); 111 decryptedString = "Great!: data was encrypted with secure key\n" 112 + new String(decryptedData, StandardCharsets.UTF_8); 113 } 114 return decryptedString; 115 } 116 117 /* 118 *********************************************************************************************** 119 * The essential point of this example are the three methods above. Everything below this 120 * comment just gives a concrete example of usage and defines mock methods. 121 *********************************************************************************************** 122 */ 123 124 /** 125 * Retrieves encrypted data twice and displays the results. 126 * 127 * The mock data is encrypted with an insecure key (see {@link #cleanRoomStart()}) and so the 128 * first time {@link #retrieveData(String)} reencrypts it and returns the plain text with a 129 * warning message. The second time, as the data is properly encrypted, the plain text is 130 * returned with a congratulations message. 131 */ 132 @Override 133 public void onCreate(Bundle savedInstanceState) { 134 super.onCreate(savedInstanceState); 135 136 // Remove any files from previous executions of this app and initialize mock encrypted data. 137 // Just so that the application has the same behaviour every time is run. You don't need to 138 // do this in your app. 139 cleanRoomStart(); 140 141 // Set the layout for this activity. You can find it 142 // in res/layout/brokenkeyderivation_activity.xml 143 View view = getLayoutInflater().inflate(R.layout.brokenkeyderivation_activity, null); 144 setContentView(view); 145 146 // Find the text editor view inside the layout. 147 EditText mEditor = (EditText) findViewById(R.id.text); 148 149 String password = "unguessable"; 150 String firstResult = retrieveData(password); 151 String secondResult = retrieveData(password); 152 153 mEditor.setText("First result: " + firstResult + "\nSecond result: " + secondResult); 154 155 } 156 157 private static byte[] encryptOrDecrypt( 158 byte[] data, SecretKey key, byte[] iv, boolean isEncrypt) { 159 try { 160 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING"); 161 cipher.init(isEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, 162 new IvParameterSpec(iv)); 163 return cipher.doFinal(data); 164 } catch (GeneralSecurityException e) { 165 throw new RuntimeException("This is unconceivable!", e); 166 } 167 } 168 169 private static byte[] encryptData(byte[] data, byte[] iv, SecretKey key) { 170 return encryptOrDecrypt(data, key, iv, true); 171 } 172 173 private static byte[] decryptData(byte[] data, byte[] iv, SecretKey key) { 174 return encryptOrDecrypt(data, key, iv, false); 175 } 176 177 /** 178 * Remove any files from previous executions of this app and initialize mock encrypted data. 179 * 180 * <p>Just so that the application has the same behaviour every time is run. You don't need to 181 * do this in your app. 182 */ 183 private void cleanRoomStart() { 184 removeFile("salt"); 185 removeFile("iv"); 186 removeFile(SECURE_ENCRYPTION_INDICATOR_FILE_NAME); 187 // Mock initial data 188 encryptedData = encryptData( 189 "I hope it helped!".getBytes(), retrieveIv(), 190 deriveKeyInsecurely("unguessable", KEY_SIZE)); 191 } 192 193 /* 194 *********************************************************************************************** 195 * Everything below this comment is a succession of mocks that would rarely interest someone on 196 * Earth. They are merely intended to make the example self contained. 197 *********************************************************************************************** 198 */ 199 200 private boolean isDataStoredWithInsecureKey() { 201 // Your app should have a way to tell whether the data has been re-encrypted in a secure 202 // fashion, in this mock we use the existence of a file with a certain name to indicate 203 // that. 204 return !fileExists("encrypted_with_secure_key"); 205 } 206 207 private byte[] retrieveIv() { 208 byte[] iv = new byte[IV_SIZE]; 209 // Ideally your data should have been encrypted with a random iv. This creates a random iv 210 // if not present, in order to encrypt our mock data. 211 readFromFileOrCreateRandom("iv", iv); 212 return iv; 213 } 214 215 private byte[] retrieveSalt() { 216 // Salt must be at least the same size as the key. 217 byte[] salt = new byte[KEY_SIZE]; 218 // Create a random salt if encrypting for the first time, and save it for future use. 219 readFromFileOrCreateRandom("salt", salt); 220 return salt; 221 } 222 223 private byte[] encryptedData = null; 224 225 private byte[] retrieveEncryptedData() { 226 return encryptedData; 227 } 228 229 private void storeDataEncryptedWithSecureKey(byte[] encryptedData) { 230 // Mock implementation. 231 this.encryptedData = encryptedData; 232 writeToFile(SECURE_ENCRYPTION_INDICATOR_FILE_NAME, new byte[1]); 233 } 234 235 /** 236 * Read from file or return random bytes in the given array. 237 * 238 * <p>Save to file if file didn't exist. 239 */ 240 private void readFromFileOrCreateRandom(String fileName, byte[] bytes) { 241 if (fileExists(fileName)) { 242 readBytesFromFile(fileName, bytes); 243 return; 244 } 245 SecureRandom sr = new SecureRandom(); 246 sr.nextBytes(bytes); 247 writeToFile(fileName, bytes); 248 } 249 250 private boolean fileExists(String fileName) { 251 File file = new File(getFilesDir(), fileName); 252 return file.exists(); 253 } 254 255 private void removeFile(String fileName) { 256 File file = new File(getFilesDir(), fileName); 257 file.delete(); 258 } 259 260 private void writeToFile(String fileName, byte[] bytes) { 261 try (FileOutputStream fos = openFileOutput(fileName, Context.MODE_PRIVATE)) { 262 fos.write(bytes); 263 } catch (IOException e) { 264 throw new RuntimeException("Couldn't write to " + fileName, e); 265 } 266 } 267 268 private void readBytesFromFile(String fileName, byte[] bytes) { 269 try (FileInputStream fis = openFileInput(fileName)) { 270 int numBytes = 0; 271 while (numBytes < bytes.length) { 272 int n = fis.read(bytes, numBytes, bytes.length - numBytes); 273 if (n <= 0) { 274 throw new RuntimeException("Couldn't read from " + fileName); 275 } 276 numBytes += n; 277 } 278 } catch (IOException e) { 279 throw new RuntimeException("Couldn't read from " + fileName, e); 280 } 281 } 282 283 private static final int IV_SIZE = 16; 284 private static final int KEY_SIZE = 32; 285 private static final String SECURE_ENCRYPTION_INDICATOR_FILE_NAME = 286 "encrypted_with_secure_key"; 287 } 288 289