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

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments