(원문: 

http://developer.android.com/guide/topics/security/permissions.html)

(위치: Develop > API Guides > Introduction > System Permissions)

(전체 번역 글 목록으로 이동하기)


시스템 퍼미션


안드로이드는 유저 및 그룹별로 권한이 분리되어 있는 OS로서, 앱들은 각각의 시스템 ID(리눅스 유저 ID와 그룹 ID)를 가지고 실행됩니다. 시스템의 각 부분들도 각각의 ID를 갖고 분리되어 있습니다. 리눅스에서, 앱들은 서로 분리된 환경에서 구동되고, 또 시스템과도 분리되어 있는데, 안드로이드가 이러한 리눅스를 기반으로 하고 있는 것입니다.

추가로 도입된 보안적 특징은 퍼미션(permission) 메커니즘을 통해 실행을 제한한다는 점입니다. 특정 프로세스의 실행을 제한할 수 있고, 특정 데이터에 접근을 허용하는 URI들에 대해서도 실행을 제한할 수 있습니다.

여기서는 개발자들이 안드로이드의 보안 특징들을 어떻게 이용할 수 있는지 설명하겠습니다. 더 자세한 내용은 안드로이드 보안 훑어보기에서 학습하실 수 있습니다.


보안 아키텍쳐 (Security Architecture)


안드로이드 보안 아키텍쳐의 핵심은, 기본적으로 앱들이 다른 앱이나 OS, 또는 사용자에게 불리한 영향을 미칠 만한 퍼미션들을 갖고 있지 않습니다. 여기에는 주소록이나 이메일과 같은 사용자 개인 정보의 읽기/쓰기 권한과, 다른 앱의 파일에 대한 읽기/쓰기 권한, 그리고 네트워크 접속과 관련된 권한, 디바이스를 켜진 상태로 유지하는 권한 등이 포함됩니다.

안드로이드 앱은 각각의 프로세스 샌드박스에서 구동되기 때문에, 앱끼리 리소스나 데이터를 공유하려면 명시적으로 해야 합니다. 이것은, 기본적으로는 사용할 수 없지만 퍼미션을 선언함으로써 사용할 수 있다는 의미입니다. 앱들은 필요한 퍼미션들을 매니페스트 파일에 선언해야 하고, 안드로이드 시스템은 앱이 설치되기 직전에 사용자에게 퍼미션 목록을 보여줌으로써 동의를 구합니다. 안드로이드는 보안과 관련된 사용자 경험을 복잡하게 만들지 않기 위해서, 런타임에 동적으로 퍼미션을 관리하지는 않습니다. 다시 말해서, 앱을 설치할 때 한번 사용자에게 모든 동의를 받고, 앱을 실행하는 중에는 퍼미션과 관련된 절차를 거치지 않는다는 것입니다.

앱의 샌드박스는 앱을 빌드할 때 사용하는 개발언어 및 기술들에 의존적이지 않습니다. 좀더 구체적으로 말하면, 달빅 가상머신(Dalvik VM)은 보안 환경 안에 있지 않으며, 어떤 앱이던 C/C++과 같은 네이티브 코드(안드로이드 NDK 참고)를 실행할 수 있습니다. 즉, 자바로 개발된 앱이나, C/C++로 개발된 앱이나, 또는 둘을 혼용한 앱이나 모두 같은 방식의 샌드박스 안에서 실행되며, 서로간의 보안 환경도 같습니다.


앱에 사인하기 (Application Signing)


모든 APK 파일들(.apk)은 개발자의 개인키가 되는 증명서로 사인이 되어 있어야만 합니다. 이 증명서는 앱의 소유자를 확인하는 용도로 사용되는데, 인증 기관을 통해 발행되는 것이 아니라 개발자가 자체적으로 만드는 것이며(self-signed), 안드로이드에서 이러한 증명서들의 목적은 앱의 소유자를 구분하는 것입니다. 시스템이 시그니쳐 레벨의 퍼미션(signature-level permissions)을 승인하거나 거부할 때, 그리고 동일한 리눅스 ID로부터 요청받은 사항을 승인하거나 거부할 때 증명서를 사용합니다.


유저 ID와 파일 접근성(User IDs and File Access)


안드로이드는 앱을 설치할 때 해당 앱의 대표 패키지명에 대응되는 리눅스 유저 ID를 부여합니다. 이 ID는 해당 패키지가 디바이스에서 제거될 때까지 상수로 남습니다. 안드로이드 OS는 각 디바이스에 따로 있기 때문에, 같은 패키지에 대한 유저 ID가 디바이스마다 서로 다를 수는 있겠지만, 한 디바이스 안에서 서로 구분만 되면 되기 때문에 문제가 되지는 않습니다.

보안 실행은 프로세스 레벨에서 발생하기 때문에, 서로 다른 리눅스 유저 ID를 갖는 두 개의 패키지가 하나의 프로세스에서 실행될 수는 없습니다. 그러나 안드로이드 매니페스트 파일(AndroidManifest.xml)의 <manifest> 태그에 있는 sharedUserId 속성값을 똑같이 설정하면, 안드로이드는 이러한 두개의 패키지를 같은 앱으로서 관리하고 같은 유저 ID 및 같은 파일 접근 권한을 사용합니다. 그러나, 보안을 유지하기 위해 두 개의 앱이 같은 증명서로 사인되어야만 합니다.

앱에서 저장한 데이터는 해당 앱의 유저 ID에 할당되기 때문에, 기본적으로 다른 앱에서는 접근하지 못합니다. 그러나, 데이터를 저장하기 위해  getSharedPreferences(String, int), openFileOutput(String, int), openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory) 메소드들을 호출할 때, MODE_WORLD_RELEASE, MODE_WORLD_WRITEABLE 값을 인자로 넘기면 저장되는 파일의 접근 권한을 변경하여, 다른 앱에서도 읽기 또는 쓰기를 할 수가 있습니다.


