diff --git a/.circleci/config.yml b/.circleci/config.yml index 387c5edd..c35ceda4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -124,7 +124,7 @@ jobs: - *attach_workspace - run: name: Setup emulator - command: sdkmanager "system-images;android-16;default;armeabi-v7a" && echo "no" | avdmanager create avd -n test -k "system-images;android-16;default;armeabi-v7a" + command: sdkmanager "system-images;android-19;default;armeabi-v7a" && echo "no" | avdmanager create avd -n test -k "system-images;android-19;default;armeabi-v7a" - run: name: Launch emulator command: export LD_LIBRARY_PATH=${ANDROID_HOME}/emulator/lib64:${ANDROID_HOME}/emulator/lib64/qt/lib && emulator64-arm -avd test -noaudio -no-boot-anim -no-window -accel on @@ -140,12 +140,9 @@ jobs: # unlock the emulator screen sleep 30 adb shell input keyevent 82 - - run: - name: Clean project - command: ./gradlew clean --no-daemon --stacktrace --console=plain -PdisablePreDex - run: name: Run instrumented tests - command: ./gradlew createDebugCoverageReport --no-daemon --stacktrace --console=plain -PdisablePreDex + command: ./gradlew clean createDebugCoverageReport jacocoTestReport --no-daemon --stacktrace --console=plain -PdisablePreDex - run: name: Collect logs from emulator command: adb logcat -d > ./app/build/reports/logcat_emulator.txt diff --git a/app/build.gradle b/app/build.gradle index 77934331..7d5ec8a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,4 +141,5 @@ dependencies { androidTestImplementation "com.android.support.test:runner:$testRunner" androidTestImplementation "org.mockito:mockito-android:$mockito" + androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } diff --git a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.java b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.java deleted file mode 100644 index 2fd1904b..00000000 --- a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.wulkanowy.utils.security; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.filters.SdkSuppress; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class ScramblerTest { - - @Test - @SdkSuppress(minSdkVersion = 18) - public void encryptDecryptTest() throws Exception { - Context targetContext = InstrumentationRegistry.getTargetContext(); - - Assert.assertEquals("PASS", Scrambler.decrypt("TEST", - Scrambler.encrypt("TEST", "PASS", targetContext))); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt new file mode 100644 index 00000000..162008de --- /dev/null +++ b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.utils.security + +import android.support.test.InstrumentationRegistry +import android.support.test.filters.SdkSuppress +import android.support.test.filters.SmallTest +import android.support.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import java.security.KeyStore +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ScramblerTest { + + @Test + fun encryptDecryptTest() { + assertEquals("TEST", Scrambler.decrypt(Scrambler.encrypt("TEST", + InstrumentationRegistry.getTargetContext()))) + } + + @Test + fun emptyTextEncryptTest() { + assertFailsWith { + Scrambler.decrypt("") + } + + assertFailsWith { + Scrambler.encrypt("", InstrumentationRegistry.getTargetContext()) + } + } + + @Test + @SdkSuppress(minSdkVersion = 18) + fun emptyKeyStoreTest() { + val text = Scrambler.encrypt("test", InstrumentationRegistry.getTargetContext()) + + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.deleteEntry("USER_PASSWORD") + + assertFailsWith { + Scrambler.decrypt(text) + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dd982626..59963c45 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ @@ -36,8 +35,7 @@ android:name=".ui.main.MainActivity" android:configChanges="orientation|screenSize" android:label="@string/activity_dashboard_text" - android:launchMode="singleTop" - /> + android:launchMode="singleTop" /> diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.java b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.java deleted file mode 100644 index d3570512..00000000 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.github.wulkanowy; - -import com.crashlytics.android.Crashlytics; -import com.crashlytics.android.answers.Answers; -import com.crashlytics.android.core.CrashlyticsCore; -import com.jakewharton.threetenabp.AndroidThreeTen; - -import org.greenrobot.greendao.query.QueryBuilder; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.support.DaggerApplication; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import io.fabric.sdk.android.Fabric; -import io.github.wulkanowy.data.RepositoryContract; -import io.github.wulkanowy.di.DaggerAppComponent; -import io.github.wulkanowy.utils.FabricUtils; -import io.github.wulkanowy.utils.LoggerUtils; -import timber.log.Timber; - -public class WulkanowyApp extends DaggerApplication { - - @Inject - RepositoryContract repository; - - @Override - public void onCreate() { - super.onCreate(); - AndroidThreeTen.init(this); - - if (BuildConfig.DEBUG) { - enableDebugLog(); - } - initializeFabric(); - initializeUserSession(); - } - - private void initializeUserSession() { - if (repository.getSharedRepo().isUserLoggedIn()) { - try { - repository.getSyncRepo().initLastUser(); - FabricUtils.logLogin("Open app", true); - } catch (Exception e) { - FabricUtils.logLogin("Open app", false); - Timber.e(e, "An error occurred when the application was started"); - } - } - } - - private void enableDebugLog() { - QueryBuilder.LOG_VALUES = true; - FlexibleAdapter.enableLogs(eu.davidea.flexibleadapter.utils.Log.Level.DEBUG); - Timber.plant(new LoggerUtils.DebugLogTree()); - } - - private void initializeFabric() { - Fabric.with(new Fabric.Builder(this) - .kits( - new Crashlytics.Builder() - .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) - .build(), - new Answers() - ) - .debuggable(BuildConfig.DEBUG) - .build()); - Timber.plant(new LoggerUtils.CrashlyticsTree()); - } - - @Override - protected AndroidInjector applicationInjector() { - return DaggerAppComponent.builder().create(this); - } -} diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt new file mode 100644 index 00000000..3e440ef1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -0,0 +1,74 @@ +package io.github.wulkanowy + +import com.crashlytics.android.Crashlytics +import com.crashlytics.android.answers.Answers +import com.crashlytics.android.core.CrashlyticsCore +import com.jakewharton.threetenabp.AndroidThreeTen +import dagger.android.AndroidInjector +import dagger.android.support.DaggerApplication +import eu.davidea.flexibleadapter.FlexibleAdapter +import io.fabric.sdk.android.Fabric +import io.github.wulkanowy.data.RepositoryContract +import io.github.wulkanowy.di.DaggerAppComponent +import io.github.wulkanowy.utils.FabricUtils +import io.github.wulkanowy.utils.LoggerUtils +import io.github.wulkanowy.utils.security.ScramblerException +import org.greenrobot.greendao.query.QueryBuilder +import timber.log.Timber +import javax.inject.Inject + +class WulkanowyApp : DaggerApplication() { + + @Inject + internal lateinit var repository: RepositoryContract + + override fun onCreate() { + super.onCreate() + AndroidThreeTen.init(this) + + if (BuildConfig.DEBUG) { + enableDebugLog() + } + initializeFabric() + initializeUserSession() + } + + private fun initializeUserSession() { + if (repository.sharedRepo.isUserLoggedIn) { + try { + repository.syncRepo.initLastUser() + FabricUtils.logLogin("Open app", true) + } catch (e: Exception) { + FabricUtils.logLogin("Open app", false) + Timber.e(e, "An error occurred when the application was started") + } catch (e: ScramblerException) { + FabricUtils.logLogin("Open app", false) + Timber.e(e, "A security error has occurred") + repository.cleanAllData() + } + + } + } + + private fun enableDebugLog() { + QueryBuilder.LOG_VALUES = true + FlexibleAdapter.enableLogs(eu.davidea.flexibleadapter.utils.Log.Level.DEBUG) + Timber.plant(LoggerUtils.DebugLogTree()) + } + + private fun initializeFabric() { + Fabric.with(Fabric.Builder(this) + .kits( + Crashlytics.Builder() + .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) + .build(), + Answers() + ) + .debuggable(BuildConfig.DEBUG) + .build()) + Timber.plant(LoggerUtils.CrashlyticsTree()) + } + + override fun applicationInjector(): AndroidInjector = + DaggerAppComponent.builder().create(this) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/migrations/Migration23.java b/app/src/main/java/io/github/wulkanowy/data/db/dao/migrations/Migration23.java index b2a41ad1..519d9153 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/migrations/Migration23.java +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/migrations/Migration23.java @@ -30,7 +30,7 @@ public class Migration23 implements DbHelper.Migration { final Map user = getAccountData(db); vulcan.setCredentials( user.get("email"), - Scrambler.decrypt(user.get("email"), user.get("password")), + Scrambler.decrypt(user.get("password")), user.get("symbol"), user.get("school_id"), "", // inserted in code bellow diff --git a/app/src/main/java/io/github/wulkanowy/data/db/resources/ResourcesRepository.java b/app/src/main/java/io/github/wulkanowy/data/db/resources/ResourcesRepository.java index f8a1baaa..1151e797 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/resources/ResourcesRepository.java +++ b/app/src/main/java/io/github/wulkanowy/data/db/resources/ResourcesRepository.java @@ -14,7 +14,6 @@ import io.github.wulkanowy.R; import io.github.wulkanowy.api.NotLoggedInErrorException; import io.github.wulkanowy.data.db.dao.entities.AttendanceLesson; import io.github.wulkanowy.utils.AppConstant; -import io.github.wulkanowy.utils.security.CryptoException; import timber.log.Timber; @Singleton @@ -39,11 +38,9 @@ public class ResourcesRepository implements ResourcesContract { @Override public String getErrorLoginMessage(Exception exception) { - Timber.e(exception,"%s encountered a error", AppConstant.APP_NAME); + Timber.e(exception, "%s encountered a error", AppConstant.APP_NAME); - if (exception instanceof CryptoException) { - return resources.getString(R.string.encrypt_failed_text); - } else if (exception instanceof UnknownHostException) { + if (exception instanceof UnknownHostException) { return resources.getString(R.string.noInternet_text); } else if (exception instanceof SocketTimeoutException) { return resources.getString(R.string.generic_timeout_error); diff --git a/app/src/main/java/io/github/wulkanowy/data/sync/AccountSync.java b/app/src/main/java/io/github/wulkanowy/data/sync/AccountSync.java index 45f8c1f5..cb51c1bb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/sync/AccountSync.java +++ b/app/src/main/java/io/github/wulkanowy/data/sync/AccountSync.java @@ -26,8 +26,8 @@ import io.github.wulkanowy.data.db.dao.entities.Symbol; import io.github.wulkanowy.data.db.dao.entities.SymbolDao; import io.github.wulkanowy.data.db.shared.SharedPrefContract; import io.github.wulkanowy.utils.DataObjectConverter; -import io.github.wulkanowy.utils.security.CryptoException; import io.github.wulkanowy.utils.security.Scrambler; +import io.github.wulkanowy.utils.security.ScramblerException; import timber.log.Timber; @Singleton @@ -51,7 +51,7 @@ public class AccountSync { } public void registerUser(String email, String password, String symbol) - throws VulcanException, IOException, CryptoException { + throws VulcanException, IOException, ScramblerException { clearUserData(); @@ -79,11 +79,11 @@ public class AccountSync { Timber.i("Register end"); } - private Account insertAccount(String email, String password) throws CryptoException { + private Account insertAccount(String email, String password) throws ScramblerException { Timber.d("Register account"); Account account = new Account() .setEmail(email) - .setPassword(Scrambler.encrypt(email, password, context)); + .setPassword(Scrambler.encrypt(password, context)); daoSession.getAccountDao().insert(account); return account; } @@ -150,7 +150,7 @@ public class AccountSync { daoSession.getSemesterDao().insertInTx(semesterList); } - public void initLastUser() throws CryptoException { + public void initLastUser() throws ScramblerException { long userId = sharedPref.getCurrentUserId(); @@ -180,7 +180,7 @@ public class AccountSync { vulcan.setCredentials( account.getEmail(), - Scrambler.decrypt(account.getEmail(), account.getPassword()), + Scrambler.decrypt(account.getPassword()), symbol.getSymbol(), school.getRealId(), student.getRealId(), diff --git a/app/src/main/java/io/github/wulkanowy/data/sync/SyncContract.java b/app/src/main/java/io/github/wulkanowy/data/sync/SyncContract.java index 67797ad1..6f4df657 100644 --- a/app/src/main/java/io/github/wulkanowy/data/sync/SyncContract.java +++ b/app/src/main/java/io/github/wulkanowy/data/sync/SyncContract.java @@ -6,15 +6,16 @@ import java.text.ParseException; import javax.inject.Singleton; import io.github.wulkanowy.api.VulcanException; -import io.github.wulkanowy.utils.security.CryptoException; +import io.github.wulkanowy.utils.security.ScramblerException; @Singleton public interface SyncContract { void registerUser(String email, String password, String symbol) throws VulcanException, - IOException, CryptoException; + IOException, ScramblerException; + + void initLastUser() throws IOException, ScramblerException; - void initLastUser() throws IOException, CryptoException; void syncGrades(int semesterName) throws VulcanException, IOException, ParseException; diff --git a/app/src/main/java/io/github/wulkanowy/data/sync/SyncRepository.java b/app/src/main/java/io/github/wulkanowy/data/sync/SyncRepository.java index 05dd14fd..eb6b4454 100644 --- a/app/src/main/java/io/github/wulkanowy/data/sync/SyncRepository.java +++ b/app/src/main/java/io/github/wulkanowy/data/sync/SyncRepository.java @@ -7,7 +7,7 @@ import javax.inject.Singleton; import io.github.wulkanowy.api.VulcanException; import io.github.wulkanowy.data.db.dao.DbContract; -import io.github.wulkanowy.utils.security.CryptoException; +import io.github.wulkanowy.utils.security.ScramblerException; @Singleton public class SyncRepository implements SyncContract { @@ -41,12 +41,12 @@ public class SyncRepository implements SyncContract { @Override public void registerUser(String email, String password, String symbol) throws VulcanException, - IOException, CryptoException { + IOException, ScramblerException { accountSync.registerUser(email, password, symbol); } @Override - public void initLastUser() throws CryptoException { + public void initLastUser() throws ScramblerException { accountSync.initLastUser(); } diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/CryptoException.java b/app/src/main/java/io/github/wulkanowy/utils/security/CryptoException.java deleted file mode 100644 index 1ee4a9fa..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/security/CryptoException.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.wulkanowy.utils.security; - - -public class CryptoException extends Exception { - - public CryptoException(String message) { - super(message); - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.java b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.java deleted file mode 100644 index 8e425a89..00000000 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.github.wulkanowy.utils.security; - - -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.security.KeyPairGeneratorSpec; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; -import android.util.Base64; - -import org.apache.commons.lang3.ArrayUtils; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.math.BigInteger; -import java.security.KeyPairGenerator; -import java.security.KeyStore; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.AlgorithmParameterSpec; -import java.util.ArrayList; -import java.util.Calendar; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.security.auth.x500.X500Principal; - -import timber.log.Timber; - -public final class Scrambler { - - private static final String ANDROID_KEYSTORE = "AndroidKeyStore"; - - private static KeyStore keyStore; - - private Scrambler() { - throw new IllegalStateException("Utility class"); - } - - public static String encrypt(String email, String plainText, Context context) - throws CryptoException { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - loadKeyStore(); - generateNewKey(email, context); - return encryptString(email, plainText); - } - return new String(Base64.encode(plainText.getBytes(), Base64.DEFAULT)); - } - - public static String decrypt(String email, String encryptedText) throws CryptoException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - loadKeyStore(); - return decryptString(email, encryptedText); - } - return new String(Base64.decode(encryptedText, Base64.DEFAULT)); - } - - private static void loadKeyStore() throws CryptoException { - try { - keyStore = KeyStore.getInstance(ANDROID_KEYSTORE); - keyStore.load(null); - } catch (Exception e) { - throw new CryptoException(e.getMessage()); - } - } - - @SuppressWarnings("deprecation") - @TargetApi(18) - private static void generateNewKey(String alias, Context context) throws CryptoException { - - Calendar start = Calendar.getInstance(); - Calendar end = Calendar.getInstance(); - - AlgorithmParameterSpec spec; - - end.add(Calendar.YEAR, 10); - if (!"".equals(alias)) { - try { - if (!keyStore.containsAlias(alias)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - spec = new KeyGenParameterSpec.Builder(alias, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - .setDigests(KeyProperties.DIGEST_SHA256) - .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) - .setCertificateNotBefore(start.getTime()) - .setCertificateNotAfter(end.getTime()) - .build(); - - } else { - spec = new KeyPairGeneratorSpec.Builder(context) - .setAlias(alias) - .setSubject(new X500Principal("CN=" + alias)) - .setSerialNumber(BigInteger.TEN) - .setStartDate(start.getTime()) - .setEndDate(end.getTime()) - .build(); - } - - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", - ANDROID_KEYSTORE); - keyPairGenerator.initialize(spec); - keyPairGenerator.generateKeyPair(); - } - } catch (Exception e) { - throw new CryptoException(e.getMessage()); - } - } else { - throw new CryptoException("GenerateNewKey - String is empty"); - } - - Timber.d("Key pair are create"); - - } - - private static String encryptString(String alias, String text) throws CryptoException { - - if (!alias.isEmpty() && !text.isEmpty()) { - try { - KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); - RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey(); - - Cipher input = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - input.init(Cipher.ENCRYPT_MODE, publicKey); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - CipherOutputStream cipherOutputStream = new CipherOutputStream( - outputStream, input); - cipherOutputStream.write(text.getBytes("UTF-8")); - cipherOutputStream.close(); - - byte[] values = outputStream.toByteArray(); - - return Base64.encodeToString(values, Base64.DEFAULT); - - } catch (Exception e) { - throw new CryptoException(e.getMessage()); - } - } else { - throw new CryptoException("EncryptString - String is empty"); - } - } - - private static String decryptString(String alias, String text) throws CryptoException { - - if (!alias.isEmpty() && !text.isEmpty()) { - try { - KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); - - Cipher output = Cipher.getInstance("RSA/ECB/PKCS1Padding"); - output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey()); - - CipherInputStream cipherInputStream = new CipherInputStream( - new ByteArrayInputStream(Base64.decode(text, Base64.DEFAULT)), output); - - ArrayList values = new ArrayList<>(); - - int nextByte; - - while ((nextByte = cipherInputStream.read()) != -1) { - values.add((byte) nextByte); - } - - Byte[] bytes = values.toArray(new Byte[values.size()]); - - return new String(ArrayUtils.toPrimitive(bytes), 0, bytes.length, "UTF-8"); - } catch (Exception e) { - throw new CryptoException(e.getMessage()); - } - } else { - throw new CryptoException("EncryptString - String is empty"); - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt new file mode 100644 index 00000000..050f474f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -0,0 +1,172 @@ +@file:Suppress("DEPRECATION") + +package io.github.wulkanowy.utils.security + +import android.annotation.TargetApi +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 +import android.os.Build.VERSION_CODES.M +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties.* +import android.util.Base64 +import android.util.Base64.DEFAULT +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.nio.charset.Charset +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.PublicKey +import java.util.* +import javax.crypto.Cipher +import javax.crypto.Cipher.DECRYPT_MODE +import javax.crypto.Cipher.ENCRYPT_MODE +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.security.auth.x500.X500Principal +import kotlin.collections.ArrayList + +object Scrambler { + + private const val KEY_ALIAS = "USER_PASSWORD" + + private const val ALGORITHM_RSA = "RSA" + + private const val KEYSTORE_NAME = "AndroidKeyStore" + + private const val KEY_TRANSFORMATION_ALGORITHM = "RSA/ECB/PKCS1Padding" + + private const val KEY_CIPHER_JELLY_PROVIDER = "AndroidOpenSSL" + + private const val KEY_CIPHER_M_PROVIDER = "AndroidKeyStoreBCWorkaround" + + private val KEY_CHARSET = Charset.forName("UTF-8") + + @JvmStatic + fun encrypt(plainText: String, context: Context): String { + if (StringUtils.isEmpty(plainText)) { + throw ScramblerException("Text to be encrypted is empty") + } + + if (SDK_INT < JELLY_BEAN_MR2) { + return String(Base64.encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + } + + try { + if (!isKeyPairExist()) { + generateKeyPair(context) + } + + val cipher = getCipher() + cipher.init(ENCRYPT_MODE, getPublicKey()) + + val outputStream = ByteArrayOutputStream() + val cipherOutputStream = CipherOutputStream(outputStream, cipher) + cipherOutputStream.write(plainText.toByteArray(KEY_CHARSET)) + cipherOutputStream.close() + + return Base64.encodeToString(outputStream.toByteArray(), DEFAULT) + } catch (e: Exception) { + throw ScramblerException("An error occurred while encrypting text", e) + } + + } + + @JvmStatic + fun decrypt(cipherText: String): String { + if (StringUtils.isEmpty(cipherText)) { + throw ScramblerException("Text to be encrypted is empty") + } + + if (SDK_INT < JELLY_BEAN_MR2) { + return String(Base64.decode(cipherText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + } + + if (!isKeyPairExist()) { + throw ScramblerException("KeyPair doesn't exist") + } + + try { + val cipher = getCipher() + cipher.init(DECRYPT_MODE, getPrivateKey()) + + val input = CipherInputStream(ByteArrayInputStream(Base64.decode(cipherText, DEFAULT)), cipher) + val values = ArrayList() + + var nextByte = 0 + while ({ nextByte = input.read(); nextByte }() != -1) { + values.add(nextByte.toByte()) + } + + val bytes = ByteArray(values.size) + for (i in bytes.indices) { + bytes[i] = values[i] + } + return String(bytes, 0, bytes.size, KEY_CHARSET) + } catch (e: Exception) { + throw ScramblerException("An error occurred while decrypting text", e) + } + + } + + private fun getKeyStoreInstance(): KeyStore { + val keyStore = KeyStore.getInstance(KEYSTORE_NAME) + keyStore.load(null) + return keyStore + } + + private fun getPublicKey(): PublicKey = + (getKeyStoreInstance().getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry) + .certificate.publicKey + + + private fun getPrivateKey(): PrivateKey = + (getKeyStoreInstance().getEntry(KEY_ALIAS, null) as KeyStore.PrivateKeyEntry).privateKey + + + private fun getCipher(): Cipher { + if (SDK_INT >= M) { + return Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_M_PROVIDER) + } + + return Cipher.getInstance(KEY_TRANSFORMATION_ALGORITHM, KEY_CIPHER_JELLY_PROVIDER) + } + + @TargetApi(JELLY_BEAN_MR2) + private fun generateKeyPair(context: Context) { + val spec = if (SDK_INT >= M) { + KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) + .setDigests(DIGEST_SHA256, DIGEST_SHA512) + .setCertificateSubject(X500Principal("CN=Wulkanowy")) + .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1) + .setSignaturePaddings(SIGNATURE_PADDING_RSA_PKCS1) + .setCertificateSerialNumber(BigInteger.TEN) + .build() + } else { + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 99) + + KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setSubject(X500Principal("CN=Wulkanowy")) + .setSerialNumber(BigInteger.TEN) + .setStartDate(start.time) + .setEndDate(end.time) + .build() + } + + val generator = KeyPairGenerator.getInstance(ALGORITHM_RSA, KEYSTORE_NAME) + generator.initialize(spec) + generator.generateKeyPair() + + Timber.i("A new KeyPair has been generated") + } + + private fun isKeyPairExist(): Boolean = getKeyStoreInstance().getKey(KEY_ALIAS, null) != null +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/ScramblerException.kt b/app/src/main/java/io/github/wulkanowy/utils/security/ScramblerException.kt new file mode 100644 index 00000000..59f830fa --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/security/ScramblerException.kt @@ -0,0 +1,6 @@ +package io.github.wulkanowy.utils.security + +class ScramblerException : Exception { + constructor(message: String, cause: Throwable) : super(message, cause) + constructor(message: String) : super(message) +}