-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 토큰 저장소 및 암호화 도구 구현 #15
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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
d0a484f
chore: add project structure tree
i-meant-to-be 2e3efb0
feat: add TokenBundle class
i-meant-to-be 01d293d
chore: add Protobuf to dependencies
i-meant-to-be b3a7902
feat: define proto structure for token bundle
i-meant-to-be 0d63b14
feat: add serializer for EncryptedTokens proto instance
i-meant-to-be f7c3b5d
feat: add CryptoManager that encrypts/decrypts tokens
i-meant-to-be 78cc0a6
feat: add TokenRepo that manages token with Protobuf
i-meant-to-be e4446f5
feat: add Hilt module to use DI
i-meant-to-be 11e2636
test: add test codes
i-meant-to-be 0c8e5c7
feat: apply CodeRabbit's suggestions
i-meant-to-be 3c10768
refactor: clarify error message and exception type
i-meant-to-be File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
app/src/androidTest/java/com/debatetimer/app/HiltTestRunner.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.debatetimer.app | ||
|
|
||
| import android.app.Application | ||
| import android.content.Context | ||
| import androidx.test.runner.AndroidJUnitRunner | ||
| import dagger.hilt.android.testing.HiltTestApplication | ||
|
|
||
| // 안드로이드의 기본 테스트 러너를 상속받는 커스텀 러너 | ||
| class HiltTestRunner : AndroidJUnitRunner() { | ||
|
|
||
| // 테스트 프로세스를 위한 새로운 Application 객체를 만드는 함수를 오버라이드 | ||
| override fun newApplication( | ||
| cl: ClassLoader?, | ||
| className: String?, | ||
| context: Context? | ||
| ): Application { | ||
| // 기본 Application 클래스 대신, Hilt 테스트 전용 Application 클래스를 사용하도록 강제 | ||
| return super.newApplication(cl, HiltTestApplication::class.java.name, context) | ||
| } | ||
| } |
47 changes: 47 additions & 0 deletions
47
app/src/androidTest/java/com/debatetimer/app/data/repo/TokenRepoImplHiltTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package com.debatetimer.app.data.repo | ||
|
|
||
| import com.debatetimer.app.data.model.TokenBundle | ||
| import dagger.hilt.android.testing.HiltAndroidRule | ||
| import dagger.hilt.android.testing.HiltAndroidTest | ||
| import kotlinx.coroutines.runBlocking | ||
| import org.hamcrest.CoreMatchers.equalTo | ||
| import org.hamcrest.MatcherAssert.assertThat | ||
| import org.junit.Before | ||
| import org.junit.Rule | ||
| import org.junit.Test | ||
| import javax.inject.Inject | ||
|
|
||
| @HiltAndroidTest | ||
| class TokenRepoImplHiltTest { | ||
|
|
||
| @get:Rule | ||
| var hiltRule = HiltAndroidRule(this) | ||
|
|
||
| // Hilt가 FakeDataModule을 기반으로 의존성을 주입해줍니다. | ||
| // 이 tokenRepo는 내부에 FakeCryptoManagerImpl을 가지고 있습니다. | ||
| @Inject | ||
| lateinit var tokenRepo: TokenRepo | ||
|
|
||
| @Before | ||
| fun setUp() { | ||
| hiltRule.inject() | ||
| } | ||
|
|
||
| @Test | ||
| fun saveAndGetTokens_withFakeCryptoManager_returnsCorrectBundle() = runBlocking { | ||
| // Arrange (준비) | ||
| val originalBundle = TokenBundle( | ||
| accessToken = "access_token_hilt_test", | ||
| refreshToken = "refresh_token_hilt_test" | ||
| ) | ||
|
|
||
| // Act (실행) | ||
| tokenRepo.saveTokens(originalBundle) | ||
| val result = tokenRepo.getTokens() | ||
|
|
||
| // Assert (검증) | ||
| assertThat(result.isSuccess, equalTo(true)) | ||
| val retrievedBundle = result.getOrNull() | ||
| assertThat(retrievedBundle, equalTo(originalBundle)) | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
app/src/androidTest/java/com/debatetimer/app/di/FakeDataModule.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.debatetimer.app.di | ||
|
|
||
| import com.debatetimer.app.data.repo.TokenRepo | ||
| import com.debatetimer.app.data.repo.TokenRepoImpl | ||
| import com.debatetimer.app.util.crypto.CryptoManager | ||
| import com.debatetimer.app.util.crypto.FakeCryptoManagerImpl | ||
| import dagger.Binds | ||
| import dagger.Module | ||
| import dagger.hilt.components.SingletonComponent | ||
| import dagger.hilt.testing.TestInstallIn | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @TestInstallIn( | ||
| components = [SingletonComponent::class], | ||
| replaces = [DataModule::class] | ||
| ) | ||
| abstract class FakeDataModule { | ||
| // 2. CryptoManager 요청 시 FakeCryptoManagerImpl을 주입하도록 바인딩합니다. | ||
| @Binds | ||
| @Singleton | ||
| abstract fun bindCryptoManager( | ||
| fakeCryptoManagerImpl: FakeCryptoManagerImpl | ||
| ): CryptoManager | ||
|
|
||
| // 3. TokenRepo는 기존과 동일하게 TokenRepoImpl을 주입하도록 바인딩합니다. | ||
| // (DataModule을 통째로 비활성화했으므로, 필요한 다른 바인딩도 여기에 다시 선언해줘야 합니다.) | ||
| @Binds | ||
| @Singleton | ||
| abstract fun bindTokenRepo( | ||
| tokenRepoImpl: TokenRepoImpl | ||
| ): TokenRepo | ||
| } |
35 changes: 35 additions & 0 deletions
35
app/src/androidTest/java/com/debatetimer/app/util/crypto/CryptoManagerImplTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.debatetimer.app.util.crypto | ||
|
|
||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import org.hamcrest.CoreMatchers.equalTo | ||
| import org.hamcrest.MatcherAssert.assertThat | ||
| import org.junit.Before | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| // 안드로이드 환경에서 JUnit4 테스트를 실행하기 위한 Runner | ||
| @RunWith(AndroidJUnit4::class) | ||
| class CryptoManagerImplTest { | ||
| private lateinit var cryptoManager: CryptoManagerImpl | ||
|
|
||
| // @Before: 각 테스트(@Test)가 실행되기 직전에 항상 먼저 실행되는 함수 | ||
| @Before | ||
| fun setUp() { | ||
| // Hilt를 사용하지 않고, 테스트 대상 클래스를 직접 생성합니다. | ||
| cryptoManager = CryptoManagerImpl() | ||
| } | ||
|
|
||
| // @Test: 이 함수가 테스트 케이스임을 나타냅니다. | ||
| @Test | ||
| fun encryptionAndDecryption_roundTrip_returnsOriginalText() { | ||
| // Arrange (준비): 테스트에 필요한 변수와 상태를 설정합니다. | ||
| val originalText = "This is a secret message for a round-trip test." | ||
|
|
||
| // Act (실행): 테스트하려는 실제 동작을 수행합니다. | ||
| val (ciphertext, iv) = cryptoManager.encrypt(originalText) | ||
| val decryptedText = cryptoManager.decrypt(ciphertext, iv) | ||
|
|
||
| // Assert (검증): 실행 결과가 예상과 일치하는지 확인합니다. | ||
| assertThat(decryptedText, equalTo(originalText)) | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
app/src/androidTest/java/com/debatetimer/app/util/crypto/FakeCryptoManagerImpl.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.debatetimer.app.util.crypto | ||
|
|
||
| import javax.inject.Inject | ||
|
|
||
| class FakeCryptoManagerImpl @Inject constructor() : CryptoManager { | ||
| private val dummyIv = "dummy_iv".toByteArray() | ||
| private val suffix = "_encrypted" | ||
|
|
||
| override fun encrypt(plainText: String): Pair<ByteArray, ByteArray> { | ||
| val fakeCiphertext = (plainText + suffix).toByteArray(Charsets.UTF_8) | ||
| return Pair(fakeCiphertext, dummyIv) | ||
| } | ||
|
|
||
| override fun decrypt(encryptedText: ByteArray, iv: ByteArray): String { | ||
| val fakePlaintext = String(encryptedText, Charsets.UTF_8) | ||
| return fakePlaintext.removeSuffix(suffix) | ||
| } | ||
| } |
13 changes: 13 additions & 0 deletions
13
app/src/main/java/com/debatetimer/app/data/model/TokenBundle.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.debatetimer.app.data.model | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class TokenBundle( | ||
| val accessToken: String, | ||
| val refreshToken: String, | ||
| ) { | ||
| override fun toString(): String { | ||
| return "TokenBundle(accessToken='***', refreshToken='***')" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.debatetimer.app.data.repo | ||
|
|
||
| import com.debatetimer.app.data.model.TokenBundle | ||
|
|
||
| interface TokenRepo { | ||
| suspend fun getTokens(): Result<TokenBundle?> | ||
| suspend fun saveTokens(bundle: TokenBundle): Result<Unit> | ||
| } |
79 changes: 79 additions & 0 deletions
79
app/src/main/java/com/debatetimer/app/data/repo/TokenRepoImpl.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.debatetimer.app.data.repo | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import com.debatetimer.app.EncryptedTokens | ||
| import com.debatetimer.app.data.model.TokenBundle | ||
| import com.debatetimer.app.data.serializer.tokenDataStore | ||
| import com.debatetimer.app.util.crypto.CryptoManager | ||
| import com.google.gson.Gson | ||
| import com.google.gson.JsonSyntaxException | ||
| import com.google.protobuf.kotlin.toByteString | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.flow.first | ||
| import javax.inject.Inject | ||
|
|
||
| class TokenRepoImpl @Inject constructor( | ||
| @ApplicationContext context: Context, | ||
| private val cryptoManager: CryptoManager | ||
| ) : | ||
| TokenRepo { | ||
| companion object { | ||
| private val gson = Gson().newBuilder() | ||
| .disableHtmlEscaping() | ||
| .create() | ||
| } | ||
|
|
||
| private val tokenDataStore: DataStore<EncryptedTokens> = context.tokenDataStore | ||
|
|
||
| override suspend fun getTokens(): Result<TokenBundle?> { | ||
| try { | ||
| val encryptedTokens = tokenDataStore.data.first() | ||
|
|
||
| if (encryptedTokens.encryptedTokenBundle.isEmpty || encryptedTokens.initializationVector.isEmpty) { | ||
| return Result.success(null) | ||
| } | ||
|
|
||
| val ciphertext = encryptedTokens.encryptedTokenBundle.toByteArray() | ||
| val iv = encryptedTokens.initializationVector.toByteArray() | ||
| val decryptedTokens = cryptoManager.decrypt(ciphertext, iv) | ||
|
|
||
| val bundle = gson.fromJson(decryptedTokens, TokenBundle::class.java) | ||
|
|
||
| return if (bundle != null) { | ||
| Result.success(bundle) | ||
| } else { | ||
| Result.failure(NoSuchElementException()) | ||
| } | ||
| } catch (e: JsonSyntaxException) { | ||
| return Result.failure(IllegalStateException("Corrupted token data", e)) | ||
| } catch (e: Exception) { | ||
| return Result.failure(e) | ||
| } | ||
| } | ||
|
|
||
| override suspend fun saveTokens(bundle: TokenBundle): Result<Unit> { | ||
| try { | ||
| require(bundle.accessToken.isNotBlank()) { "Access token cannot be blank" } | ||
| require(bundle.refreshToken.isNotBlank()) { "Refresh token cannot be blank" } | ||
|
|
||
| tokenDataStore.updateData { current -> | ||
| val serializedBundle = try { | ||
| gson.toJson(bundle) | ||
| } catch (e: Exception) { | ||
| throw IllegalStateException("Failed to serialize token bundle", e) | ||
| } | ||
| val encryptionOutput = cryptoManager.encrypt(serializedBundle) | ||
|
|
||
| current.toBuilder() | ||
| .setEncryptedTokenBundle(encryptionOutput.first.toByteString()) | ||
| .setInitializationVector(encryptionOutput.second.toByteString()) | ||
| .build() | ||
| } | ||
|
|
||
| return Result.success(Unit) | ||
| } catch (e: Exception) { | ||
| return Result.failure(e) | ||
| } | ||
| } | ||
| } |
34 changes: 34 additions & 0 deletions
34
app/src/main/java/com/debatetimer/app/data/serializer/TokenSerializer.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.debatetimer.app.data.serializer | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.CorruptionException | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.core.Serializer | ||
| import androidx.datastore.dataStore | ||
| import com.debatetimer.app.EncryptedTokens | ||
| import com.google.protobuf.InvalidProtocolBufferException | ||
| import java.io.InputStream | ||
| import java.io.OutputStream | ||
|
|
||
| const val FILE_NAME = "encrypted_tokens.pb" | ||
|
|
||
| object TokenSerializer : Serializer<EncryptedTokens> { | ||
| override val defaultValue: EncryptedTokens = EncryptedTokens.getDefaultInstance() | ||
|
|
||
| override suspend fun readFrom(input: InputStream): EncryptedTokens { | ||
| try { | ||
| return EncryptedTokens.parseFrom(input) | ||
| } catch (exception: InvalidProtocolBufferException) { | ||
| throw CorruptionException("Cannot read proto.", exception) | ||
| } | ||
| } | ||
|
|
||
| override suspend fun writeTo(t: EncryptedTokens, output: OutputStream) { | ||
| t.writeTo(output) | ||
| } | ||
| } | ||
|
|
||
| val Context.tokenDataStore: DataStore<EncryptedTokens> by dataStore( | ||
| fileName = FILE_NAME, | ||
| serializer = TokenSerializer | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.debatetimer.app.di | ||
|
|
||
| import com.debatetimer.app.data.repo.TokenRepo | ||
| import com.debatetimer.app.data.repo.TokenRepoImpl | ||
| import com.debatetimer.app.util.crypto.CryptoManager | ||
| import com.debatetimer.app.util.crypto.CryptoManagerImpl | ||
| import dagger.Binds | ||
| import dagger.Module | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.components.SingletonComponent | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| abstract class DataModule { | ||
| @Binds | ||
| @Singleton | ||
| abstract fun bindCryptoManager( | ||
| cryptoManagerImpl: CryptoManagerImpl | ||
| ): CryptoManager | ||
|
|
||
| @Binds | ||
| @Singleton | ||
| abstract fun bindTokenRepo( | ||
| tokenRepoImpl: TokenRepoImpl | ||
| ): TokenRepo | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.