// Copyright (c) 2009, Google Inc.
//
// 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 net.tawacentral.roger.secrets;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.mindrot.jbcrypt.BCrypt;
import android.util.Log;
/**
* Helper class to manage cipher keys and encrypting and decrypting data.
*
* @author rogerta
*/
public class SecurityUtils {
/** Tag for logging purposes. */
public static final String LOG_TAG = "Secrets";
/** Return value of createCiphers call. */
public static class CipherInfo {
public Cipher encryptCipher;
public Cipher decryptCipher;
public byte[] salt;
public int rounds;
}
// The following three constants were used with the initial implementation of
// secrets. Secrets now uses a more secure algorithm, but for backwards
// compatibility, the program needs to be able to load the secrets with the
// old algorithm. However, secrets will only be saved with the new algorithm
// moving forward.
//
// All members that end with V1 or v1 have to do with the old algorithm.
private static final String KEY_FACTORY_V1 =
"PBEWITHSHA-256AND256BITAES-CBC-BC";
// TODO(rogerta): for now, I'm using an iteration count of 100. I had
// initially set it to 5000, but that took *ages* to create the cipher. I
// need to figure out if this is a safe value to use.
private static final int KEY_ITERATION_COUNT_V1 = 100;
// The salt can be hardcoded, because the secrets file is never transmitted
// off the phone. Generating a ramdom salt would not provide any real extra
// protection, because if an attacker can get to the secrets file, then he
// has broken into the phone, and therefore would be able to get to the
// random salt too.
private static final byte[] salt_v1 = {
(byte)0xA4, (byte)0x0B, (byte)0xC8, (byte)0x34,
(byte)0xD6, (byte)0x95, (byte)0xF3, (byte)0x13
};
private static final String KEY_FACTORY = "PBEWITHSHA-256AND256BITAES-CBC-BC";
/** Class used to time the execution of functions */
static public class ExecutionTimer {
private long start = System.currentTimeMillis();
/** Returns the time in millisecs since the object was created. */
public long getElapsed() {
return System.currentTimeMillis() - start;
}
/** Prints the time in millisecs since the object was created to the log. */
public void logElapsed(String message) {
Log.d(LOG_TAG, message + getElapsed());
}
}
private static Cipher encryptCipher;
private static Cipher decryptCipher;
private static byte[] salt;
private static int rounds;
/**
* Get the cipher used to encrypt data using the password given to the
* createCiphers() function.
*/
public static Cipher getEncryptionCipher() {
return encryptCipher;
}
/**
* Get the cipher used to decrypt data using the password given to the
* createCiphers() function.
*/
public static Cipher getDecryptionCipher() {
return decryptCipher;
}
/**
* Gets the salt for this device.
* @return A byte array representing the salt for this specific device.
*/
public static byte[] getSalt() {
return salt;
}
/**
* Gets the rounds for this device.
* @return an integer representing the number of bcrypt rounds.
*/
public static int getRounds() {
return rounds;
}
/** Gets information about current ciphers. */
public static CipherInfo getCipherInfo() {
CipherInfo info = new CipherInfo();
info.encryptCipher = encryptCipher;
info.decryptCipher = decryptCipher;
info.salt = salt.clone();
info.rounds = rounds;
return info;
}
/**
* Creates a new unique random salt.
* @return A new salt value used to generate the secret key.
*/
private static byte[] createNewSalt() {
byte[] bytes = new byte[BCrypt.BCRYPT_SALT_LEN];
SecureRandom random = new SecureRandom();
random.nextBytes(bytes);
return bytes;
}
/**
* Create a decryption cipher using the old algorithm based on the given
* password string. The string is not stored internally.
*
* This method is used for backward compatibility only.
*
* @param password String to use for creating the ciphers.
* @return True if the ciphers were successfully created.
*/
public static Cipher createDecryptionCipherV1(String password) {
Cipher cipher = null;
ExecutionTimer timer = new ExecutionTimer();
try {
PBEKeySpec keyspec = new PBEKeySpec(password.toCharArray(),
salt_v1,
KEY_ITERATION_COUNT_V1,
32);
SecretKeyFactory skf = SecretKeyFactory.getInstance(KEY_FACTORY_V1);
SecretKey key = skf.generateSecret(keyspec);
AlgorithmParameterSpec aps = new PBEParameterSpec(salt_v1,
KEY_ITERATION_COUNT_V1);
cipher = Cipher.getInstance(KEY_FACTORY_V1);
cipher.init(Cipher.DECRYPT_MODE, key, aps);
} catch (Exception ex) {
Log.d(LOG_TAG, "createDecryptionCiphersV1", ex);
cipher = null;
}
timer.logElapsed("Time to create V1 d-cipher: ");
return cipher;
}
/**
* Create a pair of encryption and decryption ciphers based on the given
* password string. The string is not stored internally. This function
* needs to be called before calling getEncryptionCipher() or
* getDecryptionCipher().
*
* @param password String to use for creating the ciphers.
* @param salt The salt to use when creating the encryption key.
* @param rounds The number of rounds for bcrypt.
* @return CipherInfo structure with information about the created ciphers.
*/
public static CipherInfo createCiphers(String password,
byte[] salt,
int rounds) {
CipherInfo info = new CipherInfo();
ExecutionTimer timer = new ExecutionTimer();
try {
if (salt == null || rounds == 0) {
salt = createNewSalt();
rounds = determineBestRounds();
}
int plaintext[] = {0x155cbf8e, 0x57f57513, 0x3da787b9, 0x71679d82,
0x7cf72e93, 0x1ae25274, 0x64b54adc, 0x335cbd0b};
BCrypt bcrypt = new BCrypt();
byte[] rawBytes = bcrypt.crypt_raw(password.getBytes("UTF-8"),
salt, rounds, plaintext);
SecretKeySpec spec = new SecretKeySpec(rawBytes, KEY_FACTORY);
info.encryptCipher = Cipher.getInstance(KEY_FACTORY);
info.encryptCipher.init(Cipher.ENCRYPT_MODE, spec);
info.decryptCipher = Cipher.getInstance(KEY_FACTORY);
info.decryptCipher.init(Cipher.DECRYPT_MODE, spec);
info.salt = salt;
info.rounds = rounds;
} catch (Exception ex) {
Log.d(LOG_TAG, "createCiphers", ex);
info = null;
}
timer.logElapsed("Time to create ciphers rounds=" + rounds + ": ");
return info;
}
/**
* Create a pair of encryption and decryption ciphers based on the given
* password string. The string is not stored internally. This function
* needs to be called before calling getEncryptionCipher() or
* getDecryptionCipher().
*
* @param password String to use for creating the ciphers.
* @param salt The salt to use when creating the encryption key.
* @param rounds The number of rounds for bcrypt.
* @return True if the ciphers were successfully created.
*/
public static void saveCiphers(CipherInfo info) {
encryptCipher = info.encryptCipher;
decryptCipher = info.decryptCipher;
salt = info.salt.clone();
rounds = info.rounds;
}
/** Clear the ciphers from memory. */
public static void clearCiphers() {
decryptCipher = null;
encryptCipher = null;
salt = null;
rounds = 0;
}
/**
* Determines the ideal number of rounds to use for the bcrypt algorithm.
* More rounds are more secure, but require more time to log into Secrets.
* This function tries to balance security and convenience.
*
* Each round increment doubles the amount of work required by bcrypt to
* generate a key. This function assumes that time is proportional to work.
* So for example, if 4 rounds takes 0.1 seconds to generate a key, 5 rounds
* will take 0.2 seconds, 6 rounds 0.4 seconds, and so on. The assumption
* will be that the key must be generated in less than 0.9 seconds to remain
* convenient for the user.
*
* This function calculate how long it takes to generate a key using 4 rounds
* on the current device, then determines the maximum number of rounds such
* that the time to generate will remain below the convenience threshold.
*/
public static int determineBestRounds() {
byte[] salt = createNewSalt();
int plaintext[] = {0x155cbf8e, 0x57f57513, 0x3da787b9, 0x71679d82,
0x7cf72e93, 0x1ae25274, 0x64b54adc, 0x335cbd0b};
final byte[] password = {1, 2, 3, 4, 5, 6, 7, 8};
BCrypt bcrypt = new BCrypt();
// Calculate the time to create a cipher key with 4 rounds, in msecs.
// Do it twice and take the average.
final long start = System.currentTimeMillis();
bcrypt.crypt_raw(password, salt, 4, plaintext);
bcrypt.crypt_raw(password, salt, 4, plaintext);
final long T4 = (System.currentTimeMillis() - start) / 2;
// If T4 is the time in msecs to create the key with 4 rounds, then
// the time Tn to calculate the key using n rounds (n > 4) is:
//
// Tn = 2^(n - 4) * T4
//
// where we want Tn to be less than 900 msecs. Solving for n gives:
//
// Tn = 2^(n - 4) * T4 < 900
// n - 4 + log2(T4) < log2(900)
// n < 4 + log2(900) - log2(T4) -- solve for n
// n < 4 + ln(900)/ln(2) - ln(T4)/ln(2) -- convert to natural logs
//
// The best number of rounds is the floor of n.
final double n = 4 + (Math.log(900) - Math.log(T4)) / Math.log(2);
int rounds = (int) n;
// Make sure rounds does not exceed its valid range.
if (rounds < 4) {
rounds = 4;
} else if (rounds > 31) {
rounds = 31;
}
return rounds;
}
/** This method returns all available services types. */
/*public static String[] getServiceTypes() {
java.util.HashSet result = new HashSet();
// All all providers.
java.security.Provider[] providers =
java.security.Security.getProviders();
for (int i = 0; i < providers.length; ++i) {
// Get services provided by each provider.
java.util.Set keys = providers[i].keySet();
for (Iterator it=keys.iterator(); it.hasNext(); ) {
String key = (String)it.next();
key = key.split(" ")[0];
if (key.startsWith("Alg.Alias.")) {
// Strip the alias.
key = key.substring(10);
}
int ix = key.indexOf('.');
result.add(key.substring(0, ix));
}
}
return (String[])result.toArray(new String[result.size()]);
}*/
/** This method returns the available implementations for a service type. */
/*public static String[] getCryptoImpls(String serviceType) {
java.util.HashSet<String> result = new java.util.HashSet<String>();
// All all providers.
java.security.Provider[] providers =
java.security.Security.getProviders();
for (int i = 0; i < providers.length; ++i) {
// Get services provided by each provider.
java.util.Set<Object> keys = providers[i].keySet();
for (Object o : keys) {
String key = ((String)o).split(" ")[0];
if (key.startsWith(serviceType+".")) {
result.add(key.substring(serviceType.length()+1));
} else if (key.startsWith("Alg.Alias."+serviceType+".")) {
// This is an alias
result.add(key.substring(serviceType.length()+11));
}
}
}
return (String[])result.toArray(new String[result.size()]);
}*/
}