퍼미션 사용하기 (Using Permissions)


아주 기본적인 안드로이드 앱은 아무런 퍼미션도 갖고 있지 않습니다. 사용자 경험이나 디바이스에 저장된 데이터와 관련하여, 사용자 동의를 구하지 않고 얻은 퍼미션으로 인해 좋지 않은 영향을 받을 수도 있기 때문입니다. 따라서 보호된 기능들을 사용하기 위해서는 매니페스트 파일(AndroidManifest.xml)에 해당 기능들에 대한 <uses-permission> 태그들을 추가해야 합니다. 

예를 들어, SMS 메시지가 수신될 때, 내 앱에서 이벤트를 받아 관련된 기능을 구현하고자 한다면 아래와 같이 선언해 줘야 합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="com.android.app.myapp" >
   
<uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

앱이 설치되기 직전에, 패키지 인스톨러(package installer)가 정상적으로 사인된 앱에 선언된 퍼미션들을 확인하여 사용자에게 보여주고 동의를 구합니다. 그리고 나서 앱이 실행될 때에는 퍼미션과 관련하여 사용자에게 동의를 구하지 않습니다. 만약 매니페스트 파일에 선언되지 않은 기능을 런타임에 사용하려고 하면, 사용자에게 동의를 구하고자 하지 않고 바로 실패 처리해 버립니다.

퍼미션이 선언되지 않아 관련 기능에 대한 호출이 실패한 경우, 대부분은 보안 예외(SecurityException)가 발생되지만 이것이 항상 보장되는 것은 아닙니다. 예를 들면, sendBroadcast(Intent) 메소드는 비동기적으로 동작하기 때문에, 데이터가 브로드캐스트 리시버들에게 제대로 전달되었는지를, 결과를 리턴해주고 나서야 확인합니다. 따라서 sendBroadcast(Intent)를 호출하는 시점에 보안 예외를 잡을 수는 없습니다(try-catch). 하지만 거의 모든 경우에, 퍼미션 실패가 발생되면 시스템 로그가 출력됩니다.

