💡 / 키를 눌러 빠르게 검색하세요!

AndroidX에 추가된 Android Security 라이브러리는?

본문

AndroidX에 Android Security 라이브러리를 추가해주었는데, 첫 발표는 Google I/O에서 alpha1을 배포했었고, 같은 달 alpha2를 업데이트했다.

다음 영상은 DevSummit 2019 발표 영상이다.


이 글을 보기 전에

  • 보안을 깊이 다루지는 않는다.
  • androidx에 추가된 security에 관한 내용을 담는다.


Security 라이브러리를 알아보기 전에

androidX에 Security 라이브러리를 포함해주었다. 이미 많은 개발자들은 이것과 무관하게 암호화 라이브러리를 매우 잘 활용하고 있다. AES(Advanced Encryption Standard) 관련 코드는 StackOverflow에서 흔하게 확인 가능한데 그중 구글의 영상에서 하나의 예로 다음 글을 소개하고 있다.

Android encryption / decryption using AES [closed]

이 코드를 기반으로 Security를 좀 더 쉽게 활용 가능하도록 만들어준 라이브러리이다.

private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    byte[] encrypted = cipher.doFinal(clear);
    return encrypted;
}

private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, skeySpec);
    byte[] decrypted = cipher.doFinal(encrypted);
    return decrypted;
}

결국 구글에서는 이러한 코드를 매번 복사해서 사용하는 것 대신 직접 제공하고, 동시에 구글에서 직접 관리하는 라이브러리를 활용하도록 제공하게 된 것이다.

참고로 Cryptography - 링크 문서를 통해 안드로이드에서 제공하는 암호화 방식을 추가로 확인할 수 있으며, Security 라이브러리 샘플 코드를 함께 확인할 수 있다.(본 문서는 한국어 버전이 아닌 영문 문서로 확인하길 추천한다.)


Security library

Security 라이브러리는 Android 6.0(Marshmallow)에 추가된 Android KeyStore - 링크를 활용한 암호화를 제공하는 라이브러리이다. 하위에서 이를 제공하지는 않고 있어, Android Security 라이브러리는 minSdk 23 이상에서 활용할 수 있다.

참고로 minSdk 23이 더 이상 높지 않다. 서비스 제공사 기준 10% 미만의 점유율을 보일 수 있어, 충분히 검토하면 올릴 수 있는 수치이다. 구글에서 제공하는 DashBoard의 경우 2019년 5월 이후 업데이트가 없어 약 60% 이상에서 제공 가능하다고 나오나, 현 수치와는 다르다.

암호화 관련 라이브러리인 tink - 링크를 이용하고있는데, Google에서 직접 제공하는 Multi-language, cross-platform 환경을 지원하는 Open source 프로젝트를 사용하고있다.

Security 라이브러리는 크게 2가지 서비스와 Android KeyStore 활용한 MasterKeys generator을 제공한다.

  • 암호화 관련 : EncryptedFile, EncryptedSharedPreferences
  • KeyAlias 생성 : Android KeyStore를 활용하여 MasterKeys generator 제공


Security library 사용해보기

File 암호화와 SharedPreferences 암호화를 각각 살펴보겠다. 먼저 EncryptedSharedPreferences을 이용해 보자.

참고 문서는 security/data - 링크을 확인할 수 있다.

dependencies 추가

dependencies를 추가해주면 되겠다 minSdk는 23이어야 한다.

dependencies {
    implementation "androidx.security:security-crypto:1.0.0-alpha02"
}

만약 minSdk가 23 이하 버전에서 빌드 시에는 다음과 같은 오류로 빌드에 실패하고, 해결 방법도 제시해준다.

Manifest merger failed : uses-sdk:minSdkVersion 21 cannot be smaller than version 23 declared in library [androidx.security:security-crypto:1.0.0-alpha02] /Users/taehwankwon/.gradle/caches/transforms-2/files-2.1/200987cc7c597f82d688152e011ba322/jetified-security-crypto-1.0.0-alpha02/AndroidManifest.xml as the library might be using APIs not available in 21
	Suggestion: use a compatible library with a minSdk of at most 21,
		or increase this project's minSdk version to at least 23,
		or use tools:overrideLibrary="androidx.security" to force usage (may lead to runtime failures)

EncryptedSharedPreferences 초기화

