diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4efe4428e03b11a08b97f6e7e4cdb673a9813256..ca5df445a52388c9d83cb80c05d09a5edf4ea87e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,7 @@ android { } dependencies { + implementation("androidx.security:security-crypto:1.1.0-alpha06") implementation("androidx.core:core-splashscreen:1.0.1") implementation("com.android.volley:volley:1.2.1") implementation("androidx.core:core-ktx:1.12.0") diff --git a/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt b/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt index 378a6f2e1af9fda80cf7a9e5d2f51c5367ce8c23..e98414fcb27871c393616626711cf0f9c3eaac44 100644 --- a/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt +++ b/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import com.example.nerbos.model.Transaction import com.example.nerbos.data.TransactionDao import com.example.nerbos.model.TransactionCategory -import com.example.nerbos.service.Authentication class TransactionRepository(private val transactionDao: TransactionDao) { var readAllData: LiveData<List<Transaction>>? = null diff --git a/app/src/main/java/com/example/nerbos/service/Authentication.kt b/app/src/main/java/com/example/nerbos/service/Authentication.kt index ca362b71f0e9ecefbb3d227e2b12bc23aaee4016..5dd7f05369dc0c7d8ff4630ab5a4ae1046b009c2 100644 --- a/app/src/main/java/com/example/nerbos/service/Authentication.kt +++ b/app/src/main/java/com/example/nerbos/service/Authentication.kt @@ -3,12 +3,18 @@ package com.example.nerbos.service import android.content.Context import android.content.Intent import android.content.SharedPreferences +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import com.android.volley.Request import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley import com.example.nerbos.LoginActivity import com.example.nerbos.R import org.json.JSONObject +import java.security.KeyPairGenerator +import java.security.KeyStore +import javax.crypto.Cipher +import android.util.Base64 interface AuthCallback { fun onSuccess() { @@ -23,6 +29,34 @@ interface AuthCallback { } class Authentication(private val context: Context) { + private val keyStore: KeyStore = KeyStore.getInstance(context.getString(R.string.android_key_store)) + .apply { + load(null) + } + + private fun generateKeyPair() { + // Generate key pair + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_RSA, context.getString(R.string.android_key_store) + ) + keyPairGenerator.initialize( + KeyGenParameterSpec.Builder( + context.getString(R.string.key_alias), + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).apply { + setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + setDigests(KeyProperties.DIGEST_SHA256) + setUserAuthenticationRequired(false) + }.build() + ) + keyPairGenerator.generateKeyPair() + } + + private fun checkKeyPair() { + if (!keyStore.containsAlias(context.getString(R.string.key_alias))) { + generateKeyPair() + } + } // Service: API Authentication for login and token checking fun login(email: String, password: String, callback: AuthCallback) { @@ -48,7 +82,7 @@ class Authentication(private val context: Context) { }, { error -> // handle error - callback.onError(error.message ?: "Wrong email or password") + callback.onError(error.message ?: context.getString(R.string.wrong_credentials)) } ) @@ -57,40 +91,54 @@ class Authentication(private val context: Context) { private fun saveToken(token: String) { - val sharedPreferences : SharedPreferences = - context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) - val editor = sharedPreferences.edit() - editor.putString(context.getString(R.string.token), token) - editor.apply() - } + checkKeyPair() + // get public key from key store + val privateKeyEntry = keyStore.getEntry(context.getString(R.string.key_alias), null) as KeyStore.PrivateKeyEntry + val publicKey = privateKeyEntry.certificate.publicKey - private fun saveEmail(email: String) { + // initialize cipher with public key + val cipher = Cipher.getInstance(context.getString(R.string.cipher_transformation)) + cipher.init(Cipher.ENCRYPT_MODE, publicKey) + + // encrypt token + val encryptedToken = cipher.doFinal(token.toByteArray()) + + // save encrypted token to shared preferences + val encryptedTokenString = Base64.encodeToString(encryptedToken, Base64.DEFAULT) val sharedPreferences : SharedPreferences = context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) val editor = sharedPreferences.edit() - editor.putString(context.getString(R.string.email), email) + editor.putString(context.getString(R.string.token), encryptedTokenString) editor.apply() } - fun getEmail(): String { - val sharedPreferences : SharedPreferences = + fun getToken(): String { + checkKeyPair() + // get encrypted token from shared preferences + val sharedPreferences: SharedPreferences = context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) - // if no email in shared preferences, return default value - return sharedPreferences.getString(context.getString(R.string.email), "13521000@std.stei.itb.ac.id") ?: "" - } + val encryptedTokenString = sharedPreferences.getString(context.getString(R.string.token), "") ?: "" + if (encryptedTokenString == "") { + return "" + } - fun getNim(): Int { - val email = getEmail().split("@")[0] - return email.toInt() - } + // get private key from key store + val privateKeyEntry = keyStore.getEntry(context.getString(R.string.key_alias), null) as KeyStore.PrivateKeyEntry + val privateKey = privateKeyEntry.privateKey - internal fun getToken(): String { - val sharedPreferences : SharedPreferences = - context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) - return sharedPreferences.getString(context.getString(R.string.token), "") ?: "" + // initialize cipher with private key + val cipher = Cipher.getInstance(context.getString(R.string.cipher_transformation)) + cipher.init(Cipher.DECRYPT_MODE, privateKey) + + // decrypt token + val encryptedToken = Base64.decode(encryptedTokenString, Base64.DEFAULT) + val decryptedToken = cipher.doFinal(encryptedToken) + return String(decryptedToken, Charsets.UTF_8) } + fun checkToken(callback: AuthCallback) { + checkKeyPair() val token = getToken() if (token.isNotEmpty()) { val request = object : JsonObjectRequest( @@ -111,11 +159,10 @@ class Authentication(private val context: Context) { }) { override fun getHeaders(): MutableMap<String, String> { val headers = HashMap<String, String>() - headers["Authorization"] = "Bearer $token" + headers[context.getString(R.string.authorization)] = context.getString(R.string.bearer) + " " + token return headers } } - Volley.newRequestQueue(context).add(request) } } @@ -134,4 +181,25 @@ class Authentication(private val context: Context) { intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) } + + // Save and get email from shared preferences + private fun saveEmail(email: String) { + val sharedPreferences : SharedPreferences = + context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) + val editor = sharedPreferences.edit() + editor.putString(context.getString(R.string.email), email) + editor.apply() + } + + fun getEmail(): String { + val sharedPreferences : SharedPreferences = + context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) + // if no email in shared preferences, return default value + return sharedPreferences.getString(context.getString(R.string.email), context.getString(R.string.default_email)) ?: "" + } + + fun getNim(): Int { + val email = getEmail().split("@")[0] + return email.toInt() + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4850983e3b3a6d9ef0f9a0685b30a9efdcb5edb7..891f9fa2132678e79ff4dad7c11e5aed1d8996ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,9 +14,16 @@ <string name="login_password_placeholder">Password</string> <string name="error_login">Error</string> <string name="error_fields">Please fill out all the fields</string> + <string name="wrong_credentials">Wrong email or password</string> <string name="email">email</string> <string name="password">password</string> <string name="token">token</string> + <string name="key_alias">key13521000</string> + <string name="cipher_transformation">RSA/ECB/PKCS1Padding</string> + <string name="android_key_store">AndroidKeyStore</string> + <string name="default_email">13521000@std.stei.itb.ac.id</string> + <string name="authorization">Authorization</string> + <string name="bearer">Bearer</string> <string name="preferences">NosPreferences</string>