From 952349c6b908d076bfd08e33c8308aa784c627c1 Mon Sep 17 00:00:00 2001 From: Sergio Giro Date: Fri, 3 Jun 2016 19:04:23 +0100 Subject: [PATCH] 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. (cherry picked from commit 477c90ca1a08fa20448399c0606110e697d15667) Bug: 27873296 Change-Id: I180d76c4aa7fb9aa8c332119bf094e050a63b58c --- samples/BrokenKeyDerivation/Android.mk | 16 + .../BrokenKeyDerivation/AndroidManifest.xml | 32 + .../layout/brokenkeyderivation_activity.xml | 23 + .../res/values/strings.xml | 19 + .../BrokenKeyDerivationActivity.java | 288 +++++++++ .../InsecureSHA1PRNGKeyDerivator.java | 609 ++++++++++++++++++ samples/BrokenKeyDerivation/tests/Android.mk | 16 + .../tests/AndroidManifest.xml | 32 + .../tests/build.properties | 1 + .../BrokenKeyDerivationTest.java | 40 ++ 10 files changed, 1076 insertions(+) create mode 100644 samples/BrokenKeyDerivation/Android.mk create mode 100644 samples/BrokenKeyDerivation/AndroidManifest.xml create mode 100644 samples/BrokenKeyDerivation/res/layout/brokenkeyderivation_activity.xml create mode 100644 samples/BrokenKeyDerivation/res/values/strings.xml create mode 100644 samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationActivity.java create mode 100644 samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/InsecureSHA1PRNGKeyDerivator.java create mode 100644 samples/BrokenKeyDerivation/tests/Android.mk create mode 100644 samples/BrokenKeyDerivation/tests/AndroidManifest.xml create mode 100644 samples/BrokenKeyDerivation/tests/build.properties create mode 100644 samples/BrokenKeyDerivation/tests/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationTest.java diff --git a/samples/BrokenKeyDerivation/Android.mk b/samples/BrokenKeyDerivation/Android.mk new file mode 100644 index 000000000..df767c3bb --- /dev/null +++ b/samples/BrokenKeyDerivation/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := samples + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := BrokenKeyDerivation + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/samples/BrokenKeyDerivation/AndroidManifest.xml b/samples/BrokenKeyDerivation/AndroidManifest.xml new file mode 100644 index 000000000..f7929458f --- /dev/null +++ b/samples/BrokenKeyDerivation/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/samples/BrokenKeyDerivation/res/layout/brokenkeyderivation_activity.xml b/samples/BrokenKeyDerivation/res/layout/brokenkeyderivation_activity.xml new file mode 100644 index 000000000..bcbcb3ea1 --- /dev/null +++ b/samples/BrokenKeyDerivation/res/layout/brokenkeyderivation_activity.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/samples/BrokenKeyDerivation/res/values/strings.xml b/samples/BrokenKeyDerivation/res/values/strings.xml new file mode 100644 index 000000000..741d4d54c --- /dev/null +++ b/samples/BrokenKeyDerivation/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationActivity.java b/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationActivity.java new file mode 100644 index 000000000..efca31458 --- /dev/null +++ b/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationActivity.java @@ -0,0 +1,288 @@ +/* + * 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 + * + * here 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 insecure 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. + * + *

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. + * + *

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"; +} + diff --git a/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/InsecureSHA1PRNGKeyDerivator.java b/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/InsecureSHA1PRNGKeyDerivator.java new file mode 100644 index 000000000..96d8b8d28 --- /dev/null +++ b/samples/BrokenKeyDerivation/src/com/example/android/brokenkeyderivation/InsecureSHA1PRNGKeyDerivator.java @@ -0,0 +1,609 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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; + +/** + * Stripped-down version of the SHA1PRNG provided by the Crypto provider. + * + * The Crypto provider that offers this functionality was deprecated on Android. + * + * Use this class only to retrieve encrypted data that couldn't be retrieved otherwise. + */ +class InsecureSHA1PRNGKeyDerivator { + + /** + * Only public method. Derive a key from the given seed. + * + * Use this method only to retrieve encrypted data that couldn't be retrieved otherwise. + * + * @param seed seed used for the random generator, usually coming from a password + * @param keySizeInBytes length of the array returned + */ + public static byte[] deriveInsecureKey(byte[] seed, int keySizeInBytes) { + InsecureSHA1PRNGKeyDerivator derivator = new InsecureSHA1PRNGKeyDerivator(); + derivator.setSeed(seed); + byte[] key = new byte[keySizeInBytes]; + derivator.nextBytes(key); + return key; + } + + // constants to use in expressions operating on bytes in int and long variables: + // END_FLAGS - final bytes in words to append to message; + // see "ch.5.1 Padding the Message, FIPS 180-2" + // RIGHT1 - shifts to right for left half of long + // RIGHT2 - shifts to right for right half of long + // LEFT - shifts to left for bytes + // MASK - mask to select counter's bytes after shift to right + + private static final int[] END_FLAGS = { 0x80000000, 0x800000, 0x8000, 0x80 }; + + private static final int[] RIGHT1 = { 0, 40, 48, 56 }; + + private static final int[] RIGHT2 = { 0, 8, 16, 24 }; + + private static final int[] LEFT = { 0, 24, 16, 8 }; + + private static final int[] MASK = { 0xFFFFFFFF, 0x00FFFFFF, 0x0000FFFF, + 0x000000FF }; + + // HASHBYTES_TO_USE defines # of bytes returned by "computeHash(byte[])" + // to use to form byte array returning by the "nextBytes(byte[])" method + // Note, that this implementation uses more bytes than it is defined + // in the above specification. + private static final int HASHBYTES_TO_USE = 20; + + // value of 16 defined in the "SECURE HASH STANDARD", FIPS PUB 180-2 + private static final int FRAME_LENGTH = 16; + + // miscellaneous constants defined in this implementation: + // COUNTER_BASE - initial value to set to "counter" before computing "nextBytes(..)"; + // note, that the exact value is not defined in STANDARD + // HASHCOPY_OFFSET - offset for copy of current hash in "copies" array + // EXTRAFRAME_OFFSET - offset for extra frame in "copies" array; + // as the extra frame follows the current hash frame, + // EXTRAFRAME_OFFSET is equal to length of current hash frame + // FRAME_OFFSET - offset for frame in "copies" array + // MAX_BYTES - maximum # of seed bytes processing which doesn't require extra frame + // see (1) comments on usage of "seed" array below and + // (2) comments in "engineNextBytes(byte[])" method + // + // UNDEFINED - three states of engine; initially its state is "UNDEFINED" + // SET_SEED call to "engineSetSeed" sets up "SET_SEED" state, + // NEXT_BYTES call to "engineNextByte" sets up "NEXT_BYTES" state + + private static final int COUNTER_BASE = 0; + + private static final int HASHCOPY_OFFSET = 0; + + private static final int EXTRAFRAME_OFFSET = 5; + + private static final int FRAME_OFFSET = 21; + + private static final int MAX_BYTES = 48; + + private static final int UNDEFINED = 0; + + private static final int SET_SEED = 1; + + private static final int NEXT_BYTES = 2; + + // Structure of "seed" array: + // - 0-79 - words for computing hash + // - 80 - unused + // - 81 - # of seed bytes in current seed frame + // - 82-86 - 5 words, current seed hash + private transient int[] seed; + + // total length of seed bytes, including all processed + private transient long seedLength; + + // Structure of "copies" array + // - 0-4 - 5 words, copy of current seed hash + // - 5-20 - extra 16 words frame; + // is used if final padding exceeds 512-bit length + // - 21-36 - 16 word frame to store a copy of remaining bytes + private transient int[] copies; + + // ready "next" bytes; needed because words are returned + private transient byte[] nextBytes; + + // index of used bytes in "nextBytes" array + private transient int nextBIndex; + + // variable required according to "SECURE HASH STANDARD" + private transient long counter; + + // contains int value corresponding to engine's current state + private transient int state; + + /** + * constant defined in "SECURE HASH STANDARD" + */ + private static final int H0 = 0x67452301; + + + /** + * constant defined in "SECURE HASH STANDARD" + */ + private static final int H1 = 0xEFCDAB89; + + + /** + * constant defined in "SECURE HASH STANDARD" + */ + private static final int H2 = 0x98BADCFE; + + + /** + * constant defined in "SECURE HASH STANDARD" + */ + private static final int H3 = 0x10325476; + + + /** + * constant defined in "SECURE HASH STANDARD" + */ + private static final int H4 = 0xC3D2E1F0; + + + /** + * offset in buffer to store number of bytes in 0-15 word frame + */ + private static final int BYTES_OFFSET = 81; + + + /** + * offset in buffer to store current hash value + */ + private static final int HASH_OFFSET = 82; + + + /** + * # of bytes in H0-H4 words;
+ * in this implementation # is set to 20 (in general # varies from 1 to 20) + */ + private static final int DIGEST_LENGTH = 20; + + // The "seed" array is used to compute both "current seed hash" and "next bytes". + // + // As the "SHA1" algorithm computes a hash of entire seed by splitting it into + // a number of the 512-bit length frames (512 bits = 64 bytes = 16 words), + // "current seed hash" is a hash (5 words, 20 bytes) for all previous full frames; + // remaining bytes are stored in the 0-15 word frame of the "seed" array. + // + // As for calculating "next bytes", + // both remaining bytes and "current seed hash" are used, + // to preserve the latter for following "setSeed(..)" commands, + // the following technique is used: + // - upon getting "nextBytes(byte[])" invoked, single or first in row, + // which requires computing new hash, that is, + // there is no more bytes remaining from previous "next bytes" computation, + // remaining bytes are copied into the 21-36 word frame of the "copies" array; + // - upon getting "setSeed(byte[])" invoked, single or first in row, + // remaining bytes are copied back. + + private InsecureSHA1PRNGKeyDerivator() { + seed = new int[HASH_OFFSET + EXTRAFRAME_OFFSET]; + seed[HASH_OFFSET] = H0; + seed[HASH_OFFSET + 1] = H1; + seed[HASH_OFFSET + 2] = H2; + seed[HASH_OFFSET + 3] = H3; + seed[HASH_OFFSET + 4] = H4; + + seedLength = 0; + copies = new int[2 * FRAME_LENGTH + EXTRAFRAME_OFFSET]; + nextBytes = new byte[DIGEST_LENGTH]; + nextBIndex = HASHBYTES_TO_USE; + counter = COUNTER_BASE; + state = UNDEFINED; + } + + /* + * The method invokes the SHA1Impl's "updateHash(..)" method + * to update current seed frame and + * to compute new intermediate hash value if the frame is full. + * + * After that it computes a length of whole seed. + */ + private void updateSeed(byte[] bytes) { + + // on call: "seed" contains current bytes and current hash; + // on return: "seed" contains new current bytes and possibly new current hash + // if after adding, seed bytes overfill its buffer + updateHash(seed, bytes, 0, bytes.length - 1); + + seedLength += bytes.length; + } + + /** + * Changes current seed by supplementing a seed argument to the current seed, + * if this already set; + * the argument is used as first seed otherwise.
+ * + * The method overrides "engineSetSeed(byte[])" in class SecureRandomSpi. + * + * @param + * seed - byte array + * @throws + * NullPointerException - if null is passed to the "seed" argument + */ + private void setSeed(byte[] seed) { + if (seed == null) { + throw new NullPointerException("seed == null"); + } + + if (state == NEXT_BYTES) { // first setSeed after NextBytes; restoring hash + System.arraycopy(copies, HASHCOPY_OFFSET, this.seed, HASH_OFFSET, + EXTRAFRAME_OFFSET); + } + state = SET_SEED; + + if (seed.length != 0) { + updateSeed(seed); + } + } + + /** + * Writes random bytes into an array supplied. + * Bits in a byte are from left to right.
+ * + * To generate random bytes, the "expansion of source bits" method is used, + * that is, + * the current seed with a 64-bit counter appended is used to compute new bits. + * The counter is incremented by 1 for each 20-byte output.
+ * + * The method overrides engineNextBytes in class SecureRandomSpi. + * + * @param + * bytes - byte array to be filled in with bytes + * @throws + * NullPointerException - if null is passed to the "bytes" argument + */ + protected synchronized void nextBytes(byte[] bytes) { + + int i, n; + + long bits; // number of bits required by Secure Hash Standard + int nextByteToReturn; // index of ready bytes in "bytes" array + int lastWord; // index of last word in frame containing bytes + + // This is a bug since words are 4 bytes. Android used to keep it this way for backward + // compatibility. + final int extrabytes = 7;// # of bytes to add in order to computer # of 8 byte words + + if (bytes == null) { + throw new NullPointerException("bytes == null"); + } + + // This is a bug since extraBytes == 7 instead of 3. Android used to keep it this way for + // backward compatibility. + lastWord = seed[BYTES_OFFSET] == 0 ? 0 + : (seed[BYTES_OFFSET] + extrabytes) >> 3 - 1; + + if (state == UNDEFINED) { + + throw new IllegalStateException("No seed supplied!"); + + } else if (state == SET_SEED) { + + System.arraycopy(seed, HASH_OFFSET, copies, HASHCOPY_OFFSET, + EXTRAFRAME_OFFSET); + + // possible cases for 64-byte frame: + // + // seed bytes < 48 - remaining bytes are enough for all, 8 counter bytes, + // 0x80, and 8 seedLength bytes; no extra frame required + // 48 < seed bytes < 56 - remaining 9 bytes are for 0x80 and 8 counter bytes + // extra frame contains only seedLength value at the end + // seed bytes > 55 - extra frame contains both counter's bytes + // at the beginning and seedLength value at the end; + // note, that beginning extra bytes are not more than 8, + // that is, only 2 extra words may be used + + // no need to set to "0" 3 words after "lastWord" and + // more than two words behind frame + for (i = lastWord + 3; i < FRAME_LENGTH + 2; i++) { + seed[i] = 0; + } + + bits = (seedLength << 3) + 64; // transforming # of bytes into # of bits + + // putting # of bits into two last words (14,15) of 16 word frame in + // seed or copies array depending on total length after padding + if (seed[BYTES_OFFSET] < MAX_BYTES) { + seed[14] = (int) (bits >>> 32); + seed[15] = (int) (bits & 0xFFFFFFFF); + } else { + copies[EXTRAFRAME_OFFSET + 14] = (int) (bits >>> 32); + copies[EXTRAFRAME_OFFSET + 15] = (int) (bits & 0xFFFFFFFF); + } + + nextBIndex = HASHBYTES_TO_USE; // skipping remaining random bits + } + state = NEXT_BYTES; + + if (bytes.length == 0) { + return; + } + + nextByteToReturn = 0; + + // possibly not all of HASHBYTES_TO_USE bytes were used previous time + n = (HASHBYTES_TO_USE - nextBIndex) < (bytes.length - nextByteToReturn) ? HASHBYTES_TO_USE + - nextBIndex + : bytes.length - nextByteToReturn; + if (n > 0) { + System.arraycopy(nextBytes, nextBIndex, bytes, nextByteToReturn, n); + nextBIndex += n; + nextByteToReturn += n; + } + + if (nextByteToReturn >= bytes.length) { + return; // return because "bytes[]" are filled in + } + + n = seed[BYTES_OFFSET] & 0x03; + for (;;) { + if (n == 0) { + + seed[lastWord] = (int) (counter >>> 32); + seed[lastWord + 1] = (int) (counter & 0xFFFFFFFF); + seed[lastWord + 2] = END_FLAGS[0]; + + } else { + + seed[lastWord] |= (int) ((counter >>> RIGHT1[n]) & MASK[n]); + seed[lastWord + 1] = (int) ((counter >>> RIGHT2[n]) & 0xFFFFFFFF); + seed[lastWord + 2] = (int) ((counter << LEFT[n]) | END_FLAGS[n]); + } + if (seed[BYTES_OFFSET] > MAX_BYTES) { + copies[EXTRAFRAME_OFFSET] = seed[FRAME_LENGTH]; + copies[EXTRAFRAME_OFFSET + 1] = seed[FRAME_LENGTH + 1]; + } + + computeHash(seed); + + if (seed[BYTES_OFFSET] > MAX_BYTES) { + + System.arraycopy(seed, 0, copies, FRAME_OFFSET, FRAME_LENGTH); + System.arraycopy(copies, EXTRAFRAME_OFFSET, seed, 0, + FRAME_LENGTH); + + computeHash(seed); + System.arraycopy(copies, FRAME_OFFSET, seed, 0, FRAME_LENGTH); + } + counter++; + + int j = 0; + for (i = 0; i < EXTRAFRAME_OFFSET; i++) { + int k = seed[HASH_OFFSET + i]; + nextBytes[j] = (byte) (k >>> 24); // getting first byte from left + nextBytes[j + 1] = (byte) (k >>> 16); // getting second byte from left + nextBytes[j + 2] = (byte) (k >>> 8); // getting third byte from left + nextBytes[j + 3] = (byte) (k); // getting fourth byte from left + j += 4; + } + + nextBIndex = 0; + j = HASHBYTES_TO_USE < (bytes.length - nextByteToReturn) ? HASHBYTES_TO_USE + : bytes.length - nextByteToReturn; + + if (j > 0) { + System.arraycopy(nextBytes, 0, bytes, nextByteToReturn, j); + nextByteToReturn += j; + nextBIndex += j; + } + + if (nextByteToReturn >= bytes.length) { + break; + } + } + } + + /** + * The method generates a 160 bit hash value using + * a 512 bit message stored in first 16 words of int[] array argument and + * current hash value stored in five words, beginning OFFSET+1, of the array argument. + * Computation is done according to SHA-1 algorithm. + * + * The resulting hash value replaces the previous hash value in the array; + * original bits of the message are not preserved. + * + * No checks on argument supplied, that is, + * a calling method is responsible for such checks. + * In case of incorrect array passed to the method + * either NPE or IndexOutOfBoundException gets thrown by JVM. + * + * @params + * arrW - integer array; arrW.length >= (BYTES_OFFSET+6);
+ * only first (BYTES_OFFSET+6) words are used + */ + private static void computeHash(int[] arrW) { + + int a = arrW[HASH_OFFSET ]; + int b = arrW[HASH_OFFSET +1]; + int c = arrW[HASH_OFFSET +2]; + int d = arrW[HASH_OFFSET +3]; + int e = arrW[HASH_OFFSET +4]; + + int temp; + + // In this implementation the "d. For t = 0 to 79 do" loop + // is split into four loops. The following constants: + // K = 5A827999 0 <= t <= 19 + // K = 6ED9EBA1 20 <= t <= 39 + // K = 8F1BBCDC 40 <= t <= 59 + // K = CA62C1D6 60 <= t <= 79 + // are hex literals in the loops. + + for ( int t = 16; t < 80 ; t++ ) { + + temp = arrW[t-3] ^ arrW[t-8] ^ arrW[t-14] ^ arrW[t-16]; + arrW[t] = ( temp<<1 ) | ( temp>>>31 ); + } + + for ( int t = 0 ; t < 20 ; t++ ) { + + temp = ( ( a<<5 ) | ( a>>>27 ) ) + + ( ( b & c) | ((~b) & d) ) + + ( e + arrW[t] + 0x5A827999 ) ; + e = d; + d = c; + c = ( b<<30 ) | ( b>>>2 ) ; + b = a; + a = temp; + } + for ( int t = 20 ; t < 40 ; t++ ) { + + temp = ((( a<<5 ) | ( a>>>27 ))) + (b ^ c ^ d) + (e + arrW[t] + 0x6ED9EBA1) ; + e = d; + d = c; + c = ( b<<30 ) | ( b>>>2 ) ; + b = a; + a = temp; + } + for ( int t = 40 ; t < 60 ; t++ ) { + + temp = (( a<<5 ) | ( a>>>27 )) + ((b & c) | (b & d) | (c & d)) + + (e + arrW[t] + 0x8F1BBCDC) ; + e = d; + d = c; + c = ( b<<30 ) | ( b>>>2 ) ; + b = a; + a = temp; + } + for ( int t = 60 ; t < 80 ; t++ ) { + + temp = ((( a<<5 ) | ( a>>>27 ))) + (b ^ c ^ d) + (e + arrW[t] + 0xCA62C1D6) ; + e = d; + d = c; + c = ( b<<30 ) | ( b>>>2 ) ; + b = a; + a = temp; + } + + arrW[HASH_OFFSET ] += a; + arrW[HASH_OFFSET +1] += b; + arrW[HASH_OFFSET +2] += c; + arrW[HASH_OFFSET +3] += d; + arrW[HASH_OFFSET +4] += e; + } + + /** + * The method appends new bytes to existing ones + * within limit of a frame of 64 bytes (16 words). + * + * Once a length of accumulated bytes reaches the limit + * the "computeHash(int[])" method is invoked on the array to compute updated hash, + * and the number of bytes in the frame is set to 0. + * Thus, after appending all bytes, the array contain only those bytes + * that were not used in computing final hash value yet. + * + * No checks on arguments passed to the method, that is, + * a calling method is responsible for such checks. + * + * @params + * intArray - int array containing bytes to which to append; + * intArray.length >= (BYTES_OFFSET+6) + * @params + * byteInput - array of bytes to use for the update + * @params + * from - the offset to start in the "byteInput" array + * @params + * to - a number of the last byte in the input array to use, + * that is, for first byte "to"==0, for last byte "to"==input.length-1 + */ + private static void updateHash(int[] intArray, byte[] byteInput, int fromByte, int toByte) { + + // As intArray contains a packed bytes + // the buffer's index is in the intArray[BYTES_OFFSET] element + + int index = intArray[BYTES_OFFSET]; + int i = fromByte; + int maxWord; + int nBytes; + + int wordIndex = index >>2; + int byteIndex = index & 0x03; + + intArray[BYTES_OFFSET] = ( index + toByte - fromByte + 1 ) & 077 ; + + // In general case there are 3 stages : + // - appending bytes to non-full word, + // - writing 4 bytes into empty words, + // - writing less than 4 bytes in last word + + if ( byteIndex != 0 ) { // appending bytes in non-full word (as if) + + for ( ; ( i <= toByte ) && ( byteIndex < 4 ) ; i++ ) { + intArray[wordIndex] |= ( byteInput[i] & 0xFF ) << ((3 - byteIndex)<<3) ; + byteIndex++; + } + if ( byteIndex == 4 ) { + wordIndex++; + if ( wordIndex == 16 ) { // intArray is full, computing hash + + computeHash(intArray); + wordIndex = 0; + } + } + if ( i > toByte ) { // all input bytes appended + return ; + } + } + + // writing full words + + maxWord = (toByte - i + 1) >> 2; // # of remaining full words, may be "0" + for ( int k = 0; k < maxWord ; k++ ) { + + intArray[wordIndex] = ( ((int) byteInput[i ] & 0xFF) <<24 ) | + ( ((int) byteInput[i +1] & 0xFF) <<16 ) | + ( ((int) byteInput[i +2] & 0xFF) <<8 ) | + ( ((int) byteInput[i +3] & 0xFF) ) ; + i += 4; + wordIndex++; + + if ( wordIndex < 16 ) { // buffer is not full yet + continue; + } + computeHash(intArray); // buffer is full, computing hash + wordIndex = 0; + } + + // writing last incomplete word + // after writing free byte positions are set to "0"s + + nBytes = toByte - i +1; + if ( nBytes != 0 ) { + + int w = ((int) byteInput[i] & 0xFF) <<24 ; + + if ( nBytes != 1 ) { + w |= ((int) byteInput[i +1] & 0xFF) <<16 ; + if ( nBytes != 2) { + w |= ((int) byteInput[i +2] & 0xFF) <<8 ; + } + } + intArray[wordIndex] = w; + } + + return ; + } +} diff --git a/samples/BrokenKeyDerivation/tests/Android.mk b/samples/BrokenKeyDerivation/tests/Android.mk new file mode 100644 index 000000000..fc161a9b0 --- /dev/null +++ b/samples/BrokenKeyDerivation/tests/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := android.test.runner + +LOCAL_PACKAGE_NAME := BrokenKeyDerivationTests + +LOCAL_MODULE_TAGS := tests + +LOCAL_INSTRUMENTATION_FOR := BrokenKeyDerivation + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) diff --git a/samples/BrokenKeyDerivation/tests/AndroidManifest.xml b/samples/BrokenKeyDerivation/tests/AndroidManifest.xml new file mode 100644 index 000000000..323e227a3 --- /dev/null +++ b/samples/BrokenKeyDerivation/tests/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/samples/BrokenKeyDerivation/tests/build.properties b/samples/BrokenKeyDerivation/tests/build.properties new file mode 100644 index 000000000..e0c39def1 --- /dev/null +++ b/samples/BrokenKeyDerivation/tests/build.properties @@ -0,0 +1 @@ +tested.project.dir=.. diff --git a/samples/BrokenKeyDerivation/tests/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationTest.java b/samples/BrokenKeyDerivation/tests/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationTest.java new file mode 100644 index 000000000..14e11dfbb --- /dev/null +++ b/samples/BrokenKeyDerivation/tests/src/com/example/android/brokenkeyderivation/BrokenKeyDerivationTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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.test.ActivityInstrumentationTestCase2; + +/** + * Make sure that the main launcher activity opens up properly, which will be + * verified by {@link #testActivityTestCaseSetUpProperly}. + */ +public class BrokenKeyDerivationTest extends ActivityInstrumentationTestCase2 { + + /** + * Creates an {@link ActivityInstrumentationTestCase2} for the {@link BrokenKeyDerivationActivity} activity. + */ + public BrokenKeyDerivationTest() { + super(BrokenKeyDerivationActivity.class); + } + + /** + * Verifies that the activity under test can be launched. + */ + public void testActivityTestCaseSetUpProperly() { + assertNotNull("activity should be launched successfully", getActivity()); + } +}