Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation(projects.core.datastore)
implementation(projects.core.designsystem)
implementation(projects.core.network)
implementation(projects.core.security)
implementation(projects.data)
implementation(projects.domain)
implementation(projects.presentation)
Expand Down
1 change: 1 addition & 0 deletions core/security/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
11 changes: 11 additions & 0 deletions core/security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.bitnagil.android.library)
}

android {
namespace = "com.threegap.bitnagil.security"
}

dependencies {
testImplementation(libs.androidx.junit)
}
Empty file.
21 changes: 21 additions & 0 deletions core/security/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
4 changes: 4 additions & 0 deletions core/security/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.threegap.bitnagil.security

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

class AndroidKeyProvider : KeyProvider {
private val keyStore =
KeyStore
.getInstance("AndroidKeyStore")
.apply { load(null) }
Comment thread
l5x5l marked this conversation as resolved.

override fun getKey(): SecretKey {
val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey()
}
Comment thread
l5x5l marked this conversation as resolved.

private fun createKey(): SecretKey {
return KeyGenerator.getInstance(ALGORITHM).apply {
init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
).setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(PADDING)
.setRandomizedEncryptionRequired(true)
.setUserAuthenticationRequired(false)
.build(),
)
}.generateKey()
}

companion object {
private const val KEY_ALIAS = "bitnagil_auth_token"
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.threegap.bitnagil.security

import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec

class Crypto(
private val keyProvider: KeyProvider,
private val transformation: String = "AES/CBC/PKCS7Padding",
) {
fun encrypt(bytes: ByteArray): ByteArray {
val cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.ENCRYPT_MODE, keyProvider.getKey())
val iv = cipher.iv
val encrypted = cipher.doFinal(bytes)
return iv + encrypted
}

fun decrypt(bytes: ByteArray): ByteArray {
val cipher = Cipher.getInstance(transformation)
require(bytes.size >= cipher.blockSize) {
INVALID_INPUT_TOO_SHORT_MSG
}
val iv = bytes.copyOfRange(0, cipher.blockSize)
val data = bytes.copyOfRange(cipher.blockSize, bytes.size)
cipher.init(Cipher.DECRYPT_MODE, keyProvider.getKey(), IvParameterSpec(iv))
return cipher.doFinal(data)
}

companion object {
private const val INVALID_INPUT_TOO_SHORT_MSG = "[ERROR] 복호화할 수 없는 입력입니다."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.threegap.bitnagil.security

import javax.crypto.SecretKey

interface KeyProvider {
fun getKey(): SecretKey
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.threegap.bitnagil.security

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import javax.crypto.BadPaddingException
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

class CryptoTest {
private class FakeKeyProvider : KeyProvider {
private val key: SecretKey =
KeyGenerator
.getInstance("AES")
.apply { init(128) }
.generateKey()

override fun getKey(): SecretKey = key
}

private val crypto =
Crypto(
keyProvider = FakeKeyProvider(),
transformation = "AES/CBC/PKCS5Padding",
)
Comment thread
l5x5l marked this conversation as resolved.

@Test
fun `암호화 후 복호화하면 원래 데이터와 같아야 한다`() {
// given
val original = "테스트 데이터".toByteArray()

// when
val encrypted = crypto.encrypt(original)
val decrypted = crypto.decrypt(encrypted)

// then
assertEquals(String(original), String(decrypted))
}

@Test
fun `같은 데이터를 암호화해도 결과는 달라야 한다`() {
// given
val input = "같은 입력".toByteArray()

// when
val encrypted1 = crypto.encrypt(input)
val encrypted2 = crypto.encrypt(input)

// then
assertNotEquals(encrypted1.toList(), encrypted2.toList())
}

@Test
fun `입력값이 너무 짧은 경우 IllegalArgumentException이 발생해야 한다`() {
// given
val invalid = ByteArray(4) { 0x00 }

// when & then
assertThrows(IllegalArgumentException::class.java) {
crypto.decrypt(invalid)
}
}

@Test
fun `빈 바이트 배열 암호화 시 예외가 발생하지 않아야 한다`() {
val input = ByteArray(0)
val encrypted = crypto.encrypt(input)
val decrypted = crypto.decrypt(encrypted)
assertEquals(String(input), String(decrypted))
}

@Test
fun `IV 일부가 조작된 경우 복호화하면 원래 데이터와 달라야 한다`() {
// given
val original = "iv 테스트".toByteArray()
val encrypted = crypto.encrypt(original)
encrypted[0] = encrypted[0].inc()

// when
val decrypted = crypto.decrypt(encrypted)

// then
assertNotEquals(String(original), String(decrypted))
}
Comment thread
l5x5l marked this conversation as resolved.

@Test
fun `암호화된 데이터가 조작된 경우 복호화 실패해야 한다`() {
// given
val original = "데이터 조작".toByteArray()
val encrypted = crypto.encrypt(original)
encrypted[encrypted.lastIndex] = encrypted.last().inc()

// when & then
assertThrows(BadPaddingException::class.java) {
crypto.decrypt(encrypted)
}
}

@Test
fun `다른 키로 복호화하면 실패해야 한다`() {
// given
val original = "다른 키 테스트".toByteArray()
val encrypted = crypto.encrypt(original)

val otherKeyProvider =
object : KeyProvider {
override fun getKey(): SecretKey {
val keyGen = KeyGenerator.getInstance("AES")
keyGen.init(128)
return keyGen.generateKey()
}
}

val otherCrypto = Crypto(otherKeyProvider, "AES/CBC/PKCS5Padding")

// when & then
assertThrows(BadPaddingException::class.java) {
otherCrypto.decrypt(encrypted)
}
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ include(":app")
include(":core:datastore")
include(":core:designsystem")
include(":core:network")
include(":core:security")
include(":data")
include(":domain")
include(":presentation")