Skip to content

Commit 1d93680

Browse files
authored
Merge pull request #10 from YAPP-Github/feature/#9-auth-token-encryption
[Feature/#9] AuthToken 암복호화 로직 구현
2 parents 08fc652 + db59606 commit 1d93680

11 files changed

Lines changed: 241 additions & 0 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation(projects.core.datastore)
1717
implementation(projects.core.designsystem)
1818
implementation(projects.core.network)
19+
implementation(projects.core.security)
1920
implementation(projects.data)
2021
implementation(projects.domain)
2122
implementation(projects.presentation)

core/security/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

core/security/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
plugins {
2+
alias(libs.plugins.bitnagil.android.library)
3+
}
4+
5+
android {
6+
namespace = "com.threegap.bitnagil.security"
7+
}
8+
9+
dependencies {
10+
testImplementation(libs.androidx.junit)
11+
}

core/security/consumer-rules.pro

Whitespace-only changes.

core/security/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
</manifest>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.threegap.bitnagil.security
2+
3+
import android.security.keystore.KeyGenParameterSpec
4+
import android.security.keystore.KeyProperties
5+
import java.security.KeyStore
6+
import javax.crypto.KeyGenerator
7+
import javax.crypto.SecretKey
8+
9+
class AndroidKeyProvider : KeyProvider {
10+
private val keyStore =
11+
KeyStore
12+
.getInstance("AndroidKeyStore")
13+
.apply { load(null) }
14+
15+
override fun getKey(): SecretKey {
16+
val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry
17+
return existingKey?.secretKey ?: createKey()
18+
}
19+
20+
private fun createKey(): SecretKey {
21+
return KeyGenerator.getInstance(ALGORITHM).apply {
22+
init(
23+
KeyGenParameterSpec.Builder(
24+
KEY_ALIAS,
25+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
26+
).setBlockModes(BLOCK_MODE)
27+
.setEncryptionPaddings(PADDING)
28+
.setRandomizedEncryptionRequired(true)
29+
.setUserAuthenticationRequired(false)
30+
.build(),
31+
)
32+
}.generateKey()
33+
}
34+
35+
companion object {
36+
private const val KEY_ALIAS = "bitnagil_auth_token"
37+
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
38+
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
39+
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
40+
}
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.threegap.bitnagil.security
2+
3+
import javax.crypto.Cipher
4+
import javax.crypto.spec.IvParameterSpec
5+
6+
class Crypto(
7+
private val keyProvider: KeyProvider,
8+
private val transformation: String = "AES/CBC/PKCS7Padding",
9+
) {
10+
fun encrypt(bytes: ByteArray): ByteArray {
11+
val cipher = Cipher.getInstance(transformation)
12+
cipher.init(Cipher.ENCRYPT_MODE, keyProvider.getKey())
13+
val iv = cipher.iv
14+
val encrypted = cipher.doFinal(bytes)
15+
return iv + encrypted
16+
}
17+
18+
fun decrypt(bytes: ByteArray): ByteArray {
19+
val cipher = Cipher.getInstance(transformation)
20+
require(bytes.size >= cipher.blockSize) {
21+
INVALID_INPUT_TOO_SHORT_MSG
22+
}
23+
val iv = bytes.copyOfRange(0, cipher.blockSize)
24+
val data = bytes.copyOfRange(cipher.blockSize, bytes.size)
25+
cipher.init(Cipher.DECRYPT_MODE, keyProvider.getKey(), IvParameterSpec(iv))
26+
return cipher.doFinal(data)
27+
}
28+
29+
companion object {
30+
private const val INVALID_INPUT_TOO_SHORT_MSG = "[ERROR] 복호화할 수 없는 입력입니다."
31+
}
32+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.threegap.bitnagil.security
2+
3+
import javax.crypto.SecretKey
4+
5+
interface KeyProvider {
6+
fun getKey(): SecretKey
7+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.threegap.bitnagil.security
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertNotEquals
5+
import org.junit.Assert.assertThrows
6+
import org.junit.Test
7+
import javax.crypto.BadPaddingException
8+
import javax.crypto.KeyGenerator
9+
import javax.crypto.SecretKey
10+
11+
class CryptoTest {
12+
private class FakeKeyProvider : KeyProvider {
13+
private val key: SecretKey =
14+
KeyGenerator
15+
.getInstance("AES")
16+
.apply { init(128) }
17+
.generateKey()
18+
19+
override fun getKey(): SecretKey = key
20+
}
21+
22+
private val crypto =
23+
Crypto(
24+
keyProvider = FakeKeyProvider(),
25+
transformation = "AES/CBC/PKCS5Padding",
26+
)
27+
28+
@Test
29+
fun `암호화 후 복호화하면 원래 데이터와 같아야 한다`() {
30+
// given
31+
val original = "테스트 데이터".toByteArray()
32+
33+
// when
34+
val encrypted = crypto.encrypt(original)
35+
val decrypted = crypto.decrypt(encrypted)
36+
37+
// then
38+
assertEquals(String(original), String(decrypted))
39+
}
40+
41+
@Test
42+
fun `같은 데이터를 암호화해도 결과는 달라야 한다`() {
43+
// given
44+
val input = "같은 입력".toByteArray()
45+
46+
// when
47+
val encrypted1 = crypto.encrypt(input)
48+
val encrypted2 = crypto.encrypt(input)
49+
50+
// then
51+
assertNotEquals(encrypted1.toList(), encrypted2.toList())
52+
}
53+
54+
@Test
55+
fun `입력값이 너무 짧은 경우 IllegalArgumentException이 발생해야 한다`() {
56+
// given
57+
val invalid = ByteArray(4) { 0x00 }
58+
59+
// when & then
60+
assertThrows(IllegalArgumentException::class.java) {
61+
crypto.decrypt(invalid)
62+
}
63+
}
64+
65+
@Test
66+
fun `빈 바이트 배열 암호화 시 예외가 발생하지 않아야 한다`() {
67+
val input = ByteArray(0)
68+
val encrypted = crypto.encrypt(input)
69+
val decrypted = crypto.decrypt(encrypted)
70+
assertEquals(String(input), String(decrypted))
71+
}
72+
73+
@Test
74+
fun `IV 일부가 조작된 경우 복호화하면 원래 데이터와 달라야 한다`() {
75+
// given
76+
val original = "iv 테스트".toByteArray()
77+
val encrypted = crypto.encrypt(original)
78+
encrypted[0] = encrypted[0].inc()
79+
80+
// when
81+
val decrypted = crypto.decrypt(encrypted)
82+
83+
// then
84+
assertNotEquals(String(original), String(decrypted))
85+
}
86+
87+
@Test
88+
fun `암호화된 데이터가 조작된 경우 복호화 실패해야 한다`() {
89+
// given
90+
val original = "데이터 조작".toByteArray()
91+
val encrypted = crypto.encrypt(original)
92+
encrypted[encrypted.lastIndex] = encrypted.last().inc()
93+
94+
// when & then
95+
assertThrows(BadPaddingException::class.java) {
96+
crypto.decrypt(encrypted)
97+
}
98+
}
99+
100+
@Test
101+
fun `다른 키로 복호화하면 실패해야 한다`() {
102+
// given
103+
val original = "다른 키 테스트".toByteArray()
104+
val encrypted = crypto.encrypt(original)
105+
106+
val otherKeyProvider =
107+
object : KeyProvider {
108+
override fun getKey(): SecretKey {
109+
val keyGen = KeyGenerator.getInstance("AES")
110+
keyGen.init(128)
111+
return keyGen.generateKey()
112+
}
113+
}
114+
115+
val otherCrypto = Crypto(otherKeyProvider, "AES/CBC/PKCS5Padding")
116+
117+
// when & then
118+
assertThrows(BadPaddingException::class.java) {
119+
otherCrypto.decrypt(encrypted)
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)