Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
37395bf
feat/#8 : kakao SDK setting
dmp100 Jan 8, 2026
8903729
feat/#8 : kakao gradle, Manifest Setting
dmp100 Jan 9, 2026
9f97719
[merge] setting/#4 네비게이션 세팅 to feat/#8-kakao-token
dmp100 Jan 9, 2026
8119c05
feat/#8 : implement kakao login data layer
dmp100 Jan 10, 2026
3702ffe
feat/#8 : datastore,tink setting
dmp100 Jan 10, 2026
6275a0a
feat/#8 : TinkEncryption, impl refactor
dmp100 Jan 10, 2026
3b16e24
feat/#8 : AuthLocalDataSourceImpl
dmp100 Jan 10, 2026
85e3f62
feat/#8 : Network module to ServiceModule
dmp100 Jan 10, 2026
d7ab3bb
feat/#8 : 인증 로직을 위한 DI 설정 및 TinkEncryption 모듈 추가
dmp100 Jan 10, 2026
ac6ab4f
feat/#8 : Kakao Login UI & Navigation
dmp100 Jan 10, 2026
7c7e505
refactor/#8 : add Timber, suspendRunCatching
dmp100 Jan 10, 2026
1042987
[Merge] feat/#12 SnackBar to feat/#8-kakao-token
dmp100 Jan 10, 2026
525f190
refactor/#8 : 주소랑 BaseResponse 수정
dmp100 Jan 10, 2026
94176d1
refactor/#8 : move to common:sercurity
dmp100 Jan 11, 2026
f5c74fb
refactor/#8 : dir sercurity
dmp100 Jan 11, 2026
17c6265
refactor/#8 : TinkEncryption을 CryptoManager 인터페이스로 리팩토링
dmp100 Jan 11, 2026
dc0ce56
refactor/#8 : AuthLocalDataSourceImpl DataStore 싱글톤 구조 개선
dmp100 Jan 11, 2026
bc217c7
refactor/#8 : encryptedToken 빠진거 추가
dmp100 Jan 11, 2026
daaaff0
refactor/#8 : add black color to KakaoLoginBtn
dmp100 Jan 11, 2026
dcbcc3a
efactor/#8 : naming KakaoLoginScreen
dmp100 Jan 11, 2026
a7ecf2e
refactor/#8 : 카카오 로그인 연동 및 Context 의존성 구조 개선
dmp100 Jan 11, 2026
fdd9d7b
refactor/#8 : TinkCryptoManager 책임 분리
dmp100 Jan 11, 2026
257204e
refactor/#8 : naming KaKao -> AuthParent
dmp100 Jan 11, 2026
80b0d7d
refactor/#8 : move to auth/parent
dmp100 Jan 11, 2026
bce3fc0
refactor/#8 : delete object{}
dmp100 Jan 11, 2026
bd58645
refactor/#8 : 상태로 네비게이션 관리하기
dmp100 Jan 11, 2026
7cc7caa
refactor/#8 : naming ParentLoginViewModel
dmp100 Jan 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,24 @@ android {
versionName = libs.versions.versionName.get()

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

// Server URL to BuildConfig
buildConfigField("String", "BASE_URL", properties["base.url"].toString())

// KAKAO_NATIVE_APP_KEY
buildConfigField(
"String",
"KAKAO_NATIVE_APP_KEY",
"\"${properties["kakao.native.app.key"]}\"" // 명시적으로 따옴표 추가
)

// manifestPlaceholders for AndroidManifest
manifestPlaceholders["NATIVE_APP_KEY"] = properties["kakao.native.app.key"].toString()

// Todo : (Issue) LocalProperties의 "" 유무 및 일관성
//buildConfigField("String", "KAKAO_NATIVE_KEY", properties["kakao.native.app.key"].toString())
// manifestPlaceholders["NATIVE_APP_KEY"] = properties["kakao.native.app.key"].toString().replace("\"", "")

}

