forked from github/wulkanowy-mirror
Add clearing all data and key entry when decryption failed (#2386)
This commit is contained in:
parent
40df80371c
commit
6ee38e9259
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,11 +68,20 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, 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()
|
||||
}
|
||||
|
@ -28,8 +28,12 @@ abstract class BaseDialogFragment<VB : ViewBinding> : 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() {
|
||||
|
@ -39,8 +39,12 @@ abstract class BaseFragment<VB : ViewBinding>(@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() {
|
||||
|
@ -28,20 +28,37 @@ open class BasePresenter<T : BaseView>(
|
||||
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<T : BaseView>(
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ interface BaseView {
|
||||
|
||||
fun showMessage(text: String)
|
||||
|
||||
fun showExpiredDialog()
|
||||
fun showExpiredCredentialsDialog()
|
||||
|
||||
fun showDecryptionFailedDialog()
|
||||
|
||||
fun showAuthDialog()
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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() {}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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<Byte>()
|
||||
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<Byte>()
|
||||
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")
|
||||
}
|
||||
|
@ -107,6 +107,8 @@
|
||||
<string name="main_log_in">Log in</string>
|
||||
<string name="main_session_expired">Session expired</string>
|
||||
<string name="main_session_relogin">Session expired, log in again</string>
|
||||
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
|
||||
<string name="main_expired_credentials_title">Password changed</string>
|
||||
<string name="main_support_title">Application support</string>
|
||||
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
|
||||
<string name="main_support_positive">Enable ads</string>
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user