EncryptedSharedPreferences은 기존 SharedPreferences에 약간의 코드 수정만으로도 암호화를 적용할 수 있다.

필요한 도구는 EncryptedSharedPreferences create를 이용하고, Android KeyStore를 활용할 수 있는 MasterKeys를 아래와 같이 초기화 시켜줘야 한다.

SharedPreferences의 Key/Value 모두에 대한 암호화 방법을 지정해야 한다. 현재는 아래와 같이 Key는 AES256_SIV, value는 AES256_GCM을 제공하고 있다.

각각을 모두 초기화해주면 다음과 같다.

private val sharedPreferences: SharedPreferences by lazy {
    val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
    val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

    EncryptedSharedPreferences.create(
        "security_test",
        masterKeyAlias,
        requireContext(),
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
}

EncryptedSharedPreferences 사용하기

사용하는 방법은 크게 다르지 않다. sharedPreferences을 수정하려면 edit로 접근하고, putString/putInt 등을 할 수 있고, apply로 적절하게 완료해주면 되겠다.

sharedPreferences
    .edit()
    .putString("edit", binding.editQuery.text.toString())
    .apply()


EncryptedSharedPreferences 쓰기/읽기 확인하기

읽는 방법도 간단하다. 그냥 기존 SharedPreferences 읽는 방법으로 접근하여 읽어오면 끝이다.

sharedPreferences.getString("edit", "")

그럼 실제 화면에 뿌려지는 데이터는? 그냥 다음 화면과 동일하다.

sample_01


실제 데이터는 어떻게 들어가 있을까?

화면은 별다른 게 없다. 그럼 실제 데이터를 확인해보는 게 가장 좋다.

실제 데이터를 보는 방법은 Debug 설치 상태에서 Android Studio의 Device File Explorer를 이용하면 데이터 접근이 가능하며, 실제 파일의 경로는 다음과 같다.

/data/data/[packageName]/shared_prefs/[preferencesName].xml

필자가 작성한 코드의 데이터는 다음과 같이 암호화되어 있음을 확인할 수 있었다.

참고로 최초로 작성한 암호화 키인 masterKeyAlias의 값이 변경되면 기존 값을 불러오지 못하고, 계속 새로운 key/value가 추가된다.

sample_02

key와 value 자체도 암호화 처리되어 있어 단순하게 확인이 불가능하다.


EncryptedFile 사용하기

EncryptedFile 역시 SharedPreferences와 동일한 방법으로 암호화 가능하며, 문서 내용을 참고할 수 있다.

암호화에 사용할 masterKeyAlias는 SharedPreferences에서 사용한 방법과 동일하게 접근할 수 있다.

EncryptedFile을 이용하여 암호화할 파일의 대상을 정의하며 암호화 방법은 AES256_GCM_HKDF_4KB을 사용한다.

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

val fileToRead = "my_sensitive_data.txt"
val file = File(requireContext().cacheDir, fileToRead)
if (file.exists()) {
    file.delete()
}
val encryptedFile = EncryptedFile.Builder(
    file,
    requireContext(),
    masterKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

저장하기

저장하는 방법은 기존 File output과 동일하며, 다음과 같이 작성할 수 있다.

encryptedFile.openFileOutput().bufferedWriter().use {
    it.write(binding.editQuery.text.toString())
}

다시 읽기

같은 암호화로 저장한 파일을 읽으려면 다음과 같이 읽을 수 있다.

val content = encryptedFile.openFileInput().bufferedReader().useLines { lines ->
    lines.fold(" ") { working, line ->
        "$working $line"
    }
}

암호화 확인

암호화 결과는 아래와 같다. 암호 데이터 저장은 지정한 파일 위치에서 확인 가능하다.

sample_03


openFileOutput 사용 시 주의사항

openFileOutput을 사용할 때 내부 코드는 다음과 같다.

@NonNull
public FileOutputStream openFileOutput()
        throws GeneralSecurityException, IOException {
    if (mFile.exists()) {
        throw new IOException("output file already exists, please use a new file: "
                + mFile.getName());
    }
    FileOutputStream fileOutputStream = new FileOutputStream(mFile);
    OutputStream encryptingStream = mStreamingAead.newEncryptingStream(fileOutputStream,
            mFile.getName().getBytes(UTF_8));
    return new EncryptedFileOutputStream(fileOutputStream.getFD(), encryptingStream);
}

여기에서 볼 수 있듯 mFile.exists를 이용하여 기존 파일 생성 여부를 확인하는데, 기존 파일 덮어쓰는 것을 방지하기 위함이나 이미 있을 경우에는 즉시 오류가 발생한다.

그렇기에 예외 처리해야 하는데 다음과 같은 방법이 있을 수 있다.

  • file.exists을 이용하여 사전에 삭제하기
    • 사전에 삭제할 경우 기존 파일을 그냥 제거하는 부분으로 데이터 유실 가능성이 매우 크다.
  • 이미 있는 경우 데이터를 불러오고 다시 저장하도록 작업한다.


masterKeyAlias 처리에 관해서

masterKeyAlias는 Google에서 권장하는 방법인 MasterKeys를 이용해 가져오는게 좋다.

하지만 이에 대한 결과 값은 내부적으로 validation하고, 체크하고나서 _androidx_security_master_key_을 리턴해준다.

하지만 KeyStore를 사용하지만 저런 값을 validation을 해줄 뿐 별다른건 없다.

그래서 다른 키 값을 추가한다고해서 동작을 하지 않는것은 아니고, 실제 동작은 EncryptedSharedPreferences, EncryptedFile의 내부 코드를 통해 확인할 수 있는데 다음과 같다.

키스토어 기본값과 KEYSTORE_PATH_URI와 masterKeyAlias를 통합해버리는 형태로 KeyStore를 저장한다.

.withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)

처음 생성한 KeyStore를 보증하지는 않지만, 없어도 문제 없이 동작한다. 하지만 이후 KeyStore에 저장되었는 데이터를 기준으로 불러오기 때문에 존재하지 않는 앱에서는 읽는건 어렵다.

결국 내부적으로 KeyStore를 잘 활용하고 있는데, 다른 기기에서 확인하려고 시도하면 존재하지 않는 Key를 읽으려 한다며 오류가 발생한다.

Caused by: java.io.IOException: No matching key found for the ciphertext in the stream.

결국 MasterKeys를 이용하지 않아도 문제는 없다.

다만 MasterKeys를 통해 Key 생성은 하지만 그 키를 보증하지는 않는다. 이건 좀 아쉽군


추가적인 활용

EncryptedFileEncryptedSharedPreferences을 지원하고 있다. 보통 개발자가 사용하기에는 부족한 부분이 있는데 다음과 같다.

  • Local DB인 SQLite에서 활용이 어렵다.
  • 네트워크에 전달해야 하는 데이터 이 암호화를 사용하기 어렵다.

결국 해결해보려면 Google Tink를 직접 사용하여 조치하는 방법이나, 기존처럼 복사/붙여넣기의 형태를 사용하여 커스텀 하는 게 좋다.

alpha2 이후 배포한 게 없기 때문에 얼마나 수정 가능성이 있는지 알기는 어렵다.


GDG 발표 내용


BiometricPrompt도 포함되어 있을까?

문서에도 나와 있는데 BiometricPrompt은 여기에 포함되어 있지 않고, 별도로 작업해주어야 한다. BiometricPrompt을 사용하는 방법은 다음 글에서 좀 더 알아보겠다.


마무리

기본적으로 제공하는 Security library를 소개하고, 사용 방법을 알아보았다. 매번 복사하던 AES(Advanced Encryption Standard) 암호화 코드를 구글에서 직접 제공하며, EncryptedFileEncryptedSharedPreferences를 좀 더 쉽게 접근할 수 있도록 제공하는 라이브러리이다. 안드로이드에서 저장하는 데이터의 대부분은 여기에 해당하기에 2개의 데이터 처리를 간단하게 만들어둔 라이브러리이다 보니 확장성이 좋지는 않다.



About Taehwan

안드로이드 개발 12년차, 모바일앱 개발자 권태환입니다. 코드와 아키텍처, 그리고 개발 문화에 관심이 많습니다. GDG Korea Android와 DroidKnights에서 개발자 커뮤니티를 함께 만들어가고 있습니다.

Comments

테오 AI powered by Gemini 2.5 Flash
🤖
안녕하세요! 테오입니다. 😊
이 블로그의 모든 포스트를 학습했어요.
Android, Kotlin, 개발 관련 궁금한 점을 물어보세요!