Merge "KeyDerivationFunction: example about treating data encrypted via SHA1PRNG" into nyc-dev
This commit is contained in:
16
samples/BrokenKeyDerivation/Android.mk
Normal file
16
samples/BrokenKeyDerivation/Android.mk
Normal file
@@ -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))
|
||||
32
samples/BrokenKeyDerivation/AndroidManifest.xml
Normal file
32
samples/BrokenKeyDerivation/AndroidManifest.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<!-- Declare the contents of this Android application. The namespace
|
||||
attribute brings in the Android platform namespace, and the package
|
||||
supplies a unique name for the application. When writing your
|
||||
own application, the package name must be changed from "com.example.*"
|
||||
to come from a domain that you own or have control over. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.brokenkeyderivation">
|
||||
<application android:label="Broken Key Derivation">
|
||||
<activity android:name="BrokenKeyDerivationActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<EditText xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="18sp"
|
||||
android:autoText="true"
|
||||
android:capitalize="sentences" />
|
||||
|
||||
19
samples/BrokenKeyDerivation/res/values/strings.xml
Normal file
19
samples/BrokenKeyDerivation/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
</resources>
|
||||
@@ -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
|
||||
* <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";
|
||||
}
|
||||
|
||||
@@ -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; <BR>
|
||||
* 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. <BR>
|
||||
*
|
||||
* 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. <BR>
|
||||
*
|
||||
* 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. <BR>
|
||||
*
|
||||
* 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); <BR>
|
||||
* 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 ;
|
||||
}
|
||||
}
|
||||
16
samples/BrokenKeyDerivation/tests/Android.mk
Normal file
16
samples/BrokenKeyDerivation/tests/Android.mk
Normal file
@@ -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)
|
||||
32
samples/BrokenKeyDerivation/tests/AndroidManifest.xml
Normal file
32
samples/BrokenKeyDerivation/tests/AndroidManifest.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.brokenkeyderivation.tests">
|
||||
|
||||
<!-- We add an application tag here just so that we can indicate that
|
||||
this package needs to link against the android.test library,
|
||||
which is needed when building test cases. -->
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.example.android.brokenkeyderivation"
|
||||
android:label="BrokenKeyDerivation tests">
|
||||
</instrumentation>
|
||||
|
||||
</manifest>
|
||||
1
samples/BrokenKeyDerivation/tests/build.properties
Normal file
1
samples/BrokenKeyDerivation/tests/build.properties
Normal file
@@ -0,0 +1 @@
|
||||
tested.project.dir=..
|
||||
@@ -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<BrokenKeyDerivationActivity> {
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user