-
Notifications
You must be signed in to change notification settings - Fork 1
[init] #4 DataStore, API 응답 핸들링 로직 추가 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ce92ad
ea9260a
5164727
17badae
33f276f
b7b5419
992dadd
27371e9
722b61b
8db2485
ba03dca
1f5b96d
ba9c1fd
e61e08e
b3553f9
dbb910d
2d00217
21ae929
182f305
448a732
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,5 +9,6 @@ android { | |
|
|
||
| dependencies { | ||
| api(libs.timber) | ||
| implementation(libs.androidx.security.crypto) | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.neki.android.core.common.crypto | ||
|
|
||
| import android.annotation.SuppressLint | ||
| import android.security.keystore.KeyGenParameterSpec | ||
| import android.security.keystore.KeyProperties | ||
| import android.util.Base64 | ||
| import java.security.KeyStore | ||
| import javax.crypto.Cipher | ||
| import javax.crypto.KeyGenerator | ||
| import javax.crypto.SecretKey | ||
| import javax.crypto.spec.GCMParameterSpec | ||
|
|
||
| object CryptoManager { | ||
| private const val ALGORITHM = "AES/GCM/NoPadding" | ||
| private const val KEY_ALIAS = "neki_token_encryption_key" | ||
|
|
||
| private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { | ||
| load(null) | ||
| } | ||
|
|
||
| private fun getSecretKey(): SecretKey { | ||
| val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry | ||
| return existingKey?.secretKey ?: createKey() | ||
| } | ||
|
|
||
| @SuppressLint("NewApi") | ||
| private fun createKey(): SecretKey { | ||
| return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply { | ||
| init( | ||
| KeyGenParameterSpec.Builder( | ||
| KEY_ALIAS, | ||
| KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, | ||
| ) | ||
| .setBlockModes(KeyProperties.BLOCK_MODE_GCM) | ||
| .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) | ||
| .build(), | ||
| ) | ||
| }.generateKey() | ||
| } | ||
|
|
||
| @SuppressLint("NewApi") | ||
| fun encrypt(text: String): String { | ||
| val cipher = Cipher.getInstance(ALGORITHM) | ||
| cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) | ||
|
|
||
| val encryptedBytes = cipher.doFinal(text.toByteArray()) | ||
| val combined = cipher.iv + encryptedBytes | ||
|
|
||
| return Base64.encodeToString(combined, Base64.NO_WRAP) | ||
| } | ||
|
|
||
| @SuppressLint("NewApi") | ||
| fun decrypt(encryptedText: String): String { | ||
| val combined = Base64.decode(encryptedText, Base64.NO_WRAP) | ||
| val encryptedData = combined.sliceArray(12 until combined.size) | ||
|
|
||
| val cipher = Cipher.getInstance(ALGORITHM) | ||
| val iv = combined.sliceArray(0 until 12) | ||
| val spec = GCMParameterSpec(128, iv) | ||
| cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) | ||
|
|
||
| return String(cipher.doFinal(encryptedData)) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,4 +4,6 @@ plugins { | |
|
|
||
| dependencies { | ||
| implementation(projects.core.model) | ||
| implementation(libs.kotlinx.coroutines.core) | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.neki.android.core.dataapi.repository | ||
|
|
||
| import kotlinx.coroutines.flow.Flow | ||
|
|
||
| interface DataStoreRepository { | ||
| suspend fun saveTokens( | ||
| accessToken: String, | ||
| refreshToken: String, | ||
| ) | ||
|
|
||
| fun getAccessToken(): Flow<String?> | ||
| fun getRefreshToken(): Flow<String?> | ||
| suspend fun clearTokens() | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,20 @@ | ||
| package com.neki.android.core.data.repository.di | ||
|
|
||
| import com.neki.android.core.data.repository.impl.SampleRepositoryImpl | ||
| import com.neki.android.core.dataapi.repository.SampleRepository | ||
| import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl | ||
| import com.neki.android.core.dataapi.repository.DataStoreRepository | ||
| import dagger.Binds | ||
| import dagger.Module | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.components.SingletonComponent | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| internal abstract class RepositoryModule { | ||
| internal interface RepositoryModule { | ||
|
|
||
| @Binds | ||
| abstract fun bindSampleRepositoryImpl( | ||
| sampleRepositoryImpl: SampleRepositoryImpl, | ||
| ): SampleRepository | ||
| @Singleton | ||
| fun bindDataStoreRepositoryImpl( | ||
| dataStoreRepositoryImpl: DataStoreRepositoryImpl, | ||
| ): DataStoreRepository | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.neki.android.core.data.repository.impl | ||
|
|
||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.core.edit | ||
| import androidx.datastore.preferences.core.stringPreferencesKey | ||
| import com.neki.android.core.common.crypto.CryptoManager | ||
| import com.neki.android.core.dataapi.repository.DataStoreRepository | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.map | ||
| import javax.inject.Inject | ||
|
|
||
| class DataStoreRepositoryImpl @Inject constructor( | ||
| private val dataStore: DataStore<Preferences>, | ||
| ) : DataStoreRepository { | ||
| companion object { | ||
| private val ACCESS_TOKEN = stringPreferencesKey("access_token") | ||
| private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") | ||
| } | ||
|
|
||
| override suspend fun saveTokens( | ||
| accessToken: String, | ||
| refreshToken: String, | ||
| ) { | ||
| dataStore.edit { preferences -> | ||
| preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) | ||
| preferences[REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) | ||
| } | ||
| } | ||
|
Ojongseok marked this conversation as resolved.
|
||
|
|
||
| override fun getAccessToken(): Flow<String?> { | ||
| return dataStore.data.map { preferences -> | ||
| preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } | ||
| } | ||
| } | ||
|
Ojongseok marked this conversation as resolved.
|
||
|
|
||
| override fun getRefreshToken(): Flow<String?> { | ||
| return dataStore.data.map { preferences -> | ||
| preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } | ||
| } | ||
| } | ||
|
|
||
| override suspend fun clearTokens() { | ||
| dataStore.edit { it.clear() } | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.neki.android.core.data.util | ||
|
|
||
| import kotlin.contracts.ExperimentalContracts | ||
| import kotlin.contracts.InvocationKind | ||
| import kotlin.contracts.contract | ||
| import kotlin.coroutines.cancellation.CancellationException | ||
|
|
||
| @OptIn(ExperimentalContracts::class) | ||
| internal inline fun <T> runSuspendCatching(block: () -> T): Result<T> { | ||
| // Kotlin 의 contract(계약) 시스템을 이용해 block 이 정확히 한번만 호출 되어야 함을 나타냄 | ||
| contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } | ||
|
|
||
| return runCatching(block).also { result -> | ||
| // 만약 람다에서 예외가 발생하면, Result 객체는 실패를 나타내고 해당 예외를 포함, 추가적인 작업을 실행 | ||
| val maybeException = result.exceptionOrNull() | ||
| // 만약 예외가 CancellationException 이면 예외를 던져 코루틴 계층 구조에 따라 상위 코루틴까지 취소 신호를 전파 | ||
| // 이를 통해, 상위 코루틴에서 적절한 예외 처리 루틴을 수행할 수 있음 | ||
| if (maybeException is CancellationException) throw maybeException | ||
| } | ||
|
Ojongseok marked this conversation as resolved.
|
||
| } | ||
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.