From 97100834a5bcb08f2fdf2eb6c580d3ceeb8b6b2f Mon Sep 17 00:00:00 2001 From: steckbrief Date: Sat, 16 Jul 2016 15:11:36 +0200 Subject: Implements FS#227: Store password encrypted in internal database --- build.gradle | 14 ++- .../conversationsplus/entities/Account.java | 8 +- .../persistance/DatabaseBackend.java | 16 ++- .../conversationsplus/utils/SimpleCryptoUtil.java | 110 +++++++++++++++++ .../utils/SimpleCryptoUtilTest.java | 136 +++++++++++++++++++++ 5 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java create mode 100644 src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java diff --git a/build.gradle b/build.gradle index de4d418e..f58ac6df 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ buildscript { repositories { jcenter() - mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:2.0.0' @@ -21,6 +20,9 @@ repositories { flatDir { dirs 'libs/3rdParty/jcenter', 'libs/3rdParty/maven' } + maven { + url "http://snippets.thedevstack.de/mvn" + } } dependencies { @@ -55,6 +57,10 @@ dependencies { // Android dependencies compile 'com.android.support:support-v13:23.2.0' + + testCompile 'junit:junit:4.12' + // Optional -- Mockito framework + testCompile 'org.mockito:mockito-core:1.10.19' } ext { @@ -90,6 +96,12 @@ android { playstore free } + testOptions { + unitTests.all { + // All the usual Gradle options. + jvmArgs '-XX:MaxPermSize=256m' + } + } if (project.hasProperty('mStoreFile') && project.hasProperty('mStorePassword') && project.hasProperty('mKeyAlias') && diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java index bc956364..2ee76504 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -28,6 +28,7 @@ import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceStub; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil; import de.thedevstack.conversationsplus.xmpp.XmppConnection; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -172,6 +173,8 @@ public class Account extends AbstractEntity { private List bookmarks = new CopyOnWriteArrayList<>(); private final Collection blocklist = new CopyOnWriteArraySet<>(); + public static final String PW_SEED = "sadjgdiahsdkhashp3zt98edAFSFIOKZUIUOz23ejj12ezhez2398iehz"; + public Account() { this.uuid = "0"; } @@ -210,9 +213,10 @@ public class Account extends AbstractEntity { cursor.getString(cursor.getColumnIndex(SERVER)), "mobile"); } catch (final InvalidJidException ignored) { } + String password = SimpleCryptoUtil.decrypt(PW_SEED, cursor.getString(cursor.getColumnIndex(PASSWORD))); return new Account(cursor.getString(cursor.getColumnIndex(UUID)), jid, - cursor.getString(cursor.getColumnIndex(PASSWORD)), + password, cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), cursor.getString(cursor.getColumnIndex(KEYS)), @@ -342,7 +346,7 @@ public class Account extends AbstractEntity { values.put(UUID, uuid); values.put(USERNAME, jid.getLocalpart()); values.put(SERVER, jid.getDomainpart()); - values.put(PASSWORD, password); + values.put(PASSWORD, SimpleCryptoUtil.encrypt(PW_SEED, password)); values.put(OPTIONS, options); values.put(KEYS, this.keys.toString()); values.put(ROSTERVERSION, rosterVersion); diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java index 5a5746d5..6442e909 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -44,6 +44,7 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Roster; import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; +import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -54,7 +55,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; private static final int DATABASE_VERSION = 25; private static final int C_TO_CPLUS_VERSION_OFFSET = 1000; - private static final int CPLUS_DATABASE_VERSION = 1; + private static final int CPLUS_DATABASE_VERSION = 2; private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100; private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER); @@ -203,6 +204,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") " + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME); } + if (newVersion == 2) { + Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time"); + Cursor cursor = db.rawQuery("SELECT " + Account.UUID + ", " + Account.PASSWORD + " FROM " + Account.TABLENAME, new String[0]); + while (cursor.moveToNext()) { + String uuid = CursorHelper.getString(cursor, Account.UUID); + String password = CursorHelper.getString(cursor, Account.PASSWORD); + String encryptedPassword = SimpleCryptoUtil.encrypt(Account.PW_SEED, password); + ContentValues values = new ContentValues(); + values.put(Account.PASSWORD, encryptedPassword); + db.update(Account.TABLENAME, values, Account.UUID + "=?", new String[] {uuid}); + } + cursor.close(); + } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java new file mode 100644 index 00000000..0a8c80d1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java @@ -0,0 +1,110 @@ +package de.thedevstack.conversationsplus.utils; + + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * + */ +public final class SimpleCryptoUtil { + + public static String encrypt(String seed, String cleartext) { + String result = null; + if (null != seed && null != cleartext) { + try { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] encryptedBytes = encrypt(rawKey, cleartext.getBytes()); + result = toHex(encryptedBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) { + // FIXME + } + } + + return result; + } + + public static String decrypt(String seed, String encrypted) { + String result = null; + if (null != seed && null != encrypted) { + try { + byte[] rawKey = getRawKey(seed.getBytes()); + byte[] enc = toByte(encrypted); + byte[] decryptedBytes = decrypt(rawKey, enc); + result = new String(decryptedBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) { + // FIXME + } + } + + return result; + } + + private static byte[] encrypt(byte[] raw, byte[] clear) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { + byte[] encrypted = doCipherOperation(raw, clear, Cipher.ENCRYPT_MODE); + return encrypted; + } + + private static byte[] decrypt(byte[] raw, byte[] encrypted) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + byte[] decrypted = doCipherOperation(raw, encrypted, Cipher.DECRYPT_MODE); + return decrypted; + } + + private static byte[] doCipherOperation(byte[] raw, byte[] input, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(mode, skeySpec); + + return cipher.doFinal(input); + } + + private static byte[] getRawKey(byte[] seed) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] md5Bytes = md.digest(seed); // 128 Bit = 16 byte + SecretKey skey = new SecretKeySpec(md5Bytes, "AES"); + byte[] raw = skey.getEncoded(); + return raw; + } + + public static byte[] toByte(String hexString) { + int len = hexString.length() / 2; + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) { + result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue(); + } + return result; + } + + public static String toHex(byte[] buf) { + if (buf == null) { + return null; + } + StringBuffer result = new StringBuffer(2 * buf.length); + for (int i = 0; i < buf.length; i++) { + appendHex(result, buf[i]); + } + return result.toString(); + } + + private final static String HEX = "0123456789ABCDEF"; + + private static void appendHex(StringBuffer sb, byte b) { + sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f)); + } + + /** + * private constructor to avoid instantiation + */ + private SimpleCryptoUtil() { + // private constructor to avoid instantiation + } +} + diff --git a/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java b/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java new file mode 100644 index 00000000..7e71a3bf --- /dev/null +++ b/src/test/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtilTest.java @@ -0,0 +1,136 @@ +package de.thedevstack.conversationsplus.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Created by steckbrief on 10.07.2016. + */ +public class SimpleCryptoUtilTest { + @Test + public void testEncryptWithNullSeed() { + String seed = null; + String cleartext = "Test123"; + String expectedResult = null; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithWhitespaceSeed() { + String seed = " "; + String cleartext = "Test123"; + String expectedResult = "3E7721EF6F8D9E47403AC03F45341E5C"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithZeroLengthSeed() { + String seed = ""; + String cleartext = "Test123"; + String expectedResult = "BEFF4A6800B91AE505672CA238D7C72F"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithNullCleartext() { + String seed = "asdasdadasd"; + String cleartext = null; + String expectedResult = null; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithWhitespaceCleartext() { + String seed = "asdasdadasd"; + String cleartext = " "; + String expectedResult = "1C476E5E4E66809AB6C91EEF9C93A32F"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncryptWithZeroLengthCleartext() { + String seed = "asdasdadasd"; + String cleartext = ""; + String expectedResult = "22BD1DA9232CA6594A466B419B2AFB8C"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testEncrypt() { + String seed = "asdasdadasd"; + String cleartext = "Test123"; + String expectedResult = "E1D9C96AAF09434D915A2F0C147CBF64"; + String result = SimpleCryptoUtil.encrypt(seed, cleartext); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithNullSeed() { + String seed = null; + String encryptedText = "E1D9C96AAF09434D915A2F0C147CBF64"; + String expectedResult = null; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithWhitespaceSeed() { + String seed = " "; + String encryptedText = "3E7721EF6F8D9E47403AC03F45341E5C"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithZeroLengthSeed() { + String seed = ""; + String encryptedText = "BEFF4A6800B91AE505672CA238D7C72F"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithNullEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = null; + String expectedResult = null; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithWhitespaceEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = " "; + String expectedResult = ""; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecryptWithZeroLengthEncryptedText() { + String seed = "asdasdadasd"; + String encryptedText = ""; + String expectedResult = ""; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } + + @Test + public void testDecrypt() { + String seed = "asdasdadasd"; + String encryptedText = "E1D9C96AAF09434D915A2F0C147CBF64"; + String expectedResult = "Test123"; + String result = SimpleCryptoUtil.decrypt(seed, encryptedText); + assertEquals(expectedResult, result); + } +} -- cgit v1.2.3