Skip to content

Commit

Permalink
[Feture/#845] 로컬 저장소 암호화를 위한 암호화 모듈 만들기 (#927)
Browse files Browse the repository at this point in the history
* [feature/#845] security module

* [feature/#845] SecurityExt

* [feature/#845] cryptoManager 적용

* [feature/#845] spotlessApply

* [feature/#845] EncryptedContent data class

* [feature/#845] SharedPreference get, set 함수 확장함수화 및 DEBUG 모드에서만 암호화 적용

* [feature/#845] getOrCreateSecretKey -> getSecretKey

* [feature/#845] cipher 객체 지역변수화

* [feature/#845] key값 수정

* [feature/#845] KEY_ALIAS 변경

* [feature/#845] KEY_ALIAS 변경으로 인한 merge builder, PR builder 수정

* [feature/#845] backup_rules.xml (sharedpref backup에서 제외)

* [feature/#845] CryptoManager 예외 처리

* [feature/#845] CryptoManager object화

* [feature/#845] hilt 재적용

* [feature/#845] DataStore가 CryptoManager를 알지 못하도록 수정

* [feature/#845] CryptoManagerTest

* [feature/#845] SoptDataStore 기본 값 수정

* [feature/#845] CryproManagerTest AndroidTest로 변경

* [feature/#845] testCryptoSuccess 추가 및 로직 수정

* [feature/#845] spotlessApply
  • Loading branch information
jihyunniiii authored Nov 22, 2024
1 parent 17e17d8 commit f616a8f
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 18 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/develop_PR_builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ jobs:
DEV_AMPLITUDE_KEY: ${{ secrets.SENTRY_DSN }}
AMPLITUDE_KEY: ${{ secrets.SENTRY_DSN }}
POKE_DATA_STORE_KEY: ${{ secrets.SENTRY_DSN }}
ACCESS_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
REFRESH_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
PLAYGROUND_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
USER_STATUS_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
PUSH_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
run: |
echo apiKey=\"$API_KEY\" >> ./local.properties
echo dataStoreKey=\"$DATA_STORE_KEY\" >> ./local.properties
Expand All @@ -63,6 +68,11 @@ jobs:
echo devAmplitudeKey=\"$DEV_AMPLITUDE_KEY\" >> ./local.properties
echo amplitudeKey=\"$AMPLITUDE_KEY\" >> ./local.properties
echo pokeDataStoreKey=\"$POKE_DATA_STORE_KEY\" >> ./local.properties
echo accessTokenKeyAlias=\"ACCESS_TOKEN_KEY_ALIAS\" >> ./local.properties
echo refreshTokenKeyAlias=\"REFRESH_TOKEN_KEY_ALIAS\" >> ./local.properties
echo playgroundTokenKeyAlias=\"PLAYGROUND_TOKEN_KEY_ALIAS\" >> ./local.properties
echo userStatusKeyAlias=\"USER_STATUS_KEY_ALIAS\" >> ./local.properties
echo pushTokenKeyAlias=\"PUSH_TOKEN_KEY_ALIAS\" >> ./local.properties
- name: Access Firebase Service
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/develop_merge_builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ jobs:
DEV_AMPLITUDE_KEY: ${{ secrets.SENTRY_DSN }}
AMPLITUDE_KEY: ${{ secrets.SENTRY_DSN }}
POKE_DATA_STORE_KEY: ${{ secrets.SENTRY_DSN }}
ACCESS_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
REFRESH_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
PLAYGROUND_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
USER_STATUS_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
PUSH_TOKEN_KEY_ALIAS: ${{ secrets.SENTRY_DSN }}
run: |
echo apiKey=\"$API_KEY\" >> ./local.properties
echo dataStoreKey=\"$DATA_STORE_KEY\" >> ./local.properties
Expand All @@ -63,6 +68,11 @@ jobs:
echo devAmplitudeKey=\"$DEV_AMPLITUDE_KEY\" >> ./local.properties
echo amplitudeKey=\"$AMPLITUDE_KEY\" >> ./local.properties
echo pokeDataStoreKey=\"$POKE_DATA_STORE_KEY\" >> ./local.properties
echo accessTokenKeyAlias=\"ACCESS_TOKEN_KEY_ALIAS\" >> ./local.properties
echo refreshTokenKeyAlias=\"REFRESH_TOKEN_KEY_ALIAS\" >> ./local.properties
echo playgroundTokenKeyAlias=\"PLAYGROUND_TOKEN_KEY_ALIAS\" >> ./local.properties
echo userStatusKeyAlias=\"USER_STATUS_KEY_ALIAS\" >> ./local.properties
echo pushTokenKeyAlias=\"PUSH_TOKEN_KEY_ALIAS\" >> ./local.properties
- name: Access Firebase Service
run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ dependencies {
implementation(projects.core.network)
implementation(projects.core.auth)
implementation(projects.core.authimpl)
implementation(projects.core.security)
implementation(projects.core.webview)
implementation(projects.feature.auth)
implementation(projects.feature.mypage)
Expand Down
5 changes: 1 addition & 4 deletions app/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,5 @@
SOFTWARE.
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
<exclude domain="sharedpref" path="sampleKey.xml"/>
</full-backup-content>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ internal fun Project.configureAndroidCommonPlugin() {
val operationUrl = properties["operationApi"] as? String ?: ""
val devAmplitudeKey = properties["devAmplitudeKey"] as? String ?: ""
val amplitudeKey = properties["amplitudeKey"] as? String ?: ""
val accessTokenKeyAlias = properties["accessTokenKeyAlias"] as? String ?: ""
val refreshTokenKeyAlias = properties["refreshTokenKeyAlias"] as? String ?: ""
val playgroundTokenKeyAlias = properties["playgroundTokenKeyAlias"] as? String ?: ""
val userStatusKeyAlias = properties["userStatusKeyAlias"] as? String ?: ""
val pushTokenKeyAlias = properties["pushTokenKeyAlias"] as? String ?: ""
buildConfigField("String", "SOPTAMP_API_KEY", apiKey)
buildConfigField("String", "SOPTAMP_DATA_STORE_KEY", dataStoreKey)
buildConfigField("String", "POKE_DATA_STORE_KEY", pokeDataStoreKey)
Expand All @@ -41,6 +46,12 @@ internal fun Project.configureAndroidCommonPlugin() {
buildConfigField("String", "SOPT_OPERATION_BASE_URL", operationUrl)
buildConfigField("String", "DEV_AMPLITUDE_KEY", devAmplitudeKey)
buildConfigField("String", "AMPLITUDE_KEY", amplitudeKey)
buildConfigField("String", "ACCESS_TOKEN_KEY_ALIAS", accessTokenKeyAlias)
buildConfigField("String", "REFRESH_TOKEN_KEY_ALIAS", refreshTokenKeyAlias)
buildConfigField("String", "PLAYGROUND_TOKEN_KEY_ALIAS", playgroundTokenKeyAlias)
buildConfigField("String", "USER_STATUS_KEY_ALIAS", userStatusKeyAlias)
buildConfigField("String", "PUSH_TOKEN_KEY_ALIAS", pushTokenKeyAlias)

}
buildFeatures.apply {
viewBinding = true
Expand Down
1 change: 1 addition & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ android {

dependencies {
implementation(projects.core.auth)
implementation(projects.core.security)
implementation(platform(libs.okhttp.bom))
implementation(libs.okhttp)
implementation(libs.exifinterface)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* MIT License
* Copyright 2024 SOPT - Shout Our Passion Together
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.sopt.official.common.util

import org.sopt.official.common.BuildConfig
import org.sopt.official.security.util.getDecryptedDataOrDefault
import org.sopt.official.security.util.getEncryptedDataOrDefault

fun String.encryptInReleaseMode(keyAlias: String) = if (BuildConfig.DEBUG) this else this.getEncryptedDataOrDefault(keyAlias = keyAlias)

fun String.decryptInReleaseMode(keyAlias: String, initializationVectorSize: Int = 12) =
if (BuildConfig.DEBUG) this else this.getDecryptedDataOrDefault(
keyAlias = keyAlias,
initializationVectorSize = initializationVectorSize
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.sopt.official.common.di.LocalStore
import org.sopt.official.common.file.createSharedPreference
import org.sopt.official.common.util.decryptInReleaseMode
import org.sopt.official.common.util.encryptInReleaseMode
import org.sopt.official.network.BuildConfig.ACCESS_TOKEN_KEY_ALIAS
import org.sopt.official.network.BuildConfig.PLAYGROUND_TOKEN_KEY_ALIAS
import org.sopt.official.network.BuildConfig.PUSH_TOKEN_KEY_ALIAS
import org.sopt.official.network.BuildConfig.REFRESH_TOKEN_KEY_ALIAS
import org.sopt.official.network.BuildConfig.USER_STATUS_KEY_ALIAS
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SoptDataStore @Inject constructor(
@ApplicationContext private val context: Context,
@LocalStore private val fileName: String,
@LocalStore private val fileName: String
) {
private val store = createSharedPreference(fileName, context)

Expand All @@ -49,35 +56,35 @@ class SoptDataStore @Inject constructor(
}

var accessToken: String
set(value) = store.edit { putString(ACCESS_TOKEN, value) }
get() = store.getString(ACCESS_TOKEN, "") ?: ""
set(value) = store.edit { putString(ACCESS_TOKEN, value.encryptInReleaseMode(keyAlias = ACCESS_TOKEN_KEY_ALIAS)) }
get() = store.getString(ACCESS_TOKEN, null)?.decryptInReleaseMode(keyAlias = ACCESS_TOKEN_KEY_ALIAS) ?: DEFAULT_VALUE

var refreshToken: String
set(value) = store.edit { putString(REFRESH_TOKEN, value) }
get() = store.getString(REFRESH_TOKEN, "") ?: ""
set(value) = store.edit { putString(REFRESH_TOKEN, value.encryptInReleaseMode(keyAlias = REFRESH_TOKEN_KEY_ALIAS)) }
get() = store.getString(REFRESH_TOKEN, null)?.decryptInReleaseMode(keyAlias = REFRESH_TOKEN_KEY_ALIAS)
?: DEFAULT_VALUE

var playgroundToken: String
set(value) = store.edit { putString(PLAYGROUND_TOKEN, value) }
get() = store.getString(PLAYGROUND_TOKEN, "") ?: ""
set(value) = store.edit { putString(PLAYGROUND_TOKEN, value.encryptInReleaseMode(keyAlias = PLAYGROUND_TOKEN_KEY_ALIAS)) }
get() = store.getString(PLAYGROUND_TOKEN, null)?.decryptInReleaseMode(keyAlias = PLAYGROUND_TOKEN_KEY_ALIAS)
?: DEFAULT_VALUE

var userStatus: String
set(value) = store.edit { putString(USER_STATUS, value) }
get() = store.getString(USER_STATUS, UNAUTHENTICATED) ?: UNAUTHENTICATED
set(value) = store.edit { putString(USER_STATUS, value.encryptInReleaseMode(keyAlias = USER_STATUS_KEY_ALIAS)) }
get() = store.getString(USER_STATUS, null)?.decryptInReleaseMode(keyAlias = USER_STATUS_KEY_ALIAS) ?: UNAUTHENTICATED

var pushToken: String
set(value) = store.edit { putString(PUSH_TOKEN, value) }
get() = store.getString(PUSH_TOKEN, "") ?: ""
set(value) = store.edit { putString(PUSH_TOKEN, value.encryptInReleaseMode(keyAlias = PUSH_TOKEN_KEY_ALIAS)) }
get() = store.getString(PUSH_TOKEN, null)?.decryptInReleaseMode(keyAlias = PUSH_TOKEN_KEY_ALIAS) ?: DEFAULT_VALUE

companion object {
const val DEBUG_FILE_NAME = "sopt_debug"
private const val ACCESS_TOKEN = "access_token"
private const val REFRESH_TOKEN = "refresh_token"
private const val PLAYGROUND_TOKEN = "pg_token"
private const val USER_STATUS = "user_status"
private const val KEY_ALIAS_AUTH = "alias.preferences.auth_token"
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val PUSH_TOKEN = "push_token"
private const val UNAUTHENTICATED = "UNAUTHENTICATED"
private const val DEFAULT_VALUE = ""
}
}

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
32 changes: 32 additions & 0 deletions core/security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* MIT License
* Copyright 2024 SOPT - Shout Our Passion Together
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
plugins {
sopt("feature")
sopt("test")
}

android {
namespace = "org.sopt.official.security"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* MIT License
* Copyright 2024 SOPT - Shout Our Passion Together
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.sopt.official.security

import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.sopt.official.security.util.getDecryptedDataOrDefault
import org.sopt.official.security.util.getEncryptedDataOrDefault

class CryptoManagerTest {

@ParameterizedTest
@MethodSource("generateEncryptedTestData")
@DisplayName("keyAlias와 암호화 할 데이터를 입력하면 암호화를 진행한다")
fun testEncryptSuccess(keyAlias: String, bytes: ByteArray) {
// when
val encryptedResult = CryptoManager.encrypt(keyAlias = keyAlias, bytes = bytes)

// then
assertTrue(encryptedResult.isSuccess, "암호화가 성공적으로 진행되었습니다.")
}

@ParameterizedTest
@MethodSource("generateCryptoTestData")
@DisplayName("keyAlias와 데이터를 입력하면 암호화, 복호화를 순차적으로 진행해 기존의 데이터를 반환한다")
fun testCryptoSuccess(keyAlias: String, data: String) {
// given
val encryptedData = data.getEncryptedDataOrDefault(keyAlias = keyAlias)

// when
val decryptedData = encryptedData.getDecryptedDataOrDefault(keyAlias = keyAlias)

// then
assertThat(decryptedData).isEqualTo(data)
}

companion object {
@JvmStatic
fun generateEncryptedTestData() = listOf(
Arguments.of("ACCESS_TOKEN", "accessToken".toByteArray()),
Arguments.of("REFRESH_TOKEN", "refreshToken".toByteArray()),
Arguments.of("PLAYGROUND_TOKEN", "playgroundToken".toByteArray()),
Arguments.of("USER_STATUS", "userStatus".toByteArray()),
Arguments.of("PUSH_TOKEN", "pushToken".toByteArray())
)

@JvmStatic
fun generateCryptoTestData() = listOf(
Arguments.of("ACCESS_TOKEN", "accessToken"),
Arguments.of("REFRESH_TOKEN", "refreshToken"),
Arguments.of("PLAYGROUND_TOKEN", "playgroundToken"),
Arguments.of("USER_STATUS", "userStatus"),
Arguments.of("PUSH_TOKEN", "pushToken")
)
}
}
Loading

0 comments on commit f616a8f

Please sign in to comment.