/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.gps2;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import junit.framework.Assert;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.util.Log;
import com.rareventure.android.Crypt;
import com.rareventure.android.DbUtil;
import com.rareventure.android.Util;
import com.rareventure.android.AndroidPreferenceSet.AndroidPreferences;
import com.rareventure.gps2.database.GpsLocationRow;
import com.rareventure.gps2.database.TAssert;
import com.rareventure.gps2.database.TimeZoneTimeRow;
import com.rareventure.gps2.database.UserLocationRow;
import com.rareventure.gps2.database.cache.AreaPanel;
import com.rareventure.gps2.database.cache.MediaLocTime;
import com.rareventure.gps2.database.cache.TimeTree;
public class GpsTrailerCrypt {
private static final int SALT_LENGTH = 32;
/**
* Does the actual encryption and decryption
*/
public Crypt crypt;
public static Preferences prefs = new Preferences();
private static HashMap<Integer, GpsTrailerCrypt> userDataKeyIdToGpsCrypt = new HashMap<Integer, GpsTrailerCrypt>();
/**
* Encodes userdatakey for encrypting new rows
*/
private static PrivateKey privateKey;
/**
* To be funny, we create a password even when there isn't one as specified
* by prefs.isNoPassword.
*/
public static final String NO_PASSWORD_PASSWORD = Util.rot13(/* ttt_installer:obfuscate_str */"cappadocia");
private static final String INTERNAL_SYMMETRIC_ENCRYPTION_NAME = "AES";
/**
* This is for encrypting the symmetric keys
*/
public static final String INTERNAL_SYMMETRIC_ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding";
// We use "AES/ECB/PKCS1Padding"; I think NoPadding (which is the default) is causing the leading zero bytes to be stripped
// from the data
//we use just AES here because that is what AESObfuscator from the google LVL uses, so if it's good enough
// for them...
public static final String SECRET_KEY_SPEC_ALGORITHM = "AES";
public static final String INTERNAL_ASYMMETRIC_ENCRYPTION_NAME = "RSA";
public static final String INTERNAL_ASYMMETRIC_ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding";
/**
* This uses RSA because we are basically signing timestamps with our private key.
* It shouldn't be used for any long messages, since it's using ECB
*/
private static final String TTT_ENCRYPTION_ALGORITHM = "RSA/ECB/PKCS1Padding";
public static final int RSA_KEY_SIZE = 2048;
/**
* Id of USER_DATA_KEY we are using to encrypt. Global to all tables
*/
public int userDataKeyId;
public GpsTrailerCrypt(int userDataKeyId, byte[] key) {
this.userDataKeyId = userDataKeyId;
crypt = new Crypt(key);
}
/**
* Initializes the database when the password is not known (for example,
* when we are starting the GpsTrailerService on system startup). Existing
* data cannot be decrypted, but can encrypt and decrypt new rows. Use the
* "instance" field.
*
* @param appId no longer used, use MASTER_APP_ID for now
*/
public static void initializeWithoutPassword(int appId) {
Assert.assertTrue("Initial database setup not done",
prefs.initialWorkPerformed);
//TODO 3: co: we don't really need a master app id, because we won't be
//cleaning data all at once, but rather only when we read it.
// Assert.assertTrue("Don't use the MASTER_APP_ID", appId != GTG.MASTER_APP_ID);
GTG.crypt = generateAndInitializeNewUserDataEncryptingKey(appId, GTG.db);
}
/**
* Initializes the database if necessary for one off first time setup and
* then sets up data so the instance() and instance(userDataKeyId) can be
* called
*
* @param password
* if null, then it is assumed there is no password
*/
public static boolean initialize(String password) {
if(!initializePrivateKey(password))
return false;
loadDefaultUserDataKey();
return true;
}
/**
* Sets a new password for the first time.
*
* All encrypted data and tables will be destroyed if present.
*
* @param password is only needed if prefs.isNoPassword is not set
*/
public static void deleteAllDataAndSetNewPassword(Context context, String password)
{
GpsTrailerDb.dropAndRecreateEncryptedTables(GTG.db);
setupPreferencesForCrypt(context, password);
}
public static boolean resetPassword(Context context, String oldPassword, String newPassword)
{
if(!initializePrivateKey(oldPassword))
return false; //bad old password
encryptPrivateKeyAndStoreInPrefs(privateKey, newPassword);
GTG.savePreferences(context);
return true;
}
/**
* Initializes privateKey field
*
* @param password is only needed if prefs.isNoPassword is not set
*/
public static boolean initializePrivateKey(String password) {
privateKey = decryptPrivateKey(password);
return privateKey != null;
}
/**
* Prefs must be setup before this is called (because of
* the salt)
* @param password
* @return
*/
public static boolean verifyPassword(String password)
{
return decryptPrivateKey(password) != null;
}
private static PrivateKey decryptPrivateKey(String password)
{
if (prefs.isNoPassword) {
Assert.assertTrue(password == null);
password = Util.rot13(GpsTrailerCrypt.NO_PASSWORD_PASSWORD);
}
try {
// Crypt keyDecryptor = new
// Crypt(Crypt.getRawKeyOldWay(password.getBytes(), prefs.salt),
// prefs.salt);
Crypt keyDecryptor = new Crypt(Crypt.getRawKey(password, prefs.salt));
byte[] encryptedPrivateKey = prefs.encryptedPrivateKey;
byte[] output = new byte[keyDecryptor
.getNumOutputBytesForDecryption(encryptedPrivateKey.length)];
int keyLength = keyDecryptor.decryptData(output,
encryptedPrivateKey);
byte[] output2 = new byte[keyLength];
System.arraycopy(output, 0, output2, 0, keyLength);
// Private keys are encoded with PKCS#8 (or so they say)
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(output);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
//we are assuming all errors are because of a wrong password
//TODO 3: we could alternately store an additional digest of the password,
//and check against that, but it seems that it would be another avenue of attack.
//and not worth it
return null;
}
}
private static void loadDefaultUserDataKey() {
Cursor c = GTG.db.rawQuery(
"select _id from user_data_key where app_id = ?",
new String[] { String.valueOf(GTG.MASTER_APP_ID) });
try {
c.moveToNext();
GTG.crypt = instance(c.getInt(0));
} finally {
DbUtil.closeCursors(c);
}
}
/**
* Initializes the preferences to setup public and private keys
* <p>
* Basically, we have a public-private key pair, and encrypt the private key
* with a password. When we actually encrypt data, we use a random key. We
* store the random key encrypted with the public key in the database
* <p>
* In this way, we can start the gps trailer service on startup on the phone
* and encrypt values right away, without needing a password.
*
* @param password may be null if one not given
*/
public static void setupPreferencesForCrypt(Context context, String password) {
// The output of the below is to populate:
// prefs.salt
// prefs.encryptedPrivateKey
// prefs.publicKey
/* ttt_installer:remove_line */Log.d(GTG.TAG,"Initializing database encryption");
try {
/* ttt_installer:remove_line */Log.d(GTG.TAG,"Generating public/private keys");
//
// Generate public and private key pair
//
KeyPairGenerator kpg = KeyPairGenerator.getInstance(INTERNAL_ASYMMETRIC_ENCRYPTION_NAME);
kpg.initialize(RSA_KEY_SIZE);
KeyPair kp = kpg.genKeyPair();
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
encryptPrivateKeyAndStoreInPrefs(privateKey, password);
prefs.publicKey = publicKey.getEncoded();
/* ttt_installer:remove_line */Log.d(GTG.TAG,"Saving preferences to database");
// now we need to save the settings
prefs.initialWorkPerformed = true;
GTG.savePreferences(context);
} catch (Exception e) {
prefs.publicKey = null;
prefs.encryptedPrivateKey = null;
prefs.salt = null;
prefs.initialWorkPerformed = false;
throw new IllegalStateException("There is a problem somewhere", e);
}
}
private static void encryptPrivateKeyAndStoreInPrefs(PrivateKey privateKey, String password) {
prefs.salt = new byte[SALT_LENGTH];
/* ttt_installer:remove_line */Log.d(GTG.TAG, "Generating salt");
//
// Generate salt
//
SecureRandom sr = new SecureRandom();
sr.nextBytes(prefs.salt);
/* ttt_installer:remove_line */Log.d(GTG.TAG, "Encrypting private key");
//
// Encrypt private key
//
if (password == null) {
prefs.isNoPassword = true;
password = Util.rot13(NO_PASSWORD_PASSWORD);
}
else prefs.isNoPassword = false;
Crypt keyEncryptor = new Crypt(Crypt.getRawKey(password, prefs.salt));
byte[] privateKeyData = privateKey.getEncoded();
prefs.encryptedPrivateKey = new byte[keyEncryptor
.getNumOutputBytesForEncryption(privateKeyData.length)];
keyEncryptor.encryptData(prefs.encryptedPrivateKey, 0, privateKeyData,
0, privateKeyData.length);
}
/**
* Creates a key used to encrypt and decrypt new rows.
*
* @param appId
* identifies the application that created the row.
* @param db
* @return GpsTrailerCrypt instance created
*/
public static GpsTrailerCrypt generateAndInitializeNewUserDataEncryptingKey(int appId, SQLiteDatabase db) {
try {
byte[] userDataKey = new byte[prefs.aesKeySize/8];
// this is really only going to called once per application, so we
// generate and forget our
// SecureRandom instance even though it's slow to create
new SecureRandom().nextBytes(userDataKey);
// now we need to encrypt the key using our public key, and store
// the encrypted key into
// the database so we can decrypt it using a private key when
// starting the reviewer
// (because the user will have entered the password at that point
// for decrypting
// the private key)
PublicKey publicKey = constructPublicKey();
Cipher cipher = Cipher.getInstance(INTERNAL_ASYMMETRIC_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherData = cipher.doFinal(userDataKey);
SQLiteStatement s = DbUtil
.createOrGetStatement(db,
"insert into USER_DATA_KEY (app_id, encrypted_key) values (?,?)");
s.bindLong(1, appId);
s.bindBlob(2, cipherData);
int userDataKeyId = (int) s.executeInsert();
// finally we create an instance using our non-encrypted key
return new GpsTrailerCrypt(userDataKeyId, userDataKey);
} catch (Exception e) {
throw new IllegalStateException("Can't seem to encrypt a key", e);
}
}
//TODO 2.5 we don't need app id anymore
/**
* Used to pull a particular crypt setup to decrypt a row
*/
public static GpsTrailerCrypt instance(int userDataKeyId) {
GpsTrailerCrypt crypt = userDataKeyIdToGpsCrypt.get(userDataKeyId);
// if we haven't cached a gpstrailercrypt for this row
if (crypt == null) {
Cursor c = GTG.db.rawQuery(
"select encrypted_key from user_data_key where _id = ?",
new String[] { String.valueOf(userDataKeyId) });
try {
c.moveToNext();
byte[] encryptedSymKey = c.getBlob(0);
Cipher cipher = Cipher.getInstance(INTERNAL_ASYMMETRIC_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] symKey = cipher.doFinal(encryptedSymKey);
crypt = new GpsTrailerCrypt(userDataKeyId, symKey);
userDataKeyIdToGpsCrypt.put(userDataKeyId, crypt);
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
DbUtil.closeCursors(c);
}
}
return crypt;
}
public static PublicKey constructPublicKey() throws InvalidKeySpecException,
NoSuchAlgorithmException {
// Public keys are encrypted with X.509 (or so they say)
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(prefs.publicKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
public static PrivateKey getPrivateKey() throws InvalidKeySpecException,
NoSuchAlgorithmException {
return privateKey;
}
/**
* Cleans up old keys in user_data_key table
*/
public static void cleanUp() {
if (privateKey == null)
TAssert.fail("Must call initialize with a password to clean up data");
SQLiteDatabase db = GTG.db;
// this will select only rows that are not from the latest instance of
// each application
// that encrypts data. So if another application such as
// GpsTrailerService is actively
// creating rows, we won't touch it's row. But any key that was
// generated by GpsTrailerService
// before the current one is fair game.
Cursor c = db
.rawQuery(
"select _id, encrypted_data from user_data_key udk where "
+ "udk.app_id != ? and udk._id != "
+ "(select max(_id) from user_data_key udk2 where udk2.app_id = udk.app_id)",
new String[] { String.valueOf(GTG.MASTER_APP_ID) });
Cursor c2 = null;
// TODO 2.5: NOTE :: THIS DOESN'T WORK!!!!!!! where is the while loop for
// c??? It's also not being used
// try {
//
// for(EncryptedRow er : new EncryptedRow [] { allocateGpsLocationRow(),
// allocateUserLocationRow(),
// allocateAreaPanel(), allocateTimeTree(), allocateMediaLocTime()})
// {
// c2 = er.query(db, "user_data_key_fk = ?",c.getString(0));
//
// while(c2.moveToNext())
// {
// //this will read the row using it's key and update it using
// //our key (the master key)
// er.readRow(c2);
// er.updateRow2(db);
// }
//
// c2.close();
// }
//
// SQLiteStatement s = DbUtil.createOrGetStatement(db,
// "delete from user_data_key where _id = ?");
// s.bindLong(1, c.getLong(0));
// }
// finally
// {
// DbUtil.closeCursors(c, c2);
// }
}
public static class Preferences implements AndroidPreferences {
/**
* True if the salt, password, public and private keys, etc. are filled
* out and encryption is ready to go
*/
public boolean initialWorkPerformed;
/**
* True if there is no password (in which case we encrypt using "")
* (saved to db)
*/
public boolean isNoPassword;
/**
* The salt for you know, well if you don't, look it up. (saved to db)
*/
public byte[] salt;
/**
* Used for encrypting an AES key used to encrypt sensitive user data
* (such as the location points and saved user data) (saved to db)
*/
public byte[] publicKey;
/**
* Used for decrypting tge AES key for sensitive user data (such as the
* location points and saved user data) (saved to db)
*/
public byte[] encryptedPrivateKey;
public int aesKeySize = calcMaxKeySize();
}
public static int calcMaxKeySize()
{
//co: this returns Integer.MAX_VALUE
// System.out.println("Allowed Key Length: "
// + cipher.getMaxAllowedKeyLength("AES"));
int [] keySizes = new int [] { 256, 192, 128 };
for(int keySize : keySizes)
{
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(INTERNAL_SYMMETRIC_ENCRYPTION_NAME);
keyGenerator.init(keySize);
SecretKey key = keyGenerator.generateKey();
Cipher cipher = Cipher.getInstance(INTERNAL_SYMMETRIC_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
}
catch(Exception e)
{
Log.d(GTG.TAG, "can't use keysize "+keySize+": "+e);
continue;
}
return keySize;
}
throw new IllegalStateException("can't find a good keysize");
}
public int getDecryptedSize(int length) {
return crypt.getNumOutputBytesForDecryption(length);
}
public boolean canDecrypt() {
return privateKey != null;
}
//TODO 3.5 we can probably get rid of these and just use constructors
public static GpsLocationRow allocateGpsLocationRow() {
return new GpsLocationRow();
}
public static UserLocationRow allocateUserLocationRow() {
return new UserLocationRow();
}
public static AreaPanel allocateAreaPanel() {
return new AreaPanel();
}
public static TimeTree allocateTimeTree() {
return new TimeTree();
}
public static MediaLocTime allocateMediaLocTime() {
return new MediaLocTime();
}
public static TimeZoneTimeRow allocateTztRow() {
return new TimeZoneTimeRow();
}
/**
* Uses private key directly to encrypt the data. Note that this
* is a slow method and not meant for high performance. See
* crypt.encryptData() for an alternative
*/
public byte[] encryptDataWithPrivateKey(byte[] buffer, int length) {
try {
Cipher cipher = Cipher.getInstance(TTT_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] cipherData = cipher.doFinal(buffer,0, length);
return cipherData;
} catch (GeneralSecurityException e) {
throw new IllegalStateException(e);
}
}
public byte [] decryptDataWithPrivateKey(byte[] data) {
try {
Cipher cipher = Cipher.getInstance(TTT_ENCRYPTION_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] cipherData = cipher.doFinal(data);
return cipherData;
} catch (GeneralSecurityException e) {
throw new IllegalStateException(e);
}
}
public byte[] encryptDataWithPrivateKey(byte[] bytes) {
return encryptDataWithPrivateKey(bytes, bytes.length);
}
}