앱 보안을 강화하여 사용자의 신뢰와 기기 무결성을 유지할 수 있습니다.
이 페이지에서는 앱 보안에 유의미하고 긍정적인 영향을 미치는 몇 가지 권장사항을 설명합니다.
보안 통신 적용
여러 앱 사이 또는 앱과 웹사이트 사이에 교환하는 데이터를 보호하면 앱 안정성이 향상되고 주고받는 데이터가 보호됩니다.
앱 간의 통신 보호하기
앱 간에 보다 안전하게 통신하려면 앱 선택기, 서명 기반 권한, 내보내기되지 않는 콘텐츠 제공자와 함께 암시적 인텐트를 사용합니다.
앱 선택기 표시하기
암시적 인텐트가 사용자 기기에서 가능한 앱을 2개 이상 시작할 수 있는 경우에는 앱 선택기를 명시적으로 표시합니다. 이 상호작용 전략을 사용하면 사용자는 자신이 신뢰하는 앱에 민감한 정보를 전송할 수 있습니다.
Kotlin
val intent = Intent(Intent.ACTION_SEND) val possibleActivitiesList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL) // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." val chooser = resources.getString(R.string.chooser_title).let { title -> Intent.createChooser(intent, title) } startActivity(chooser) } else if (intent.resolveActivity(packageManager) != null) { startActivity(intent) }
자바
Intent intent = new Intent(Intent.ACTION_SEND); List<ResolveInfo> possibleActivitiesList = getPackageManager() .queryIntentActivities(intent, PackageManager.MATCH_ALL); // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size() > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." String title = getResources().getString(R.string.chooser_title); Intent chooser = Intent.createChooser(intent, title); startActivity(chooser); } else if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); }
관련 정보:
서명 기반 권한 적용하기
개발자가 소유하거나 제어하는 두 앱 사이에 데이터를 공유할 때 서명 기반 권한을 사용합니다. 이 권한을 사용하면 사용자 확인을 요구하지 않고 그 대신 데이터에 액세스하는 앱이 동일한 서명 키를 사용해 서명되는지 여부를 확인합니다. 따라서 사용자 환경이 더 간편하고 안전해집니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <permission android:name="my_custom_permission_name" android:protectionLevel="signature" />
관련 정보:
앱 콘텐츠 제공자 액세스 허용하지 않기
내 앱에서 내 소유가 아닌 다른 앱에 데이터를 보내지 않으려는 경우에는 다른 개발자의 앱이 내 앱의 ContentProvider
객체에 액세스하지 못하도록 명시적으로 지정해야 합니다. 이 설정은 Android 4.1.1(API 수준 16) 이하를 실행하는 기기에 앱을 설치할 수 있는 경우에 특히 중요합니다. <provider>
요소의 android:exported
속성은 이 버전의 Android에서 기본적으로 true
이기 때문입니다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <application ... > <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" ... android:exported="false"> <!-- Place child elements of <provider> here. --> </provider> ... </application> </manifest>
민감한 정보를 표시하기 전에 사용자 인증 정보 요청하기
사용자가 앱의 민감한 정보나 고급 콘텐츠에 액세스할 수 있도록 사용자 인증 정보를 요청할 때 PIN/비밀번호/패턴이나 생체 인식 사용자 인증 정보(예: 얼굴 인식, 지문 인식)를 요구합니다.
생체 인식 사용자 인증 정보를 요청하는 방법을 자세히 알아보려면 생체 인식 인증 관련 가이드를 참고하세요.
네트워크 보안 조치 적용하기
다음 섹션에서는 앱의 네트워크 보안을 향상할 수 있는 방법을 설명합니다.
TLS 트래픽 사용하기
앱이 신뢰할 수 있으며 잘 알려진 인증 기관(CA)에서 발행한 인증서가 있는 웹 서버와 통신하는 경우 아래와 같은 HTTPS 요청을 사용합니다.
Kotlin
val url = URL("https://www.google.com") val urlConnection = url.openConnection() as HttpsURLConnection urlConnection.connect() urlConnection.inputStream.use { ... }
자바
URL url = new URL("https://www.google.com"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.connect(); InputStream in = urlConnection.getInputStream();
네트워크 보안 구성 추가하기
앱이 새 CA나 맞춤 CA를 사용하는 경우 구성 파일에서 네트워크의 보안 설정을 선언할 수 있습니다. 이 프로세스를 통해 앱 코드를 수정하지 않고도 구성을 만들 수 있습니다.
네트워크 보안 구성 파일을 앱에 추가하려면 다음 단계를 따르세요.
- 앱의 매니페스트에 구성을 선언합니다.
-
XML 리소스 파일(위치:
res/xml/network_security_config.xml
)을 추가합니다.clear-text를 사용 중지하여 특정 도메인으로 향하는 전체 트래픽에 HTTPS를 사용하도록 지정합니다.
<network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">secure.example.com</domain> ... </domain-config> </network-security-config>
개발 프로세스 중에
<debug-overrides>
요소를 사용하여 사용자가 설치한 인증서를 명시적으로 허용할 수 있습니다. 이 요소는 앱의 릴리스 구성에는 영향을 미치지 않고 디버깅 및 테스트 도중에 앱의 보안상 중요한 옵션을 재정의합니다. 다음 스니펫에서는 앱의 네트워크 보안 구성 XML 파일에서 이 요소를 정의하는 방법을 보여줍니다.<network-security-config> <debug-overrides> <trust-anchors> <certificates src="user" /> </trust-anchors> </debug-overrides> </network-security-config>
<manifest ... > <application android:networkSecurityConfig="@xml/network_security_config" ... > <!-- Place child elements of <application> element here. --> </application> </manifest>
관련 정보: 네트워크 보안 구성
고유한 트러스트 관리자 만들기
TLS 검사기가 모든 인증서를 수락해서는 안 됩니다. 트러스트 관리자를 설정하고 다음 조건 중 하나가 사용 사례에 적용되는 경우 발생하는 모든 TLS 경고를 처리해야 할 수도 있습니다.
- 새로운 CA나 맞춤 CA가 서명한 인증서가 있는 웹 서버와 통신합니다.
- 이 CA가 사용 중인 기기에서 신뢰되지 않습니다.
- 네트워크 보안 구성을 사용할 수 없습니다.
이 단계를 완료하는 방법을 자세히 알아보려면 알 수 없는 인증 기관을 처리하는 방법에 관한 설명을 참고하세요.
관련 정보:
신중하게 WebView 객체 사용하기
앱의 WebView
객체는 제어되지 않는 사이트로 사용자가 이동하는 것을 허용해서는 안 됩니다. 가능하면 허용 목록을 사용하여 앱의 WebView
객체를 통해 로드되는 콘텐츠를 제한합니다.
또한 앱의 WebView
객체에 있는 콘텐츠를 완벽하게 제어하고 신뢰하지 않는 경우 자바스크립트 인터페이스 지원을 절대로 사용 설정해서는 안 됩니다.
HTML 메시지 채널 사용하기
Android 6.0(API 수준 23) 이상 버전을 실행하는 기기에서 자바스크립트 인터페이스 지원을 사용해야 하는 경우 다음 코드 스니펫과 같이 웹사이트와 앱 사이에 통신하는 대신 HTML 메시지 채널을 사용합니다.
Kotlin
val myWebView: WebView = findViewById(R.id.webview) // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel() // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() { override fun onMessage(port: WebMessagePort, message: WebMessage) { Log.d(TAG, "On port $port, received this message: $message") } }) // Send a message from channel[1] to channel[0]. channel[1].postMessage(WebMessage("My secure message"))
자바
WebView myWebView = (WebView) findViewById(R.id.webview); // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. WebMessagePort[] channel = myWebView.createWebMessageChannel(); // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() { @Override public void onMessage(WebMessagePort port, WebMessage message) { Log.d(TAG, "On port " + port + ", received this message: " + message); } }); // Send a message from channel[1] to channel[0]. channel[1].postMessage(new WebMessage("My secure message"));
관련 정보:
적합한 권한 제공하기
앱이 제대로 기능하는 데 필요한 최소 권한 수만 요청합니다. 가능하면 앱에 더 이상 관련 권한이 필요하지 않으면 권한을 포기합니다.
권한을 지연하는 인텐트 사용하기
가능한 경우 다른 앱에서 완료될 수 있는 작업을 완료할 권한을 앱에 추가하지 마세요. 대신, 이미 필요한 권한이 있는 다른 앱으로 요청을 지연하는 인텐트를 사용합니다.
다음 예에서는 READ_CONTACTS
권한과 WRITE_CONTACTS
권한을 요청하는 대신 인텐트를 사용하여 사용자를 연락처 앱으로 안내하는 방법을 보여줍니다.
Kotlin
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent(Intent.ACTION_INSERT).apply { type = ContactsContract.Contacts.CONTENT_TYPE }.also { intent -> // Make sure that the user has a contacts app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
자바
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent insertContactIntent = new Intent(Intent.ACTION_INSERT); insertContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE); // Make sure that the user has a contacts app installed on their device. if (insertContactIntent.resolveActivity(getPackageManager()) != null) { startActivity(insertContactIntent); }
또한, 앱이 파일 기반 I/O(예: 저장소 액세스, 파일 선택)를 실행해야 하는 경우 시스템이 앱 대신에 작업을 완료할 수 있으므로 특별한 권한이 필요하지 않습니다. 더 좋은 것은 사용자가 특정 URI에서 콘텐츠를 선택하고 나면 선택한 리소스 관련 권한이 통화 앱에 부여됩니다.
관련 정보:
여러 앱에서 안전하게 데이터 공유하기
보다 안전한 방법으로 앱의 콘텐츠를 다른 앱과 공유하려면 다음 권장사항을 따르세요.
- 필요에 따라 읽기 전용 권한이나 쓰기 전용 권한을 적용합니다.
FLAG_GRANT_READ_URI_PERMISSION
플래그와FLAG_GRANT_WRITE_URI_PERMISSION
플래그를 사용하여 클라이언트에 일회성 데이터 액세스 권한을 제공합니다.- 데이터를 공유할 때
file://
URI가 아닌content://
URI를 사용합니다.FileProvider
인스턴스가 이를 자동으로 수행합니다.
다음 코드 스니펫은 URI 권한 부여 플래그와 콘텐츠 제공자 권한을 사용하여 앱의 PDF 파일을 별도의 PDF 뷰어 앱에 표시하는 방법을 보여줍니다.
Kotlin
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("content://com.example/personal-info.pdf") // This flag gives the started app read access to the file. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }.also { intent -> // Make sure that the user has a PDF viewer app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
자바
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent viewPdfIntent = new Intent(Intent.ACTION_VIEW); viewPdfIntent.setData(Uri.parse("content://com.example/personal-info.pdf")); // This flag gives the started app read access to the file. viewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Make sure that the user has a PDF viewer app installed on their device. if (viewPdfIntent.resolveActivity(getPackageManager()) != null) { startActivity(viewPdfIntent); }
참고: 쓰기 가능한 앱 홈 디렉터리에서 파일을 실행하는 것은 W^X 위반입니다.
따라서 Android 10(API 수준 29) 이상을 타겟팅하는 신뢰할 수 없는 앱은 앱의 홈 디렉터리 내의 파일에서 exec()
를 호출할 수 없으며 앱의 APK 파일 내에 삽입된 바이너리 코드만 호출할 수 있습니다.
또한 Android 10 이상을 타겟팅하는 앱은 dlopen()
을 사용하여 연 파일에서 실행 코드를 메모리 내에서 수정할 수 없습니다. 여기에는 텍스트가 재배치된 모든 공유 객체(.so
) 파일이 포함됩니다.
관련 정보: android:grantUriPermissions
안전하게 데이터 저장하기
앱이 민감한 사용자 정보에 액세스할 권한을 요구하더라도 사용자는 앱의 보안이 적절하다고 신뢰하는 경우에만 자신의 데이터에 액세스할 수 있도록 앱에 권한을 부여합니다.
내부 저장소 내에 비공개 데이터 저장하기
비공개 사용자 데이터는 앱별로 샌드박스가 배정된 기기 내부 저장소 내에 모두 저장합니다. 그러면 앱이 이러한 파일을 보기 위해 권한을 요청할 필요가 없으며, 다른 앱은 이 파일에 액세스할 수 없습니다. 추가 보안 조치로서, 사용자가 앱을 제거하면 기기에서 앱이 내부 저장소 내에 저장한 모든 파일이 삭제됩니다.
다음 코드 스니펫에서는 내부 저장소에 데이터를 쓰는 방법을 보여줍니다.
Kotlin
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. val FILE_NAME = "sensitive_info.txt" val fileContents = "This is some top-secret information!" File(filesDir, FILE_NAME).bufferedWriter().use { writer -> writer.write(fileContents) }
Java
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. final String FILE_NAME = "sensitive_info.txt"; String fileContents = "This is some top-secret information!"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(new File(getFilesDir(), FILE_NAME)))) { writer.write(fileContents); } catch (IOException e) { // Handle exception. }
다음 코드 스니펫에서는 내부 저장소에서 데이터를 읽는 역작업을 보여줍니다.
Kotlin
val FILE_NAME = "sensitive_info.txt" val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines -> lines.fold("") { working, line -> "$working\n$line" } }
자바
final String FILE_NAME = "sensitive_info.txt"; StringBuffer stringBuffer = new StringBuffer(); try (BufferedReader reader = new BufferedReader(new FileReader(new File(getFilesDir(), FILE_NAME)))) { String line = reader.readLine(); while (line != null) { stringBuffer.append(line).append('\n'); line = reader.readLine(); } } catch (IOException e) { // Handle exception. }
관련 정보:
사용 사례별로 외부 저장소에 데이터 저장하기
앱이 다른 앱과 공유하는 파일뿐만 아니라 앱에 특정한 민감하지 않은 대형 파일을 저장하는 데는 외부 저장소를 사용합니다. 사용하는 특정 API는 앱이 앱별 파일에 액세스하도록 설계되었는지 또는 공유 파일에 액세스하도록 설계되었는지에 따라 다릅니다.
파일이 비공개 정보나 민감한 정보를 포함하지 않지만 사용자가 앱을 이용 중일 때만 사용자에게 값을 제공하는 경우에는 파일을 외부 저장소의 앱별 디렉터리에 저장합니다.
다른 앱에 값을 제공하는 파일을 액세스하거나 저장해야 하는 앱의 경우 사용 사례에 따라 다음 API 중 하나를 사용합니다.
- 미디어 파일: 앱 간에 공유되는 이미지, 오디오 파일, 동영상을 저장하고 액세스하려면 Media Store API를 사용합니다.
- 기타 파일: 다운로드한 파일을 비롯한 다른 유형의 공유 파일을 저장하고 액세스하려면 저장소 액세스 프레임워크를 사용합니다.
저장소 볼륨의 사용 가능 여부 확인하기
앱이 이동식 외부 저장소 기기와 상호작용하는 경우에는 앱이 저장소 기기에 액세스하는 동안 사용자가 저장소 기기 연결을 끊을 수도 있습니다. 저장소 기기를 사용할 수 있는지 확인하는 로직을 포함하세요.
데이터 유효성 확인하기
앱이 외부 저장소의 데이터를 사용하는 경우 데이터의 콘텐츠가 손상되거나 수정되지 않았는지 확인합니다. 더 이상 안정적인 형식이 아닌 파일을 처리하는 로직을 포함합니다.
다음 코드 스니펫은 해시 인증 도구의 예를 보여줍니다.
Kotlin
val hash = calculateHash(stream) // Store "expectedHash" in a secure location. if (hash == expectedHash) { // Work with the content. } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. suspend fun calculateHash(stream: InputStream): String { return withContext(Dispatchers.IO) { val digest = MessageDigest.getInstance("SHA-512") val digestStream = DigestInputStream(stream, digest) while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } digest.digest().joinToString(":") { "%02x".format(it) } } }
자바
Executor threadPoolExecutor = Executors.newFixedThreadPool(4); private interface HashCallback { void onHashCalculated(@Nullable String hash); } boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -> { if (Objects.equals(hash, expectedHash)) { // Work with the content. } }); if (!hashRunning) { // There was an error setting up the hash function. } private boolean calculateHash(@NonNull InputStream stream, @NonNull Executor executor, @NonNull HashCallback hashCallback) { final MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-512"); } catch (NoSuchAlgorithmException nsa) { return false; } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. executor.execute(() -> { String hash; try (DigestInputStream digestStream = new DigestInputStream(stream, digest)) { while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } StringBuilder builder = new StringBuilder(); for (byte aByte : digest.digest()) { builder.append(String.format("%02x", aByte)).append(':'); } hash = builder.substring(0, builder.length() - 1); } catch (IOException e) { hash = null; } final String calculatedHash = hash; runOnUiThread(() -> hashCallback.onHashCalculated(calculatedHash)); }); return true; }
민감하지 않은 데이터만 캐시 파일에 저장하기
민감하지 않은 앱 데이터에 더 빠르게 액세스할 수 있게 하려면 이 데이터를 기기의 캐시에 저장합니다. 1MB보다 큰 캐시에는 getExternalCacheDir()
을 사용합니다.
캐시가 1MB 이하인 경우에는 getCacheDir()
을 사용합니다.
두 메서드는 앱의 캐시 데이터가 포함된 File
객체를 제공합니다.
다음 코드 스니펫은 앱에서 최근에 다운로드한 파일을 캐시하는 방법을 보여줍니다.
Kotlin
val cacheFile = File(myDownloadedFileUri).let { fileToCache -> File(cacheDir.path, fileToCache.name) }
자바
File cacheDir = getCacheDir(); File fileToCache = new File(myDownloadedFileUri); String fileToCacheName = fileToCache.getName(); File cacheFile = new File(cacheDir.getPath(), fileToCacheName);
참고: getExternalCacheDir()
을 사용하여 앱 캐시를 공유 저장소 내에 저장하는 경우 사용자가 앱 실행 중에 이 저장소가 포함된 매체를 뺄 수도 있습니다. 이 사용자 동작으로 인해 발생하는 캐시 부적중을 원활하게 처리하는 로직을 포함합니다.
주의: 이러한 파일에는 적용되는 보안이 없습니다.
따라서 Android 10(API 수준 29) 이하를 타겟팅하고 WRITE_EXTERNAL_STORAGE
권한이 있는 모든 앱에서는 이 캐시의 콘텐츠에 액세스할 수 있습니다.
관련 정보: 데이터 및 파일 저장소 개요
비공개 모드로 SharedPreferences 사용하기
getSharedPreferences()
를 사용하여 앱의 SharedPreferences
객체를 만들거나 액세스할 때는 MODE_PRIVATE
을 사용하세요. 이렇게 하면 내 앱만 공유 환경설정 파일 내의 정보에 액세스할 수 있습니다.
데이터를 여러 앱에서 공유하려면 SharedPreferences
객체를 사용하지 마세요. 대신, 여러 앱에서 안전하게 데이터를 공유하기 위한 단계를 따릅니다.
또한 보안 라이브러리는 SharedPreferences 클래스를 래핑하고 키와 값을 자동으로 암호화하는 EncryptedSharedPreferences 클래스를 제공합니다.
관련 정보:
서비스 및 종속 항목 최신 상태로 유지하기
대부분의 앱은 외부 라이브러리와 기기 시스템 정보를 사용하여 전문 작업을 완료합니다. 앱의 종속 항목을 최신 상태로 유지하여 통신 지점의 보안을 강화할 수 있습니다.
Google Play 서비스 보안 제공업체 확인하기
참고: 이 섹션은 Google Play 서비스가 설치되어 있는 기기를 타겟팅하는 앱에만 적용됩니다.
앱이 Google Play 서비스를 사용하는 경우에는 앱이 설치된 기기에서 업데이트되도록 해야 합니다. UI 스레드에서 비동기식으로 검사를 실행합니다. 기기가 최신 상태가 아니면 승인 오류를 트리거합니다.
앱이 설치되어 있는 기기에서 Google Play 서비스의 최신 상태 여부를 확인하려면 가이드에서 SSL 악용을 차단하기 위한 보안 프로바이더 업데이트 단계를 따릅니다.
관련 정보:
모든 앱 종속 항목 업데이트하기
앱을 배포하기 전에 모든 라이브러리, SDK, 기타 종속 항목이 최신 상태인지 확인합니다.
- Android SDK와 같은 퍼스트 파티 종속 항목의 경우 Android 스튜디오에 있는 업데이트 도구(예: SDK Manager)를 사용합니다.
- 서드 파티 종속 항목의 경우 앱에서 사용하는 라이브러리의 웹사이트를 확인하고 사용할 수 있는 업데이트와 보안 패치를 설치하세요.
관련 정보: 빌드 종속 항목 추가
추가 정보
앱의 보안을 강화하는 방법을 자세히 알아보려면 다음 리소스를 확인하세요.
- 핵심 앱 품질 보안 체크리스트
- 앱 보안 개선 프로그램
- YouTube의 Android 개발자 채널
- Android 네트워크 보안 구성 Codelab
- Android 보안 확인: 트랜잭션 보안 한 단계 높이기