diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index d7847c24..d9326ff6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,11 +1,16 @@ package io.github.wulkanowy.data.db.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @Singleton @@ -47,6 +52,9 @@ abstract class StudentDao { @Query("UPDATE Students SET is_current = 0") abstract suspend fun resetCurrent() + @Query("DELETE FROM Students WHERE email = :email AND user_name = :userName") + abstract suspend fun deleteByEmailAndUserName(email: String, userName: String) + @Transaction open suspend fun switchCurrent(id: Long) { resetCurrent() diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 2e04224a..bfad12a8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -1,8 +1,6 @@ package io.github.wulkanowy.data.repositories -import android.content.Context import androidx.room.withTransaction -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -17,20 +15,19 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.security.decrypt -import io.github.wulkanowy.utils.security.encrypt +import io.github.wulkanowy.utils.security.Scrambler import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class StudentRepository @Inject constructor( - @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appDatabase: AppDatabase + private val appDatabase: AppDatabase, + private val scrambler: Scrambler, ) { suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false @@ -68,7 +65,7 @@ class StudentRepository @Inject constructor( student = student.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } }, @@ -86,7 +83,7 @@ class StudentRepository @Inject constructor( }.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } } @@ -96,7 +93,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -107,7 +104,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -120,7 +117,7 @@ class StudentRepository @Inject constructor( it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { - encrypt(password, context) + scrambler.encrypt(password) } } } @@ -166,4 +163,15 @@ class StudentRepository @Inject constructor( studentDb.update(studentName) } + + suspend fun deleteStudentsAssociatedWithAccount(student: Student) { + studentDb.deleteByEmailAndUserName(student.email, student.userName) + } + + suspend fun clearAll() { + withContext(dispatchers.io) { + scrambler.clearKeyPair() + appDatabase.clearAllTables() + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index f622209a..026d38de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -68,11 +68,20 @@ abstract class BaseActivity, VB : ViewBinding> : } else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { + override fun showExpiredCredentialsDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_expired_credentials_title) + .setMessage(R.string.main_expired_credentials_description) + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + override fun showDecryptionFailedDialog() { MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) .setMessage(R.string.main_session_relogin) - .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 84540b1c..50e4b05d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -28,8 +28,12 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView Toast.makeText(context, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun openClearLoginView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index b25346a7..cec2670b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -39,8 +39,12 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showAuthDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 2d913103..ee92e4fc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -28,20 +28,37 @@ open class BasePresenter( this.view = view errorHandler.apply { showErrorMessage = view::showError - onSessionExpired = view::showExpiredDialog + onExpiredCredentials = view::showExpiredCredentialsDialog + onDecryptionFailed = view::showDecryptionFailedDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar onAuthorizationRequired = view::showAuthDialog } } - fun onExpiredLoginSelected() { - Timber.i("Attempt to switch the student after the session expires") + fun onConfirmDecryptionFailedSelected() { + Timber.i("Attempt to clear all data") + + presenterScope.launch { + runCatching { studentRepository.clearAll() } + .onFailure { + Timber.i("Clear data result: An exception occurred") + errorHandler.dispatch(it) + } + .onSuccess { + Timber.i("Clear data result: Open login view") + view?.openClearLoginView() + } + } + } + + fun onConfirmExpiredCredentialsSelected() { + Timber.i("Attempt to delete students associated with the account and switch to new student") presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(false) - studentRepository.logoutStudent(student) + studentRepository.deleteStudentsAssociatedWithAccount(student) val students = studentRepository.getSavedStudents(false) if (students.isNotEmpty()) { @@ -50,11 +67,11 @@ open class BasePresenter( } } .onFailure { - Timber.i("Switch student result: An exception occurred") + Timber.i("Delete students result: An exception occurred") errorHandler.dispatch(it) } .onSuccess { - Timber.i("Switch student result: Open login view") + Timber.i("Delete students result: Open login view") view?.openClearLoginView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index b31737e2..e97a6ab9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -6,7 +6,9 @@ interface BaseView { fun showMessage(text: String) - fun showExpiredDialog() + fun showExpiredCredentialsDialog() + + fun showDecryptionFailedDialog() fun showAuthDialog() diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index 0a41a47b..56905709 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -15,7 +15,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } - var onSessionExpired: () -> Unit = {} + var onExpiredCredentials: () -> Unit = {} + + var onDecryptionFailed: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {} @@ -32,7 +34,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co showErrorMessage(context.resources.getErrorString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) - is ScramblerException, is BadCredentialsException -> onSessionExpired() + is ScramblerException -> onDecryptionFailed() + is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() is AuthorizationRequiredException -> onAuthorizationRequired() } @@ -40,7 +43,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co open fun clear() { showErrorMessage = { _, _ -> } - onSessionExpired = {} + onExpiredCredentials = {} + onDecryptionFailed = {} onNoCurrentStudent = {} onPasswordChangeRequired = {} onAuthorizationRequired = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index ce17c763..301262a0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -30,7 +30,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.getErrorString +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 21f56498..19c4ef6b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -24,7 +24,9 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showMessage(text: String) {} - override fun showExpiredDialog() {} + override fun showExpiredCredentialsDialog() {} + + override fun showDecryptionFailedDialog() {} override fun openClearLoginView() {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 1b8d1a8f..256b1337 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -47,8 +47,12 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 70dd694c..20423eb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -63,8 +63,12 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index af4c4e6a..2ae983c2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -133,8 +133,12 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index f48abe9b..133b1ff4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -84,8 +84,12 @@ class SyncFragment : PreferenceFragmentCompat(), } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { 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 index c994ebab..db16a256 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT import android.util.Base64.decode import android.util.Base64.encode import android.util.Base64.encodeToString +import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.PSource.PSpecified +import javax.inject.Inject +import javax.inject.Singleton import javax.security.auth.x500.X500Principal -private const val KEYSTORE_NAME = "AndroidKeyStore" +@Singleton +class Scrambler @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val keyCharset = Charset.forName("UTF-8") -private const val KEY_ALIAS = "wulkanowy_password" + private val isKeyPairExists: Boolean + get() = keyStore.getKey(KEY_ALIAS, null) != null -private val KEY_CHARSET = Charset.forName("UTF-8") + private val keyStore: KeyStore + get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } -private val isKeyPairExists: Boolean - get() = keyStore.getKey(KEY_ALIAS, null) != null + private val cipher: Cipher + get() { + return if (SDK_INT >= M) Cipher.getInstance( + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + "AndroidKeyStoreBCWorkaround" + ) + else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + } -private val keyStore: KeyStore - get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } + fun encrypt(plainText: String): String { + if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") -private val cipher: Cipher - get() { - return if (SDK_INT >= M) Cipher.getInstance( - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", - "AndroidKeyStoreBCWorkaround" - ) - else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + return try { + if (!isKeyPairExists) generateKeyPair() + + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + } + } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + + ByteArrayOutputStream().let { output -> + CipherOutputStream(output, it).apply { + write(plainText.toByteArray(keyCharset)) + close() + } + encodeToString(output.toByteArray(), DEFAULT) + } + } + } catch (exception: Exception) { + Timber.e(exception, "An error occurred while encrypting text") + String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset) + } } -fun encrypt(plainText: String, context: Context): String { - if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") + fun decrypt(cipherText: String): String { + if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - return try { - if (!isKeyPairExists) generateKeyPair(context) + return try { + if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) + } + } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) + + CipherInputStream( + ByteArrayInputStream(decode(cipherText, DEFAULT)), + it + ).let { input -> + val values = ArrayList() + var nextByte: Int + while (run { nextByte = input.read(); nextByte } != -1) { + values.add(nextByte.toByte()) + } + val bytes = ByteArray(values.size) + for (i in bytes.indices) { + bytes[i] = values[i] + } + String(bytes, 0, bytes.size, keyCharset) } - } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + } + } catch (e: Exception) { + throw ScramblerException("An error occurred while decrypting text", e) + } + } - ByteArrayOutputStream().let { output -> - CipherOutputStream(output, it).apply { - write(plainText.toByteArray(KEY_CHARSET)) - close() - } - encodeToString(output.toByteArray(), DEFAULT) + private fun generateKeyPair() { + (if (SDK_INT >= M) { + KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) + .setDigests(DIGEST_SHA256, DIGEST_SHA512) + .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) + .setCertificateSerialNumber(BigInteger.TEN) + .setCertificateSubject(X500Principal("CN=Wulkanowy")) + .build() + } else { + KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setSubject(X500Principal("CN=Wulkanowy")) + .setSerialNumber(BigInteger.TEN) + .setStartDate(Calendar.getInstance().time) + .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) + .build() + }).let { + KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { + initialize(it) + genKeyPair() } } - } catch (exception: Exception) { - Timber.e(exception, "An error occurred while encrypting text") - String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + Timber.i("A new KeyPair has been generated") + } + + fun clearKeyPair() { + keyStore.deleteEntry(KEY_ALIAS) + Timber.i("KeyPair has been cleared") + } + + private companion object { + private const val KEYSTORE_NAME = "AndroidKeyStore" + private const val KEY_ALIAS = "wulkanowy_password" } } - -fun decrypt(cipherText: String): String { - if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - - return try { - if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) - } - } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) - - CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input -> - val values = ArrayList() - var nextByte: Int - while (run { nextByte = input.read(); nextByte } != -1) { - values.add(nextByte.toByte()) - } - val bytes = ByteArray(values.size) - for (i in bytes.indices) { - bytes[i] = values[i] - } - String(bytes, 0, bytes.size, KEY_CHARSET) - } - } - } catch (e: Exception) { - throw ScramblerException("An error occurred while decrypting text", e) - } -} - -private fun generateKeyPair(context: Context) { - (if (SDK_INT >= M) { - KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) - .setDigests(DIGEST_SHA256, DIGEST_SHA512) - .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) - .setCertificateSerialNumber(BigInteger.TEN) - .setCertificateSubject(X500Principal("CN=Wulkanowy")) - .build() - } else { - KeyPairGeneratorSpec.Builder(context) - .setAlias(KEY_ALIAS) - .setSubject(X500Principal("CN=Wulkanowy")) - .setSerialNumber(BigInteger.TEN) - .setStartDate(Calendar.getInstance().time) - .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) - .build() - }).let { - KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { - initialize(it) - genKeyPair() - } - } - Timber.i("A new KeyPair has been generated") -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27c454ad..72910b85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,6 +107,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index ec6027e9..d7d83e6c 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -101,8 +101,12 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) {