안드로이드 Jetpack의 Security 라이브러리의 구현원리와 EncryptedFile, EncryptedSharedPreferences 의 사용법
TL;DR
샘플 프로젝트의 패키지 루트/authentication 에서 Security를 사용한 코드를 살펴보실 수 있습니다.
Jetpack Security
Jetpack의 새로운 라이브러리 중 하나인 Security는 파일이나 데이터를 읽고 쓰는 과정에 암호화/복호화를 간단한 API를 통해 제공해줍니다.
흔히 사용되는 Java의 File
이나 안드로이드의 SharedPreferences
를 이름 앞에 Encrypted
가 붙은 형태로 EncryptedFile
, EncryptedSharedPreferences
가 구현이 되어있습니다. 이 객체들을 생성한 후에 우리는 기존 File
, SharedPreferences
를 사용하듯이 동일한 문법으로 파일, 데이터 입출력에 암호화를 첨가해줄 수 있습니다.
동적으로 저장되어야 하는 API 키들이나 토큰들 그리고 민감한 정보들을 로컬에 저장해야 하는 상황에 적합한 라이브러리입니다. 현재 1.0.0은 API 23 이상만 사용할 수 있지만, 추후의 버전(1.1.0)에서는 API 21 부터도 사용할 수 있을 것으로 보입니다.
구현
Security 는 내부적으로 Tink와 Android Keystore를 사용합니다. Tink는 구글이 개발한 여러 크로스 플랫폼을 지원하는 암호화 API 라이브러리이며 Android Keystore는 안드로이드가 제공하는 기능으로, 암호화 키를 컨테이너에 저장하여 보안을 강화해주는 역할을 맡습니다. API 28 부터는 Keystore의 StrongBox 라는 Keymaster HAL 구현체가 암호화 알고리즘을 구현합니다.
각 File이나 SharedPreferences
를 암/복호화 하는 과정에서 쓰이는 key들의 집합을 keyset이라 합니다. 이 keyset은 SharedPreferences
에 저장됩니다. 그리고 모든 keyset들을 암호화하는 데 쓰이는 primary(master) key가 존재합니다. 이 master key는 Android keystore 에 저장됩니다.
Java의 Crypto API 의 Cipher
라는 암호화 알고리즘은 복호화를 진행하는데 필요한 암호화 키를 의미하는 SecretKey
객체를 이용해 복호화를 진행합니다. 이 SecretKey
는 Android Keystore에서 내부적으로 안드로이드 OS가 아닌 외부의 특별한 컨테이너(TEE or StrongBox)를 이용해 저장되고 절대 그 공간 밖으로 나오지 않습니다.
우리가 Android Keystore에게 SecretKey
를 만들어달라고 요청하면 Keystore는 내부적으로 SecretKey
를 만들고 그것에 대한 alias를 반환합니다. 오직 Keystore만이 내부적으로 반환해준 alias와 실제 Keystore에 대한 매핑을 해놓습니다.
암호화를 하고싶으면 Keystore에게 alias와 plaintext를 전달해주면 Keystore가 내부적으로 알아서 암호화된 ciphertext를 반환해줍니다. 반대로 복호화는 ciphertext와 alias를 전달해주면 plaintext를 반환해주는 식입니다. 대칭키(symmetric key) 알고리즘입니다.
사용법 EncryptedFile
사용법은 간단합니다. master key를 의미하는 MasterKey 적절한 설정과 함께 만들고 이를 EncryptedFile 이나 EncryptedSharedPreferences 의 생성 과정에서 전달해주면 됩니다. 그 후에는 얻은 객체들을 File 이나 SharedPreferences 처럼 쓸 수 있습니다.
우선 MasterKey 객체를 하나 생성하겠습니다.
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
그런 다음 EncryptedFile 객체를 생성해보겠습니다.
val encryptedFile = EncryptedFile.Builder(
context,
File(context.getExternalFilesDir(null), "data.txt"),
masterKey,
AES256_GCM_HKDF_4KB
).build()
다음 예제는 아이디와 비밀번호를 가지고 로그인, 회원가입, 로그아웃, 자동 로그인을 구현한 예시입니다.
코루틴을 조금 섞어봤는데 읽히시나요?
자동 로그인이 성공하려면 파일이 존재하기만 하면 되고, 로그아웃을 할 때 파일을 삭제해줍니다. 그리고 outputStream
과 inputStream
을 이용해서 아이디와 비밀번호를 각줄에 적고 입출력을 해서 저장, 비교를 합니다.
사용법 EncryptedSharedPreferences
제가 샘플 프로젝트의 컨셉상 아이디와 비밀번호를 이용하는 여러 Authenticator
를 만들고 이에 대한 구현체를 많이 만들어두었는데, SharedPreferences
를 이용한 구현체와 EncryptedSharedPreferences
를 이용한 구현체가 있습니다. SharedPreferences
를 이용한 구현체는 보지 않아도 감이 잡히실 겁니다. 저는 코틀린의 클래스 위임을 이용해 EncryptedSharedPreferences
를 구현해보았습니다.
MasterKey를 만드는 부분은 EncryptedFile
때와 똑같고, EncryptedSharedPreferences
의 create
팩토리 함수를 이용해 객체를 생성해줍니다. 인자로 key와 value를 각각 어떤 암호화 알고리즘들을 이용해 암호화할 것인지를 넘깁니다. 알고리즘들은 AES(Advanced Encryption Standard)의 어떤 것들이지만 이 부분은 저도 잘 모르니 넘어가도록 하겠습니다.
결국 우리는 EncryptedSharedPreferences
객체를 만들었고 이는 SharedPreferences
와 사용법이 완전히 동일하므로 더 살펴볼 것은 없습니다.
실제로 암호화가 되었나요?
한번 살펴보겠습니다. EncryptedFile로 저장된 data.txt 라는 파일의 내부를 살펴보죠.
좋습니다. 이 파일에 설마 유저의 아이디와 비밀번호가 저장되어있을 것이라고는 쉬이 짐작하기 어렵겠군요.
다음 포스팅
이번 글에서는 Jetpack의 Security 라이브러리에 대한 간단한 구현원리와 사용법에 대해서 살펴보았습니다.
다음 포스팅에서는 Cryptography를 제공해주는 Security 라이브러리와 생체인증을 구현해준 Biometric라이브러리을 결합시켜 암호화된 데이터를 복호화하는 과정을 정리하겠습니다.
피드백은 환영입니다.
감사합니다 👏
- Github
- Website
- Medium Blog, Dev Blog, Naver Blog
- Contact: mym0404@gmail.com