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      */
deriveKeyInsecurely(String password, int keySizeInBytes)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      */
deriveKeySecurely(String password, int keySizeInBytes)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      */
retrieveData(String password)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
onCreate(Bundle savedInstanceState)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 
encryptOrDecrypt( byte[] data, SecretKey key, byte[] iv, boolean isEncrypt)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 
encryptData(byte[] data, byte[] iv, SecretKey key)169     private static byte[] encryptData(byte[] data, byte[] iv, SecretKey key) {
170         return encryptOrDecrypt(data, key, iv, true);
171     }
172 
decryptData(byte[] data, byte[] iv, SecretKey key)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      */
cleanRoomStart()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 
isDataStoredWithInsecureKey()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 
retrieveIv()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 
retrieveSalt()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 
retrieveEncryptedData()225     private byte[] retrieveEncryptedData() {
226         return encryptedData;
227     }
228 
storeDataEncryptedWithSecureKey(byte[] encryptedData)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      */
readFromFileOrCreateRandom(String fileName, byte[] bytes)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 
fileExists(String fileName)250     private boolean fileExists(String fileName) {
251         File file = new File(getFilesDir(), fileName);
252         return file.exists();
253     }
254 
removeFile(String fileName)255     private void removeFile(String fileName) {
256         File file = new File(getFilesDir(), fileName);
257         file.delete();
258     }
259 
writeToFile(String fileName, byte[] bytes)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 
readBytesFromFile(String fileName, byte[] bytes)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