buildTypes {
Expand Down Expand Up @@ -78,4 +95,9 @@ dependencies {

implementation(libs.timber)

implementation(libs.kakao.user)


implementation(libs.androidx.datastore.preferences)
implementation(libs.tink.android)
}
25 changes: 24 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,27 @@

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

# 카카오 SDK의 모델 클래스는 JSON 변환에 사용되므로 난독화하지 않음
-keep class com.kakao.sdk.**.model.* { <fields>; }

# OkHttp 관련 선택적 보안 라이브러리 경고 무시
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.conscrypt.*
-dontwarn org.openjsse.**

# Retrofit2 (with r8 full mode)
# Retrofit API 인터페이스 보존 (어노테이션 기반이므로 필수)
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>

# 코루틴 Continuation 클래스 보존 (suspend 함수 지원용)
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

# Retrofit 메서드의 반환 타입 보존
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

# Retrofit Response 클래스 보존
-keep,allowobfuscation,allowshrinking class retrofit2.Response
21 changes: 20 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- 인터넷 권한 -->
<uses-permission android:name="android.permission.INTERNET" />

<application
Expand All @@ -14,17 +15,35 @@
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Kiero">

<!-- MainActivity -->
<activity
android:name="com.kiero.presentation.main.activity.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Kiero">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- KaKao_Login_Activity -->
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<!-- manifestPlaceholders from build.gradle.kts(App) -->
<data
android:host="oauth"
android:scheme="kakao${NATIVE_APP_KEY}" />
</intent-filter>
</activity>

</application>

</manifest>
12 changes: 11 additions & 1 deletion app/src/main/java/com/kiero/KieroApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.kiero

import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

Expand All @@ -11,6 +12,7 @@ class KieroApplication : Application() {
super.onCreate()
setTimber()
setDayMode()
initKakaoSdk()
}

private fun setTimber() {
Expand All @@ -22,5 +24,13 @@ class KieroApplication : Application() {
private fun setDayMode() {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
}

private fun initKakaoSdk() {
try {
KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
// 태그를 명시적으로 지정
Timber.tag("KAKAO_INIT").d("✅ 카카오 SDK 초기화 성공")
} catch (e: Exception) {
Timber.tag("KAKAO_INIT").e(e, "❌ 카카오 SDK 초기화 실패")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ object NetworkModule {
.addConverterFactory(converterFactory)
.client(client)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable

@Serializable
data class BaseResponse<T>(
@SerialName("code")
val code: String,
@SerialName("status")
val status: Int,
@SerialName("message")
val message: String,
@SerialName("data")
val data: T,
val data: T? = null,
)
// TODO : 명세서 확인 후 수정 예정

6 changes: 6 additions & 0 deletions app/src/main/java/com/kiero/core/security/CryptoManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kiero.core.security

interface CryptoManager {
fun encrypt(plaintext: String): String
fun decrypt(ciphertext: String): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.kiero.core.security

class EncryptionException(message: String, cause: Throwable? = null) :
Exception(message, cause)
92 changes: 92 additions & 0 deletions app/src/main/java/com/kiero/core/security/TinkCryptoManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.kiero.core.security

import android.content.Context
import android.util.Base64
import com.google.crypto.tink.Aead
import com.google.crypto.tink.InsecureSecretKeyAccess
import com.google.crypto.tink.KeysetHandle
import com.google.crypto.tink.TinkJsonProtoKeysetFormat
import com.google.crypto.tink.aead.AeadConfig
import com.google.crypto.tink.aead.PredefinedAeadParameters
import dagger.hilt.android.qualifiers.ApplicationContext
import jakarta.inject.Inject
import java.io.File
import java.nio.charset.StandardCharsets


class TinkCryptoManager @Inject constructor(
@param:ApplicationContext private val context: Context
) : CryptoManager {

private val aead: Aead

init {
try {
AeadConfig.register()

val keysetFile = File(context.filesDir, KEYSET_FILENAME)
val keysetHandle = if (keysetFile.exists()) {
loadKeysetHandle(keysetFile)
} else {
generateAndSaveKeysetHandle(keysetFile)
}

aead = keysetHandle.getPrimitive(Aead::class.java)
} catch (e: Exception) {
throw IllegalStateException("암호화 초기화 실패", e)
}
}

private fun generateAndSaveKeysetHandle(keysetFile: File): KeysetHandle {
return try {
val keysetHandle = KeysetHandle.generateNew(PredefinedAeadParameters.AES256_GCM)
val keysetJson = TinkJsonProtoKeysetFormat.serializeKeyset(
keysetHandle,
InsecureSecretKeyAccess.get()
)
keysetFile.writeText(keysetJson)
keysetHandle
} catch (e: Exception) {
throw EncryptionException("키셋 생성 및 저장 실패", e)
}
}

private fun loadKeysetHandle(keysetFile: File): KeysetHandle {
return try {
val keysetJson = keysetFile.readText()
TinkJsonProtoKeysetFormat.parseKeyset(
keysetJson,
InsecureSecretKeyAccess.get()
)
} catch (e: Exception) {
throw EncryptionException("키셋 로드 실패", e)
}
}

override fun encrypt(plaintext: String): String {
return try {
val plaintextBytes = plaintext.toByteArray(CHARSET)
val encryptedBytes = aead.encrypt(plaintextBytes, ASSOCIATED_DATA)
Base64.encodeToString(encryptedBytes, BASE64_FLAGS)
} catch (e: Exception) {
throw EncryptionException("암호화 실패", e)
}
}

override fun decrypt(ciphertext: String): String {
return try {
val encryptedBytes = Base64.decode(ciphertext, BASE64_FLAGS)
val decryptedBytes = aead.decrypt(encryptedBytes, ASSOCIATED_DATA)
String(decryptedBytes, CHARSET)
} catch (e: Exception) {
throw EncryptionException("복호화 실패", e)
}
}

companion object {
private const val KEYSET_FILENAME = "kiero_tink_keyset.json"
private val CHARSET = StandardCharsets.UTF_8
private const val BASE64_FLAGS = Base64.NO_WRAP
private val ASSOCIATED_DATA = null
}
}
24 changes: 24 additions & 0 deletions app/src/main/java/com/kiero/core/security/di/SecurityModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.kiero.core.security.di

import android.content.Context
import com.kiero.core.security.CryptoManager
import com.kiero.core.security.TinkCryptoManager
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 SecurityModule {

@Provides
@Singleton
fun providesCryptoManager(
@ApplicationContext context: Context
): CryptoManager {
return TinkCryptoManager(context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kiero.data.auth.local.datasource


interface AuthLocalDataSource {

// @param token JWT 액세스 토큰
suspend fun saveAccessToken(token: String)
// @param token JWT 리프레시 토큰
suspend fun saveRefreshToken(token: String)
// @return 저장된 액세스 토큰, 없으면 null
suspend fun getAccessToken(): String?
// @return 저장된 리프레시 토큰, 없으면 null
suspend fun getRefreshToken(): String?
// 토큰 삭제.
suspend fun clearTokens()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.kiero.data.auth.local.datasourceimpl

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.kiero.core.common.util.suspendRunCatching
import com.kiero.core.security.CryptoManager
import com.kiero.data.auth.local.datasource.AuthLocalDataSource
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject

private const val DATASTORE_NAME = "kiero_auth_datastore"
private val KEY_ACCESS_TOKEN = stringPreferencesKey("access_token")
private val KEY_REFRESH_TOKEN = stringPreferencesKey("refresh_token")

private val Context.authDataStore: DataStore<Preferences> by preferencesDataStore(
name = DATASTORE_NAME
)

class AuthLocalDataSourceImpl @Inject constructor(
@param:ApplicationContext private val context: Context,
private val cryptoManager: CryptoManager,
) : AuthLocalDataSource {

override suspend fun saveAccessToken(token: String) {
suspendRunCatching {
val encryptedToken = cryptoManager.encrypt(token)
context.authDataStore.edit { it[KEY_ACCESS_TOKEN] = encryptedToken }
}.onFailure { throwable ->
Timber.e(throwable, "AccessToken 저장 실패")
}
}

override suspend fun saveRefreshToken(token: String) {
suspendRunCatching {
val encryptedToken = cryptoManager.encrypt(token)
context.authDataStore.edit { it[KEY_REFRESH_TOKEN] = encryptedToken }
}.onFailure { throwable ->
Timber.e(throwable, "RefreshToken 저장 실패")
}
}

override suspend fun getAccessToken(): String? {
return suspendRunCatching {
val encryptedToken = context.authDataStore.data
.map { it[KEY_ACCESS_TOKEN] }
.first()
encryptedToken?.let { cryptoManager.decrypt(it) }
}.onFailure { throwable ->
Timber.e(throwable, "AccessToken 로드 실패")
}.getOrNull()
}

override suspend fun getRefreshToken(): String? {
return suspendRunCatching {
val encryptedToken = context.authDataStore.data
.map { it[KEY_REFRESH_TOKEN] }
.first()
encryptedToken?.let { cryptoManager.decrypt(it) }
}.onFailure { throwable ->
Timber.e(throwable, "RefreshToken 로드 실패")
}.getOrNull()
}

override suspend fun clearTokens() {
suspendRunCatching {
context.authDataStore.edit { it.clear() }
}.onFailure { throwable ->
Timber.e(throwable, "토큰 삭제 실패")
}
}
}
Loading