Skip to content

Commit 0c8e5c7

Browse files
committed
feat: apply CodeRabbit's suggestions
1 parent 11e2636 commit 0c8e5c7

6 files changed

Lines changed: 79 additions & 28 deletions

File tree

app/src/main/java/com/debatetimer/app/data/model/TokenBundle.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ import kotlinx.serialization.Serializable
66
data class TokenBundle(
77
val accessToken: String,
88
val refreshToken: String,
9-
)
9+
) {
10+
override fun toString(): String {
11+
return "TokenBundle(accessToken='***', refreshToken='***')"
12+
}
13+
}

app/src/main/java/com/debatetimer/app/data/repo/TokenRepo.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ package com.debatetimer.app.data.repo
33
import com.debatetimer.app.data.model.TokenBundle
44

55
interface TokenRepo {
6-
suspend fun getTokens(): Result<TokenBundle>
7-
suspend fun saveTokens(bundle: TokenBundle): Result<Boolean>
6+
suspend fun getTokens(): Result<TokenBundle?>
7+
suspend fun saveTokens(bundle: TokenBundle): Result<Unit>
88
}

app/src/main/java/com/debatetimer/app/data/repo/TokenRepoImpl.kt

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.debatetimer.app.data.model.TokenBundle
77
import com.debatetimer.app.data.serializer.tokenDataStore
88
import com.debatetimer.app.util.crypto.CryptoManager
99
import com.google.gson.Gson
10+
import com.google.gson.JsonSyntaxException
1011
import com.google.protobuf.kotlin.toByteString
1112
import dagger.hilt.android.qualifiers.ApplicationContext
1213
import kotlinx.coroutines.flow.first
@@ -17,41 +18,53 @@ class TokenRepoImpl @Inject constructor(
1718
private val cryptoManager: CryptoManager
1819
) :
1920
TokenRepo {
21+
companion object {
22+
private val gson = Gson().newBuilder()
23+
.disableHtmlEscaping()
24+
.create()
25+
}
26+
2027
private val tokenDataStore: DataStore<EncryptedTokens> = context.tokenDataStore
2128

22-
private suspend fun getBundle(): TokenBundle? {
29+
override suspend fun getTokens(): Result<TokenBundle?> {
2330
try {
2431
val encryptedTokens = tokenDataStore.data.first()
32+
33+
if (encryptedTokens.encryptedTokenBundle.isEmpty || encryptedTokens.initializationVector.isEmpty) {
34+
return Result.success(null)
35+
}
36+
2537
val ciphertext = encryptedTokens.encryptedTokenBundle.toByteArray()
2638
val iv = encryptedTokens.initializationVector.toByteArray()
2739
val decryptedTokens = cryptoManager.decrypt(ciphertext, iv)
2840

29-
return Gson().fromJson(decryptedTokens, TokenBundle::class.java)
30-
} catch (_: NoSuchElementException) {
31-
return null
32-
} catch (e: Exception) {
33-
throw e
34-
}
35-
}
36-
37-
override suspend fun getTokens(): Result<TokenBundle> {
38-
try {
39-
val bundle = getBundle()
41+
val bundle = gson.fromJson(decryptedTokens, TokenBundle::class.java)
4042

4143
return if (bundle != null) {
4244
Result.success(bundle)
4345
} else {
4446
Result.failure(NoSuchElementException())
4547
}
48+
} catch (_: NoSuchElementException) {
49+
return Result.success(null)
50+
} catch (e: JsonSyntaxException) {
51+
return Result.failure(IllegalStateException("Corrupted token data", e))
4652
} catch (e: Exception) {
4753
return Result.failure(e)
4854
}
4955
}
5056

51-
override suspend fun saveTokens(bundle: TokenBundle): Result<Boolean> {
57+
override suspend fun saveTokens(bundle: TokenBundle): Result<Unit> {
5258
try {
59+
require(bundle.accessToken.isNotBlank())
60+
require(bundle.refreshToken.isNotBlank())
61+
5362
tokenDataStore.updateData { current ->
54-
val serializedBundle = Gson().toJson(bundle)
63+
val serializedBundle = try {
64+
gson.toJson(bundle)
65+
} catch (e: Exception) {
66+
throw IllegalStateException("Failed to serialize token bundle", e)
67+
}
5568
val encryptionOutput = cryptoManager.encrypt(serializedBundle)
5669

5770
current.toBuilder()
@@ -60,7 +73,7 @@ class TokenRepoImpl @Inject constructor(
6073
.build()
6174
}
6275

63-
return Result.success(true)
76+
return Result.success(Unit)
6477
} catch (e: Exception) {
6578
return Result.failure(e)
6679
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
package com.debatetimer.app.util.crypto
22

3+
/**
4+
* 토큰의 암호화 및 복호화를 담당하는 인터페이스
5+
*/
36
interface CryptoManager {
7+
/**
8+
* 평문을 암호화합니다.
9+
* @param plainText 암호화 할 평문
10+
* @return 암호화된 암호문와 IV의 Pair
11+
*/
412
fun encrypt(plainText: String): Pair<ByteArray, ByteArray>
13+
14+
/**
15+
* 암호문을 평문으로 복호화합니다.
16+
* @param encryptedText 복호화 할 암호문
17+
* @param iv IV
18+
* @return 복호화된 평문
19+
*/
520
fun decrypt(encryptedText: ByteArray, iv: ByteArray): String
621
}

app/src/main/java/com/debatetimer/app/util/crypto/CryptoManagerImpl.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ class CryptoManagerImpl @Inject constructor() : CryptoManager {
2222
}
2323

2424
// Keystore instance
25-
private val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
26-
load(null)
25+
private val keyStore by lazy {
26+
try {
27+
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
28+
load(null)
29+
}
30+
} catch (e: Exception) {
31+
throw RuntimeException("Failed to load Android KeyStore", e)
32+
}
2733
}
2834

2935
/**
@@ -56,25 +62,36 @@ class CryptoManagerImpl @Inject constructor() : CryptoManager {
5662
}
5763

5864
override fun encrypt(plainText: String): Pair<ByteArray, ByteArray> {
65+
// 0. Check whether plainText is not empty
66+
require(plainText.isNotEmpty()) { "Plain text cannot be empty" }
67+
5968
// 1. Load cipher instance
6069
val cipher = Cipher.getInstance(TRANSFORMATION)
6170

6271
// 2. Init cipher with encryption mode
6372
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
6473

6574
// 3. Return encrypted text and IV
66-
return Pair(cipher.doFinal(plainText.toByteArray()), cipher.iv)
75+
return Pair(cipher.doFinal(plainText.toByteArray(charset = Charsets.UTF_8)), cipher.iv)
6776
}
6877

6978
override fun decrypt(encryptedText: ByteArray, iv: ByteArray): String {
70-
// 1. Load cipher instance and prepare variables
71-
val cipher = Cipher.getInstance(TRANSFORMATION)
72-
val spec = GCMParameterSpec(128, iv)
79+
require(encryptedText.isNotEmpty()) { "Encrypted text cannot be empty" }
80+
require(iv.isNotEmpty()) { "IV cannot be empty" }
81+
require(iv.size == 12) { "IV must be 12 bytes" }
7382

74-
// 2. Init cipher with decryption mode
75-
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
83+
try {
84+
// 1. Load cipher instance and prepare variables
85+
val cipher = Cipher.getInstance(TRANSFORMATION)
86+
val spec = GCMParameterSpec(128, iv)
7687

77-
// 3. Return decrypted text
78-
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
88+
// 2. Init cipher with decryption mode
89+
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
90+
91+
// 3. Return decrypted text
92+
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
93+
} catch (e: Exception) {
94+
throw RuntimeException("Failed to decrypt data", e)
95+
}
7996
}
8097
}

app/src/main/proto/encrypted_tokens.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
syntax = "proto3";
22

3+
package com.debatetimer.app.data;
4+
35
option java_package = "com.debatetimer.app";
46
option java_multiple_files = true;
57

0 commit comments

Comments
 (0)