Android/Module

[Android Multi module] DataStore protobuf 암호화/복호화 적용

whjungdev 2024. 9. 13. 21:06

이전에 작성된 내용으로 DataStore protobuf Read/Write를 사용하게 되면 저장된 파일을 봤을 때 평문으로 저장되는 타입이 생깁니다.

 

저장되는 데이터를 직접 확인해보기 위해서 test_data.proto 파일에
string 타입을 추가해보았습니다.

syntax = "proto3";

option java_package = "com.example.sampleapp.core.datastore";
option java_multiple_files = true;

message TestData {
  bool isTest = 1;
  string test = 2;
}

그리고 string 테스트 값을 넣어보았습니다.

viewModel.updateTestString("hi")

 

Android Studio Tool의 View > Device Explorer를 선택합니다.

/data/data/App Package(설치한 앱 패키지명)/files/datastore/.pb 파일
해당 경로에 파일을 더블클릭해서 열게되면 데이터를 확인해볼 수 있습니다.

boolean 값은 알아볼 수 없는 값으로 저장되었지만, string type 경우 평문으로 저장된 것을 확인해 볼 수 있습니다.

protoBuf 방식으로 저장하는 부분에서 암호화/복호화를 넣어서 Access Token 같은 값들을 보안 처리를 하고 싶었습니다.

이를 위해 구글 tink 라이브러리를 세팅을 해서 처리하였습니다. (완벽한 방법인지 좀 더 조사가 필요합니다.)

 

1. libs.version.toml 파일에 암호화/복호화 라이브러리 추가

tink="1.7.0"
crypto-tink-android = { group = "com.google.crypto.tink", name = "tink-android", version.ref = "tink"}

 

2. tink 라이브러리의 암호화/복호화를 이용한 처리
Crpyo.kt File

interface Crypto {
    fun encrypt(text: ByteArray): ByteArray

    fun decrypt(encryptedData: ByteArray): ByteArray
}

class CryptoImpl @Inject constructor(
    private val aead: Aead
) : Crypto {
    override fun encrypt(text: ByteArray): ByteArray {
        return aead.encrypt(text, null)
    }

    override fun decrypt(encryptedData: ByteArray): ByteArray {
        return if (encryptedData.isNotEmpty()) {
            aead.decrypt(encryptedData, null)
        } else {
            encryptedData
        }
    }
}

TestSerializer.kt File

class TestSerializer @Inject constructor(
    private val crypto: Crypto
) : Serializer<TestData> {
    override val defaultValue: TestData get() = TestData.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): TestData {
        return try {
            TestData.parseFrom(
                input
                    .readBytes()
                    .let(crypto::decrypt)
            )
        } catch (e: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", e)
        }
    }

    override suspend fun writeTo(t: TestData, output: OutputStream) {
        output.write(crypto.encrypt(t.toByteArray()))
        output.flush()
    }
}

DataStoreModule.kt File

@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
    private const val TEST_DATASTORE_FILE_NAME = "TEST_DATA.pb"
    private const val KEYSET_NAME = "__androidx_encrypted_prefs_keyset__"
    private const val PREF_FILE_NAME = "androidx_secret_prefs"
    private const val MASTER_KEY_URI =
        "${AndroidKeystoreKmsClient.PREFIX}androidx_secret_sample_master_key"

    @Provides
    @Singleton
    @Named("test")
    fun provideTestDataStore(
        @ApplicationContext context: Context,
        aead: Aead
    ): DataStore<TestData> = DataStoreFactory.create(
        produceFile = { File(context.filesDir, "datastore/$TEST_DATASTORE_FILE_NAME") },
        serializer = TestSerializer(CryptoImpl(aead))
    )

    @Provides
    @Singleton
    fun aead(@ApplicationContext context: Context): Aead {
        AeadConfig.register()

        return AndroidKeysetManager
            .Builder()
            .withSharedPref(context, KEYSET_NAME, PREF_FILE_NAME)
            .withKeyTemplate(KeyTemplates.get("AES256_GCM"))
            .withMasterKeyUri(MASTER_KEY_URI)
            .build()
            .keysetHandle
            .getPrimitive(Aead::class.java)
    }
}

빌드해서 테스트 해보면

 


Preference 파일에 keyset 내용이 생성됩니다. (이 부분에 대해서는 추후에 좀 더 자세히 작성을 해보려고 합니다.)

다시 TEST_DATA.pb 파일을 접속해서 보면 암호화 부분이 적용되어 알 수 없는 내용으로 적용되며 파일의 용량 크기가 암호화로 인해서 늘어남을 확인해볼 수 있습니다.

sample : https://github.com/woohyun-jeong/AndroidSampleApp/tree/datastore_test1

Reference