그러나, 구글플레이 스토어에서 앱을 설치할 때와 같이 일반적인 경우에, 사용자가 퍼미션들을 허용하는데에 동의하지 않는다면 그 앱은 설치되지 않기 때문에, 런타임때 보안 예외가 발생할 것에 대해 걱정할 필요는 없습니다. 바꿔 말하면, 앱이 설치가 되었다는 것은 그 앱이 요구하는 모든 퍼미션이 승인되었다는 것을 의미합니다. 이것은 필요한 퍼미션을 모두 매니페스트 파일에 선언했다는 것을 전제합니다.

안드로이드 시스템에 의해 제공되는 퍼미션들은 Manifest.permission 클래스에서 확인할 수 있습니다. 하지만, 내 앱에서 따로 퍼미션을 정의하고 요구할 수 있기 때문에, Manifest.permission 클래스의 퍼미션들이 전부라고 할 수는 없습니다.

내 앱이 실행되는 동안에 퍼미션이 필요한 몇가지 경우들을 요약하면 아래와 같습니다.

  • 앱이 시스템에 속한 기능을 사용하려고 할 때
  • 다른 앱의 액티비티(activity)를 실행하려고 할 때
  • 알림(broadcasts)을 발송하거나 수신하는 경우, 발송자나 수신자를 제한해야 할 때
  • 컨텐트 프로바이더(content provider)를 사용할 때
  • 서비스(service)를 바인드하거나 시작할 때


주의사항: 

시간이 흘러 안드로이드가 계속 업데이트 되다 보면, 어떤 API를 호출하기 위해 이전에는 신경쓰지 않았던 퍼미션을 요청해야 하는 경우가 생길 수도 있습니다. 이러한 변화 때문에 기존에 출시되어 있던 앱이 영향을 받지 않도록 하기 위해, 안드로이드는 targetSdkVersion 속성값을 확인하여 그 값이 현재 플랫폼 버전보다 낮을 경우, 하위호환을 위해 앱이 퍼미션을 요청하지 않았더라도 승인해 줄 수 있습니다.

예를 들면, API 레벨 4에서 외부 저장소로의 접근을 제한하기 위해 WRITE_EXTERNAL_STORAGE 퍼미션이 추가되었는데, 만약 내 앱의 targetSdkVersion이 3이하로 되어 있다면, API 레벨 4 이상의 안드로이드에서는 해당 퍼미션을 내 앱에 추가해 줍니다.

만약 내 앱에 이러한 경우가 생긴다면, 실제로 내 앱에서 외부 저장소 관련 기능을 사용하지 않더라도 구글플레이에서는 사용자에게 보여주는 퍼미션 목록에 WRITE_EXTERNAL_STORAGE 퍼미션을 추가한다는 것을 기억해야 합니다.

이렇게 필요하지도 않은 퍼미션이 사용자에게 보여지는 것을 막기 위해서는 targetSdkVersion을 가능한 한 높게 설정해야 합니다. 안드로이드 버전에 따라 어떤 퍼미션들이 추가되었는지에 대해서는 Build.VERSION_CODE 클래스 문서에서 확인하실 수 있습니다.


새로운 퍼미션의 정의와 사용

(Declaring and Enforcing Permissions)

 

새로운 퍼미션을 정의하기 위해서는 AndroidManifest.xml<permission> 태그를 선언해야 합니다.

예를 들어, 다른 앱들이 아무런 제한 없이 내 앱의 액티비티를 실행하는 것을 방지하기 위해 아래와 같이 퍼미션을 선언할 수 있습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   
package="com.me.app.myapp" >
   
<permission android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
       
android:label="@string/permlab_deadlyActivity"
       
android:description="@string/permdesc_deadlyActivity"
       
android:permissionGroup="android.permission-group.COST_MONEY"
       
