forked from github/wulkanowy-mirror
Refactoring password encryption (#147)
This commit is contained in:
parent
5dcd4f9b72
commit
ef5d3aead9
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="io.github.wulkanowy"
|
||||
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -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" />
|
||||
<activity
|
||||
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
|
||||
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);
|
||||
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
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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…
Reference in New Issue
Block a user