mirror of
https://github.com/wulkanowy/wulkanowy.git
synced 2025-01-18 17:46:44 -06:00
Refactoring password encryption (#147)
This commit is contained in:
parent
5dcd4f9b72
commit
ef5d3aead9
@ -124,7 +124,7 @@ jobs:
|
|||||||
- *attach_workspace
|
- *attach_workspace
|
||||||
- run:
|
- run:
|
||||||
name: Setup emulator
|
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:
|
- run:
|
||||||
name: Launch emulator
|
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
|
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
|
# unlock the emulator screen
|
||||||
sleep 30
|
sleep 30
|
||||||
adb shell input keyevent 82
|
adb shell input keyevent 82
|
||||||
- run:
|
|
||||||
name: Clean project
|
|
||||||
command: ./gradlew clean --no-daemon --stacktrace --console=plain -PdisablePreDex
|
|
||||||
- run:
|
- run:
|
||||||
name: Run instrumented tests
|
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:
|
- run:
|
||||||
name: Collect logs from emulator
|
name: Collect logs from emulator
|
||||||
command: adb logcat -d > ./app/build/reports/logcat_emulator.txt
|
command: adb logcat -d > ./app/build/reports/logcat_emulator.txt
|
||||||
|
@ -141,4 +141,5 @@ dependencies {
|
|||||||
|
|
||||||
androidTestImplementation "com.android.support.test:runner:$testRunner"
|
androidTestImplementation "com.android.support.test:runner:$testRunner"
|
||||||
androidTestImplementation "org.mockito:mockito-android:$mockito"
|
androidTestImplementation "org.mockito:mockito-android:$mockito"
|
||||||
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
}
|
}
|
||||||
|
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<ScramblerException> {
|
||||||
|
Scrambler.decrypt("")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFailsWith<ScramblerException> {
|
||||||
|
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<ScramblerException> {
|
||||||
|
Scrambler.decrypt(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="io.github.wulkanowy"
|
package="io.github.wulkanowy"
|
||||||
|
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@ -36,8 +35,7 @@
|
|||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:label="@string/activity_dashboard_text"
|
android:label="@string/activity_dashboard_text"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop" />
|
||||||
/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||||
android:theme="@style/WulkanowyTheme.DarkActionBar" />
|
android:theme="@style/WulkanowyTheme.DarkActionBar" />
|
||||||
|
@ -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<? extends DaggerApplication> applicationInjector() {
|
|
||||||
return DaggerAppComponent.builder().create(this);
|
|
||||||
}
|
|
||||||
}
|
|
74
app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt
Normal file
74
app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt
Normal file
@ -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<out DaggerApplication> =
|
||||||
|
DaggerAppComponent.builder().create(this)
|
||||||
|
}
|
@ -30,7 +30,7 @@ public class Migration23 implements DbHelper.Migration {
|
|||||||
final Map<String, String> user = getAccountData(db);
|
final Map<String, String> user = getAccountData(db);
|
||||||
vulcan.setCredentials(
|
vulcan.setCredentials(
|
||||||
user.get("email"),
|
user.get("email"),
|
||||||
Scrambler.decrypt(user.get("email"), user.get("password")),
|
Scrambler.decrypt(user.get("password")),
|
||||||
user.get("symbol"),
|
user.get("symbol"),
|
||||||
user.get("school_id"),
|
user.get("school_id"),
|
||||||
"", // inserted in code bellow
|
"", // inserted in code bellow
|
||||||
|
@ -14,7 +14,6 @@ import io.github.wulkanowy.R;
|
|||||||
import io.github.wulkanowy.api.NotLoggedInErrorException;
|
import io.github.wulkanowy.api.NotLoggedInErrorException;
|
||||||
import io.github.wulkanowy.data.db.dao.entities.AttendanceLesson;
|
import io.github.wulkanowy.data.db.dao.entities.AttendanceLesson;
|
||||||
import io.github.wulkanowy.utils.AppConstant;
|
import io.github.wulkanowy.utils.AppConstant;
|
||||||
import io.github.wulkanowy.utils.security.CryptoException;
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -39,11 +38,9 @@ public class ResourcesRepository implements ResourcesContract {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getErrorLoginMessage(Exception exception) {
|
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) {
|
if (exception instanceof UnknownHostException) {
|
||||||
return resources.getString(R.string.encrypt_failed_text);
|
|
||||||
} else if (exception instanceof UnknownHostException) {
|
|
||||||
return resources.getString(R.string.noInternet_text);
|
return resources.getString(R.string.noInternet_text);
|
||||||
} else if (exception instanceof SocketTimeoutException) {
|
} else if (exception instanceof SocketTimeoutException) {
|
||||||
return resources.getString(R.string.generic_timeout_error);
|
return resources.getString(R.string.generic_timeout_error);
|
||||||
|
@ -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.dao.entities.SymbolDao;
|
||||||
import io.github.wulkanowy.data.db.shared.SharedPrefContract;
|
import io.github.wulkanowy.data.db.shared.SharedPrefContract;
|
||||||
import io.github.wulkanowy.utils.DataObjectConverter;
|
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.Scrambler;
|
||||||
|
import io.github.wulkanowy.utils.security.ScramblerException;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@ -51,7 +51,7 @@ public class AccountSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void registerUser(String email, String password, String symbol)
|
public void registerUser(String email, String password, String symbol)
|
||||||
throws VulcanException, IOException, CryptoException {
|
throws VulcanException, IOException, ScramblerException {
|
||||||
|
|
||||||
clearUserData();
|
clearUserData();
|
||||||
|
|
||||||
@ -79,11 +79,11 @@ public class AccountSync {
|
|||||||
Timber.i("Register end");
|
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");
|
Timber.d("Register account");
|
||||||
Account account = new Account()
|
Account account = new Account()
|
||||||
.setEmail(email)
|
.setEmail(email)
|
||||||
.setPassword(Scrambler.encrypt(email, password, context));
|
.setPassword(Scrambler.encrypt(password, context));
|
||||||
daoSession.getAccountDao().insert(account);
|
daoSession.getAccountDao().insert(account);
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
@ -150,7 +150,7 @@ public class AccountSync {
|
|||||||
daoSession.getSemesterDao().insertInTx(semesterList);
|
daoSession.getSemesterDao().insertInTx(semesterList);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initLastUser() throws CryptoException {
|
public void initLastUser() throws ScramblerException {
|
||||||
|
|
||||||
long userId = sharedPref.getCurrentUserId();
|
long userId = sharedPref.getCurrentUserId();
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ public class AccountSync {
|
|||||||
|
|
||||||
vulcan.setCredentials(
|
vulcan.setCredentials(
|
||||||
account.getEmail(),
|
account.getEmail(),
|
||||||
Scrambler.decrypt(account.getEmail(), account.getPassword()),
|
Scrambler.decrypt(account.getPassword()),
|
||||||
symbol.getSymbol(),
|
symbol.getSymbol(),
|
||||||
school.getRealId(),
|
school.getRealId(),
|
||||||
student.getRealId(),
|
student.getRealId(),
|
||||||
|
@ -6,15 +6,16 @@ import java.text.ParseException;
|
|||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import io.github.wulkanowy.api.VulcanException;
|
import io.github.wulkanowy.api.VulcanException;
|
||||||
import io.github.wulkanowy.utils.security.CryptoException;
|
import io.github.wulkanowy.utils.security.ScramblerException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public interface SyncContract {
|
public interface SyncContract {
|
||||||
|
|
||||||
void registerUser(String email, String password, String symbol) throws VulcanException,
|
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;
|
void syncGrades(int semesterName) throws VulcanException, IOException, ParseException;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import javax.inject.Singleton;
|
|||||||
|
|
||||||
import io.github.wulkanowy.api.VulcanException;
|
import io.github.wulkanowy.api.VulcanException;
|
||||||
import io.github.wulkanowy.data.db.dao.DbContract;
|
import io.github.wulkanowy.data.db.dao.DbContract;
|
||||||
import io.github.wulkanowy.utils.security.CryptoException;
|
import io.github.wulkanowy.utils.security.ScramblerException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class SyncRepository implements SyncContract {
|
public class SyncRepository implements SyncContract {
|
||||||
@ -41,12 +41,12 @@ public class SyncRepository implements SyncContract {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerUser(String email, String password, String symbol) throws VulcanException,
|
public void registerUser(String email, String password, String symbol) throws VulcanException,
|
||||||
IOException, CryptoException {
|
IOException, ScramblerException {
|
||||||
accountSync.registerUser(email, password, symbol);
|
accountSync.registerUser(email, password, symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initLastUser() throws CryptoException {
|
public void initLastUser() throws ScramblerException {
|
||||||
accountSync.initLastUser();
|
accountSync.initLastUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package io.github.wulkanowy.utils.security;
|
|
||||||
|
|
||||||
|
|
||||||
public class CryptoException extends Exception {
|
|
||||||
|
|
||||||
public CryptoException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Byte> 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Byte>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user