android:protectionLevel="dangerous" />
    ...
</manifest>

<protectionLevel> 속성은 필수 입력 사항이며, 앱이 해당 퍼미션을 필요로 한다는 것을 사용자에게 알려줄 때 어느 정도의 영향을 줄 수 있는 퍼미션인지를 나타냅니다. 링크된 문서에서 자세히 학습하실 수 있습니다.

<permissionGroup> 속성은 선택 입력 사항이며, 시스템이 퍼미션을 사용자에게 보여줄 때 추가적으로 보여지는 정보에 불과합니다. 보통은 표준 시스템 그룹(android.Manifest.permission_group 클래스 참조)에 정의된 값을 사용하는데, 때에 따라서는 내 앱에서 정의한 값을 사용해야할 경우도 있을 것입니다. 하지만 사용자에게 보여지는 UI를 단순화하기 위해서는 기존에 있는 값을 사용하는게 권장됩니다. 이런저런 앱들을 설치할 때 항상 보던 문구가 아닌 다른 것이 있다면 사용자 입장에서는 다소 혼란스러울 수도 있기 때문입니다.

android:labelandroid:description은 사용자에게 보여지는 퍼미션 정보입니다. label은 퍼미션 목록에 보여지는 짧은 제목이고, description은 해당 퍼미션에 대한 자세한 정보로서 보통 2개의 문장으로 이루어져 있는데, 첫 문장에서 퍼미션에 대해 설명하고 두번째 문장에서는 해당 퍼미션을 승인했을 때 발생할 수도 있을 좋지 않은 영향에 대해 경고합니다.

아래의 예는 CALL_PHONE 퍼미션의 label과 description 입니다.

    <string name="permlab_callPhone">directly call phone numbers</string>
   
<string name="permdesc_callPhone">Allows the application to call
        phone numbers without your intervention. Malicious applications may
        cause unexpected calls on your phone bill. Note that this does not
        allow the application to call emergency numbers.
</string>

현재 시스템에 정의되어 있는 퍼미션들은 시스템 설정 앱이나 쉘 명령어를 통해 확인할 수 있습니다. 설정 앱을 사용할 경우, 설정 > 애플리케이션 에서 앱을 선택한 후 하단의 권한 정보를 확인하면 됩니다. 그리고 쉘 명령어를 이용하는 경우, 콘솔창에서 adb shell pm list permissions 를 입력하면 되는데 끝부분에 -s 옵션을 추가하면 사용자들이 보게 될 화면과 유사하게 콘솔창에 출력해 줍니다.

$ adb shell pm list permissions -s
All Permissions:

Network communication: view Wi-Fi state, create Bluetooth connections, full
Internet access, view network state

Your location: access extra location provider commands, fine (GPS) location,
mock location sources
for testing, coarse (network-based) location

Services that cost you money: send SMS messages, directly call phone numbers

...


AndroidManifest.xml에서 퍼미션 적용하기


시스템이나 앱의 모든 컴포넌트에 대해 접근을 제한하는 고수준의 퍼미션들은 AndroidManifest.xml을 통해 요청됩니다. 요청시 필요한 것은 매니페스트의 컴포넌트 요소에 android:permission 속성을 추가하는게 전부입니다.

액티비티의 퍼미션은 누가 그 액티비티를 시작할 수 있는지를 제한합니다. Context.startActivity()Activity.startActivityForResult()가 호출되었을 때, 호출한 쪽에서 호출받는 액티비티의 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다.

마찬가지로 서비스의 퍼미션은 누가 그 서비스를 시작하거나 바인드할 수 있는지를 제한합니다. Context.startService(), Context.stopService(), Context.bindService()가 호출되었을 때, 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다.

