Home | History | Annotate | Download | only in brokenkeyderivation
      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