diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 6212c41d..0cd4704d 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -9,5 +9,11 @@ android { } dependencies { - implementation(libs.androidx.datastore) + implementation(projects.core.security) + + implementation(libs.androidx.datastore.preferences) + implementation(libs.kotlinx.serialization.json) + + testImplementation(libs.androidx.junit) + testImplementation(libs.kotlin.coroutines.test) } diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/.gitkeep b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/di/DataStoreModule.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..e38a967b --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/di/DataStoreModule.kt @@ -0,0 +1,37 @@ +package com.threegap.bitnagil.datastore.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.dataStoreFile +import com.threegap.bitnagil.datastore.model.AuthToken +import com.threegap.bitnagil.datastore.serializer.AuthTokenSerializer +import com.threegap.bitnagil.datastore.serializer.TokenSerializer +import com.threegap.bitnagil.security.crypto.Crypto +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + @Provides + @Singleton + fun provideTokenSerializer(crypto: Crypto): TokenSerializer = AuthTokenSerializer(crypto) + + @Provides + @Singleton + fun provideAuthTokenDataStore( + @ApplicationContext context: Context, + tokenSerializer: TokenSerializer, + ): DataStore = + DataStoreFactory.create( + serializer = tokenSerializer, + produceFile = { context.dataStoreFile("auth-token.enc") }, + corruptionHandler = ReplaceFileCorruptionHandler { AuthToken() }, + ) +} diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/model/AuthToken.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/model/AuthToken.kt new file mode 100644 index 00000000..652eedd5 --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/model/AuthToken.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.datastore.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AuthToken( + val accessToken: String? = null, + val refreshToken: String? = null, +) diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializer.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializer.kt new file mode 100644 index 00000000..a22ff63a --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializer.kt @@ -0,0 +1,50 @@ +package com.threegap.bitnagil.datastore.serializer + +import com.threegap.bitnagil.datastore.model.AuthToken +import com.threegap.bitnagil.security.crypto.Crypto +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.InputStream +import java.io.OutputStream +import java.util.Base64 +import javax.inject.Inject + +internal class AuthTokenSerializer + @Inject + constructor( + private val crypto: Crypto, + ) : TokenSerializer { + override val defaultValue: AuthToken + get() = AuthToken() + + override suspend fun readFrom(input: InputStream): AuthToken { + return try { + val encryptedBytes = + withContext(Dispatchers.IO) { + input.use { it.readBytes() } + } + val decodedBytes = Base64.getDecoder().decode(encryptedBytes) + val decryptedBytes = crypto.decrypt(decodedBytes) + val decodedJsonString = decryptedBytes.decodeToString() + Json.decodeFromString(decodedJsonString) + } catch (e: Exception) { + AuthToken() + } + } + + override suspend fun writeTo( + t: AuthToken, + output: OutputStream, + ) { + val json = Json.encodeToString(t) + val bytes = json.toByteArray() + val encryptedBytes = crypto.encrypt(bytes) + val encryptedBytesBase64 = Base64.getEncoder().encode(encryptedBytes) + withContext(Dispatchers.IO) { + output.use { + it.write(encryptedBytesBase64) + } + } + } + } diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/TokenSerializer.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/TokenSerializer.kt new file mode 100644 index 00000000..24318322 --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/serializer/TokenSerializer.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.datastore.serializer + +import androidx.datastore.core.Serializer +import com.threegap.bitnagil.datastore.model.AuthToken + +interface TokenSerializer : Serializer diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStore.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStore.kt new file mode 100644 index 00000000..90058a2f --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStore.kt @@ -0,0 +1,16 @@ +package com.threegap.bitnagil.datastore.storage + +import com.threegap.bitnagil.datastore.model.AuthToken +import kotlinx.coroutines.flow.Flow + +interface AuthTokenDataStore { + val tokenFlow: Flow + + suspend fun updateAuthToken(authToken: AuthToken): AuthToken + + suspend fun updateAccessToken(accessToken: String): AuthToken + + suspend fun updateRefreshToken(refreshToken: String): AuthToken + + suspend fun clearAuthToken(): AuthToken +} diff --git a/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImpl.kt b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImpl.kt new file mode 100644 index 00000000..afdf001f --- /dev/null +++ b/core/datastore/src/main/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImpl.kt @@ -0,0 +1,67 @@ +package com.threegap.bitnagil.datastore.storage + +import android.util.Log +import androidx.datastore.core.DataStore +import com.threegap.bitnagil.datastore.model.AuthToken +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +internal class AuthTokenDataStoreImpl + @Inject + constructor( + private val dataStore: DataStore, + ) : AuthTokenDataStore { + override val tokenFlow: Flow = dataStore.data + + override suspend fun updateAuthToken(authToken: AuthToken): AuthToken = + runCatching { + dataStore.updateData { authToken } + }.fold( + onSuccess = { it }, + onFailure = { + Log.e(TAG, "updateAuthToken failed:", it) + throw it + }, + ) + + override suspend fun updateAccessToken(accessToken: String): AuthToken = + runCatching { + dataStore.updateData { authToken -> + authToken.copy(accessToken = accessToken) + } + }.fold( + onSuccess = { it }, + onFailure = { + Log.e(TAG, "updateAccessToken failed:", it) + throw it + }, + ) + + override suspend fun updateRefreshToken(refreshToken: String): AuthToken = + runCatching { + dataStore.updateData { authToken -> + authToken.copy(refreshToken = refreshToken) + } + }.fold( + onSuccess = { it }, + onFailure = { + Log.e(TAG, "updateRefreshToken failed:", it) + throw it + }, + ) + + override suspend fun clearAuthToken(): AuthToken = + runCatching { + dataStore.updateData { AuthToken() } + }.fold( + onSuccess = { it }, + onFailure = { + Log.e(TAG, "clearAuthToken failed:", it) + throw it + }, + ) + + companion object { + private const val TAG = "AuthTokenDataStore" + } + } diff --git a/core/datastore/src/test/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializerTest.kt b/core/datastore/src/test/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializerTest.kt new file mode 100644 index 00000000..a84b82b7 --- /dev/null +++ b/core/datastore/src/test/java/com/threegap/bitnagil/datastore/serializer/AuthTokenSerializerTest.kt @@ -0,0 +1,96 @@ +package com.threegap.bitnagil.datastore.serializer + +import com.threegap.bitnagil.datastore.model.AuthToken +import com.threegap.bitnagil.security.crypto.Crypto +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.util.Base64 + +class AuthTokenSerializerTest { + private lateinit var serializer: AuthTokenSerializer + private lateinit var crypto: FakeCrypto + private lateinit var fakeToken: AuthToken + private lateinit var encrypted: ByteArray + private lateinit var json: String + + private class FakeCrypto( + private val encryptResult: ByteArray, + private val decryptResult: ByteArray, + private val shouldFailDecrypt: Boolean = false, + ) : Crypto { + override fun encrypt(bytes: ByteArray): ByteArray = encryptResult + + override fun decrypt(bytes: ByteArray): ByteArray { + if (shouldFailDecrypt) throw RuntimeException("복호화 실패") + return decryptResult + } + } + + @Before + fun setUp() { + fakeToken = AuthToken("access", "refresh") + json = Json.encodeToString(fakeToken) + encrypted = "암호화된값".toByteArray() + + crypto = + FakeCrypto( + encryptResult = encrypted, + decryptResult = json.toByteArray(), + ) + + serializer = AuthTokenSerializer(crypto) + } + + @Test + fun `writeTo는 AuthToken을 직렬화하여 기록한다`() = + runTest { + // given + val outputStream = ByteArrayOutputStream() + + // when + serializer.writeTo(fakeToken, outputStream) + + // then + val expected = Base64.getEncoder().encode(encrypted) + assertEquals(expected.toList(), outputStream.toByteArray().toList()) + } + + @Test + fun `readFrom은 InputStream을 역직렬화하여 AuthToken으로 복원한다`() = + runTest { + // given + val input = Base64.getEncoder().encode(encrypted) + val inputStream = ByteArrayInputStream(input) + + // when + val result = serializer.readFrom(inputStream) + + // then + assertEquals(fakeToken, result) + } + + @Test + fun `readFrom에서 예외 발생시 기본값을 반환한다`() = + runTest { + // given + val brokenCrypto = + FakeCrypto( + encryptResult = byteArrayOf(), + decryptResult = byteArrayOf(), + shouldFailDecrypt = true, + ) + val brokenSerializer = AuthTokenSerializer(brokenCrypto) + val inputStream = ByteArrayInputStream(Base64.getEncoder().encode(encrypted)) + + // when + val result = brokenSerializer.readFrom(inputStream) + + // then + assertEquals(AuthToken(), result) + } +} diff --git a/core/datastore/src/test/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImplTest.kt b/core/datastore/src/test/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImplTest.kt new file mode 100644 index 00000000..87c395ee --- /dev/null +++ b/core/datastore/src/test/java/com/threegap/bitnagil/datastore/storage/AuthTokenDataStoreImplTest.kt @@ -0,0 +1,231 @@ +package com.threegap.bitnagil.datastore.storage + +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import com.threegap.bitnagil.datastore.model.AuthToken +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.InputStream +import java.io.OutputStream + +class AuthTokenDataStoreImplTest { + @get:Rule + val temporaryFolder: TemporaryFolder = + TemporaryFolder + .builder() + .assureDeletion() + .build() + + private lateinit var dataStore: DataStore + private lateinit var authTokenDataStore: AuthTokenDataStore + + private object FakeAuthTokenSerializer : Serializer { + override val defaultValue: AuthToken + get() = AuthToken() + + override suspend fun readFrom(input: InputStream): AuthToken { + return try { + input.bufferedReader().use { + Json.decodeFromString(AuthToken.serializer(), it.readText()) + } + } catch (e: Exception) { + AuthToken() + } + } + + override suspend fun writeTo( + t: AuthToken, + output: OutputStream, + ) { + withContext(Dispatchers.IO) { + output.writer().use { + it.write(Json.encodeToString(AuthToken.serializer(), t)) + } + } + } + } + + @Before + fun setup() { + dataStore = + DataStoreFactory.create( + serializer = FakeAuthTokenSerializer, + produceFile = { temporaryFolder.newFile("auth-token-test.enc") }, + ) + + authTokenDataStore = AuthTokenDataStoreImpl(dataStore) + } + + @Test + fun `토큰 전체 업데이트가 성공하면 저장된 토큰을 반환해야 한다`() = + runTest { + // given + val token = + AuthToken( + accessToken = "access", + refreshToken = "refresh", + ) + + // when + val result = authTokenDataStore.updateAuthToken(token) + + // then + assertEquals(token, result) + } + + @Test + fun `accessToken만 업데이트하면 기존 refreshToken은 유지되어야 한다`() = + runTest { + // given + authTokenDataStore.updateAuthToken( + AuthToken( + accessToken = "oldAccess", + refreshToken = "oldRefresh", + ), + ) + + // when + val updated = authTokenDataStore.updateAccessToken(accessToken = "newAccess") + + // then + assertEquals("newAccess", updated.accessToken) + assertEquals("oldRefresh", updated.refreshToken) + } + + @Test + fun `refreshToken만 업데이트하면 기존 accessToken은 유지되어야 한다`() = + runTest { + // given + authTokenDataStore.updateAuthToken( + AuthToken( + accessToken = "oldAccess", + refreshToken = "oldRefresh", + ), + ) + + // when + val updated = authTokenDataStore.updateRefreshToken(refreshToken = "newRefresh") + + // then + assertEquals("oldAccess", updated.accessToken) + assertEquals("newRefresh", updated.refreshToken) + } + + @Test + fun `토큰을 클리어하면 기본값이 저장되어야 한다`() = + runTest { + // given + authTokenDataStore.updateAuthToken( + AuthToken( + accessToken = "someAccess", + refreshToken = "someRefresh", + ), + ) + + // when + val cleared = authTokenDataStore.clearAuthToken() + + // then + assertEquals(AuthToken(), cleared) + } + + @Test + fun `tokenFlow는 현재 저장된 토큰을 방출해야 한다`() = + runTest { + // given + val token = + AuthToken( + accessToken = "flowAccess", + refreshToken = "flowRefresh", + ) + + // when + authTokenDataStore.updateAuthToken(token) + + // then + val flowValue = authTokenDataStore.tokenFlow.first() + assertEquals(token, flowValue) + } + + @Test(expected = RuntimeException::class) + fun `updateAuthToken에서 예외 발생시 예외가 전파되어야 한다`() = + runTest { + // given + val brokenStore = + object : DataStore { + override val data = flowOf(AuthToken()) + + override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken { + throw RuntimeException("updateAuthToken failed") + } + } + val failingDataStore = AuthTokenDataStoreImpl(brokenStore) + + // when & then + failingDataStore.updateAuthToken(AuthToken("access", "refresh")) + } + + @Test(expected = RuntimeException::class) + fun `updateAccessToken에서 예외 발생시 예외가 전파되어야 한다`() = + runTest { + // given + val brokenStore = + object : DataStore { + override val data = flowOf(AuthToken()) + + override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken { + throw RuntimeException("updateAccessToken failed") + } + } + val failingDataStore = AuthTokenDataStoreImpl(brokenStore) + + // when & then + failingDataStore.updateAccessToken("newAccess") + } + + @Test(expected = RuntimeException::class) + fun `updateRefreshToken에서 예외 발생시 예외가 전파되어야 한다`() = + runTest { + // given + val brokenStore = + object : DataStore { + override val data = flowOf(AuthToken()) + + override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken { + throw RuntimeException("updateRefreshToken failed") + } + } + val failingDataStore = AuthTokenDataStoreImpl(brokenStore) + + // when & then + failingDataStore.updateRefreshToken("newRefresh") + } + + @Test(expected = RuntimeException::class) + fun `clearAuthToken에서 예외 발생시 예외가 전파되어야 한다`() = + runTest { + // given + val brokenStore = + object : DataStore { + override val data = flowOf(AuthToken()) + + override suspend fun updateData(transform: suspend (AuthToken) -> AuthToken): AuthToken { + throw RuntimeException("clearAuthToken failed") + } + } + val failingDataStore = AuthTokenDataStoreImpl(brokenStore) + + // when & then + failingDataStore.clearAuthToken() + } +} diff --git a/core/security/build.gradle.kts b/core/security/build.gradle.kts index 03b1b8bf..16a15ac1 100644 --- a/core/security/build.gradle.kts +++ b/core/security/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.bitnagil.android.library) + alias(libs.plugins.bitnagil.android.hilt) } android { diff --git a/core/security/src/main/java/com/threegap/bitnagil/security/crypto/Crypto.kt b/core/security/src/main/java/com/threegap/bitnagil/security/crypto/Crypto.kt new file mode 100644 index 00000000..a12449cc --- /dev/null +++ b/core/security/src/main/java/com/threegap/bitnagil/security/crypto/Crypto.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.security.crypto + +interface Crypto { + fun encrypt(bytes: ByteArray): ByteArray + + fun decrypt(bytes: ByteArray): ByteArray +} diff --git a/core/security/src/main/java/com/threegap/bitnagil/security/Crypto.kt b/core/security/src/main/java/com/threegap/bitnagil/security/crypto/SecureCrypto.kt similarity index 78% rename from core/security/src/main/java/com/threegap/bitnagil/security/Crypto.kt rename to core/security/src/main/java/com/threegap/bitnagil/security/crypto/SecureCrypto.kt index 65f3a804..77ecd032 100644 --- a/core/security/src/main/java/com/threegap/bitnagil/security/Crypto.kt +++ b/core/security/src/main/java/com/threegap/bitnagil/security/crypto/SecureCrypto.kt @@ -1,13 +1,14 @@ -package com.threegap.bitnagil.security +package com.threegap.bitnagil.security.crypto +import com.threegap.bitnagil.security.keystore.KeyProvider import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec -class Crypto( +internal class SecureCrypto( private val keyProvider: KeyProvider, private val transformation: String = "AES/CBC/PKCS7Padding", -) { - fun encrypt(bytes: ByteArray): ByteArray { +) : Crypto { + override fun encrypt(bytes: ByteArray): ByteArray { val cipher = Cipher.getInstance(transformation) cipher.init(Cipher.ENCRYPT_MODE, keyProvider.getKey()) val iv = cipher.iv @@ -15,7 +16,7 @@ class Crypto( return iv + encrypted } - fun decrypt(bytes: ByteArray): ByteArray { + override fun decrypt(bytes: ByteArray): ByteArray { val cipher = Cipher.getInstance(transformation) require(bytes.size >= cipher.blockSize) { INVALID_INPUT_TOO_SHORT_MSG diff --git a/core/security/src/main/java/com/threegap/bitnagil/security/di/SecurityModule.kt b/core/security/src/main/java/com/threegap/bitnagil/security/di/SecurityModule.kt new file mode 100644 index 00000000..85fab3e6 --- /dev/null +++ b/core/security/src/main/java/com/threegap/bitnagil/security/di/SecurityModule.kt @@ -0,0 +1,23 @@ +package com.threegap.bitnagil.security.di + +import com.threegap.bitnagil.security.crypto.Crypto +import com.threegap.bitnagil.security.crypto.SecureCrypto +import com.threegap.bitnagil.security.keystore.AndroidKeyProvider +import com.threegap.bitnagil.security.keystore.KeyProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SecurityModule { + @Provides + @Singleton + fun provideKeyProvider(): KeyProvider = AndroidKeyProvider() + + @Provides + @Singleton + fun provideCrypto(keyProvider: KeyProvider): Crypto = SecureCrypto(keyProvider) +} diff --git a/core/security/src/main/java/com/threegap/bitnagil/security/AndroidKeyProvider.kt b/core/security/src/main/java/com/threegap/bitnagil/security/keystore/AndroidKeyProvider.kt similarity index 93% rename from core/security/src/main/java/com/threegap/bitnagil/security/AndroidKeyProvider.kt rename to core/security/src/main/java/com/threegap/bitnagil/security/keystore/AndroidKeyProvider.kt index f5a51359..8f8289d7 100644 --- a/core/security/src/main/java/com/threegap/bitnagil/security/AndroidKeyProvider.kt +++ b/core/security/src/main/java/com/threegap/bitnagil/security/keystore/AndroidKeyProvider.kt @@ -1,4 +1,4 @@ -package com.threegap.bitnagil.security +package com.threegap.bitnagil.security.keystore import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties @@ -6,7 +6,7 @@ import java.security.KeyStore import javax.crypto.KeyGenerator import javax.crypto.SecretKey -class AndroidKeyProvider : KeyProvider { +internal class AndroidKeyProvider : KeyProvider { private val keyStore = KeyStore .getInstance("AndroidKeyStore") diff --git a/core/security/src/main/java/com/threegap/bitnagil/security/KeyProvider.kt b/core/security/src/main/java/com/threegap/bitnagil/security/keystore/KeyProvider.kt similarity index 64% rename from core/security/src/main/java/com/threegap/bitnagil/security/KeyProvider.kt rename to core/security/src/main/java/com/threegap/bitnagil/security/keystore/KeyProvider.kt index 3a1a2c16..1977e398 100644 --- a/core/security/src/main/java/com/threegap/bitnagil/security/KeyProvider.kt +++ b/core/security/src/main/java/com/threegap/bitnagil/security/keystore/KeyProvider.kt @@ -1,4 +1,4 @@ -package com.threegap.bitnagil.security +package com.threegap.bitnagil.security.keystore import javax.crypto.SecretKey diff --git a/core/security/src/test/java/com/threegap/bitnagil/security/CryptoTest.kt b/core/security/src/test/java/com/threegap/bitnagil/security/crypto/SecureCryptoTest.kt similarity index 74% rename from core/security/src/test/java/com/threegap/bitnagil/security/CryptoTest.kt rename to core/security/src/test/java/com/threegap/bitnagil/security/crypto/SecureCryptoTest.kt index 03fd125b..ddaf2b7e 100644 --- a/core/security/src/test/java/com/threegap/bitnagil/security/CryptoTest.kt +++ b/core/security/src/test/java/com/threegap/bitnagil/security/crypto/SecureCryptoTest.kt @@ -1,5 +1,6 @@ -package com.threegap.bitnagil.security +package com.threegap.bitnagil.security.crypto +import com.threegap.bitnagil.security.keystore.KeyProvider import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertThrows @@ -8,7 +9,7 @@ import javax.crypto.BadPaddingException import javax.crypto.KeyGenerator import javax.crypto.SecretKey -class CryptoTest { +class SecureCryptoTest { private class FakeKeyProvider : KeyProvider { private val key: SecretKey = KeyGenerator @@ -19,8 +20,8 @@ class CryptoTest { override fun getKey(): SecretKey = key } - private val crypto = - Crypto( + private val secureCrypto = + SecureCrypto( keyProvider = FakeKeyProvider(), transformation = "AES/CBC/PKCS5Padding", ) @@ -31,8 +32,8 @@ class CryptoTest { val original = "테스트 데이터".toByteArray() // when - val encrypted = crypto.encrypt(original) - val decrypted = crypto.decrypt(encrypted) + val encrypted = secureCrypto.encrypt(original) + val decrypted = secureCrypto.decrypt(encrypted) // then assertEquals(String(original), String(decrypted)) @@ -44,8 +45,8 @@ class CryptoTest { val input = "같은 입력".toByteArray() // when - val encrypted1 = crypto.encrypt(input) - val encrypted2 = crypto.encrypt(input) + val encrypted1 = secureCrypto.encrypt(input) + val encrypted2 = secureCrypto.encrypt(input) // then assertNotEquals(encrypted1.toList(), encrypted2.toList()) @@ -58,15 +59,15 @@ class CryptoTest { // when & then assertThrows(IllegalArgumentException::class.java) { - crypto.decrypt(invalid) + secureCrypto.decrypt(invalid) } } @Test fun `빈 바이트 배열 암호화 시 예외가 발생하지 않아야 한다`() { val input = ByteArray(0) - val encrypted = crypto.encrypt(input) - val decrypted = crypto.decrypt(encrypted) + val encrypted = secureCrypto.encrypt(input) + val decrypted = secureCrypto.decrypt(encrypted) assertEquals(String(input), String(decrypted)) } @@ -74,11 +75,11 @@ class CryptoTest { fun `IV 일부가 조작된 경우 복호화하면 원래 데이터와 달라야 한다`() { // given val original = "iv 테스트".toByteArray() - val encrypted = crypto.encrypt(original) + val encrypted = secureCrypto.encrypt(original) encrypted[0] = encrypted[0].inc() // when - val decrypted = crypto.decrypt(encrypted) + val decrypted = secureCrypto.decrypt(encrypted) // then assertNotEquals(String(original), String(decrypted)) @@ -88,12 +89,12 @@ class CryptoTest { fun `암호화된 데이터가 조작된 경우 복호화 실패해야 한다`() { // given val original = "데이터 조작".toByteArray() - val encrypted = crypto.encrypt(original) + val encrypted = secureCrypto.encrypt(original) encrypted[encrypted.lastIndex] = encrypted.last().inc() // when & then assertThrows(BadPaddingException::class.java) { - crypto.decrypt(encrypted) + secureCrypto.decrypt(encrypted) } } @@ -101,7 +102,7 @@ class CryptoTest { fun `다른 키로 복호화하면 실패해야 한다`() { // given val original = "다른 키 테스트".toByteArray() - val encrypted = crypto.encrypt(original) + val encrypted = secureCrypto.encrypt(original) val otherKeyProvider = object : KeyProvider { @@ -112,11 +113,11 @@ class CryptoTest { } } - val otherCrypto = Crypto(otherKeyProvider, "AES/CBC/PKCS5Padding") + val otherSecureCrypto = SecureCrypto(otherKeyProvider, "AES/CBC/PKCS5Padding") // when & then assertThrows(BadPaddingException::class.java) { - otherCrypto.decrypt(encrypted) + otherSecureCrypto.decrypt(encrypted) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b449fd4d..31939a41 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,7 @@ compose-compiler-gradle-plugin = { group = "org.jetbrains.kotlin", name = "compo androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } -androidx-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDatastore" } +androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDatastore" } androidx-lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" }