브로드캐스트 리시버의 퍼미션은 누가 그 리시버에게 알림(broadcasts)을 발송할 수 있는지를 제한하지만, 액티비티나 서비스의 퍼미션과는 다르게 Context.sendBroadcast()가 반환값(void)을 리턴한 후에, 시스템이 알림을 리시버에게 전달하려고 할 때 퍼미션을 체크합니다. 따라서 퍼미션을 가지고 있지 않아 접근 제한을 받는 상황이라 하더라도 호출한 쪽에서는 SecurityException이 발생되지 않으며, 그저 리시버에게 알림이 가지 않을 뿐입니다. 리시버의 퍼미션은 매니페스트 파일 뿐만 아니라 소스코드 내에서 Context.registerReceiver()를 통해 동적으로 등록될 때에도 설정될 수 있습니다. 그리고 반대로, Context.sendBroadcast()를 호출할 때 퍼미션을 설정하여, 해당 퍼미션을 가지고 있지 않은 리시버는 받지 못하도록 제한할 수도 있습니다.

컨텐트 프로바이더의 퍼미션은 누가 데이터에 접근할 수 있는지를 제한합니다 (컨텐트 프로바이더는 URI 퍼미션이라 불리는 중요한 보안적 특징을 가지고 있습니다). 다른 컴포넌트들과는 다르게 퍼미션 속성이 둘로 나뉘어 있는데, android:readPermission은 읽기 관련 퍼미션이고 android:writePermission은 쓰기 관련 퍼미션입니다. 이 둘은 독립적이기 때문에 쓰기 관련 퍼미션을 가지고 있는 것이 읽기 관련 퍼미션도 가지고 있는 것을 의미하지는 않습니다. 컨텐트 프로바이더의 퍼미션은 프로바이더가 처음 불려져서 실행될 때 체크되며, 만약 호출하는 쪽에서 요구되는 퍼미션을 가지고 있지 않다면 SecurityException이 발생됩니다. ContentResolver.query()는 (만약 프로바이더가 요구한다면) 읽기 관련 퍼미션을 가지고 있어야 하고, ContentResolver.insert(), ContentResolver.update()ContentResolver.delete()는 쓰기 관련 퍼미션을 가지고 있어야 하며, 이 모든 경우에 대하여 요구되는 퍼미션을 가지고 있지 않은 경우에는 SecurityException이 발생됩니다.


sendBroadcast()할 때 퍼미션 적용하기

 

위에서 설명한 바와 같이 브로드캐스트 리시버의 퍼미션은 누가 그 리시버에게 알림을 발송할 수 있는지를 제한할 수도 있지만, 반대로 어떤 리시버가 내 알림을 받을 수 있는지를 제한할 수도 있습니다. Context.sendBroadcast()를 호출할 때 퍼미션을 인자로 넘기면, 그 퍼미션을 가지고 있는 리시버만이 알림을 받을 수 있습니다. 

알림을 보내는 쪽(sendBroadcast() 호출)과 알림을 받는 쪽(브로드캐스트 리시버) 모두가 퍼미션을 요구하는 경우도 있을 수 있습니다. 이러한 경우에는 양쪽 모두 퍼미션들을 제대로 가지고 있어야 알림을 제대로 전달받을 수 있습니다.


다른 프로세스의 퍼미션 사용하기


원격 서비스 호출시에도 Context.checkCallingPermission()을 사용하여 퍼미션을 확인할 수 있습니다. 메소드 호출시 퍼미션을 인자로 넘기면, 현재 프로세스가 그 퍼미션을 가지고 있는지를 리턴해 줍니다. 단, 이 메소드는 서로 다른 프로세스간 통신(IPC)일 때에만 사용가능하며, 보통 IDL인터페이스를 통해 사용됩니다. 다시 말해서 Context.checkCallingPermission()은, 다른 앱의 서비스를 바인드하여 필요한 메소드를 호출할 때 먼저 내 앱이 퍼미션을 가지고 있는지를 확인하기 위하여 사용되는 것입니다.

퍼미션을 체크하는 방법 몇 가지를 더 알려드리겠습니다. 만약 다른 프로세스의 pid를 알고 있다면, 그 프로세스가 퍼미션을 가지고 있는지 확인하기 위해 Context.checkPermission(String, int, int)를 사용할 수 있습니다. 그리고 만약 다른 앱의 대표 패키지명을 알고 있다면, 그 앱이 퍼미션을 가지고 있는지 확인하기 위해 PackageManager.checkPermission(String, String)을 사용할 수 있습니다.


URI 퍼미션


지금까지 설명한 표준 퍼미션 시스템은 컨텐트 프로바이더를 사용할 때 다소 부족한 점이 있습니다. 컨텐트 프로바이더는 프로바이더 자체에 대해 읽기 및 쓰기 관련 퍼미션을 적용하고 있는데 반해, 프로바이더를 사용하고자 하는 클라이언트는 특정 URI들만 필요로 할 수 있기 때문입니다. 한가지 예로 이메일 앱에 이미지가 첨부되어 있고, 이것을 이미지 뷰어 앱으로 보는 경우를 들 수 있습니다. 이메일 앱은 민감한 사용자 정보를 다루기 때문에 퍼미션들에 의해 보호되어야 하며, 이미지 뷰어 앱은 이메일 내용에 대해서는 알 필요가 없기 때문에 이메일 관련 퍼미션은 가지고 있지 않습니다. 

이러한 경우의 해결책이 바로 URI당 퍼미션(per-URI permissions)입니다. 액티비티를 시작할 때나(startActivity()), 시작된 액티비티에서(startActivityForResult()) 돌려줄 결과값을 추가할 때, 인텐트에 Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION을 설정할 수 있습니다. 이것은 컨텐트 프로바이더에 대한 퍼미션과 상관 없이 특정 URI에 대한 읽기 또는 쓰기 권한을 주는 것입니다.

이 메카니즘은 사용자와의 상호작용(첨부파일을 열거나, 주소록의 항목을 선택하는 등)을 통해 세분화된 퍼미션을 임시로 허용해 줍니다. 만약 이런 방법이 없고 이미지 뷰어 앱으로 첨부된 이미지를 봐야 한다면, 첨부된 이미지를 볼 수 있게 하는 퍼미션을 만들어야 할 것입니다. 이렇게 특정 앱들간의 직접적인 문제들로 인해 퍼미션들이 생성되는 것을 줄이는 것이 URI당 퍼미션의 중요한 목적 중 하나입니다.

하지만 이것은 컨텐트 프로바이더의 협조를 필요로 합니다. 컨텐트 프로바이더는 URI당 퍼미션을 허용하기 위해, 매니페스트 파일의 프로바이더 요소 안에 android:grantUriPermissions 속성을 추가하거나, <grant-uri-permissions> 태그를 추가해야 합니다.

더 자세한 내용은 Context.grantUriPermission(), Context.revokeUriPermission(), Context.checkUriPermission() 메소드에 대한 레퍼런스 정보에서 확인하시기 바랍니다. 


이어서 학습할 내용들:

암묵적으로 기능을 요구하는 퍼미션들

내 앱에서 요구되는 퍼미션이 디바이스에서 제공하는 하드웨어나 소프트웨어 기능을 암묵적으로 요구하는 경우에 대하여 학습합니다.

<uses-permission>

내 앱이 필요로 하는 퍼미션을 매니페스트 파일에 선언하는 방법을 학습합니다.

Manifest.permission

모든 시스템 퍼미션에 대한 API 레퍼런스를 학습합니다.


추가로 학습할 내용들:

기기 호환성 (Device Compativility)

다양한 종류의 디바이스들에서 안드로이드가 어떻게 동작하는지에 대해서와, 그러한 디바이스들의 제공 가능한 기능들과 관련하여 어떻게 최적화시킬 수 있는지에 대해 자세히 학습합니다.

안드로이드 보안 훑어보기 (Android Security Overview)

안드로이드의 보안 모델에 대하여 학습합니다.


Posted by 개발자 김태우
,