Об открытости данных в Android-приложениях

    Немного информации о том, какие данные в вашем приложении могут быть доступны для других программ и какие меры можно предпринять, чтобы это предотвратить.


    Компоненты

    Архитектуру Android-приложения можно представить в виде набора компонент (activities, services и пр.) и связей между ними. Android API предоставляет несколько стандартных способов общения между компонентами приложения: старт активити/сервис через контекст, отправка сообщений-интентов, использование ServiceConnection или ContentProvider’а и другое. Обо всём этом можно почитать в любом туториале о передаче данных в Android. Однако, есть один нюанс, о котором, как правило, умалчивается. Речь идет о доступности ваших компонент и данных, передаваемых между ними, для других приложений.

    Activity

    Для запуска одной из своих активити вы, наверняка, используете специально подготовленный intent:

    Intent myIntent = new Intent(CurrentActivity.this, NextActivity.class);
    startActivity(myIntent);
    


    Используя Context и Class, которые вы передали в конструктор интента, Android определяет package приложения и сам класс нужной активити, после чего запускает её.

    Вы также можете попробовать запустить чужую активити, зная ее package name и class name:

    Intent intent = new Intent();
    /* Одна из активити стандартного приложения Контакты*/
    intent.setClassName("com.android.contacts",
        "com.android.contacts.activities.PeopleActivity");
    startActivity(intent);
    


    Результат — запущенная активити.

    Значит ли это, что все активити могут быть запущены сторонним приложением? Нет. Если заменить в предыдущем примере classname на

    intent.setClassName("com.android.contacts", "com.android.contacts.preference.ContactsPreferenceActivity");
    


    то мы получим java.lang.SecurityException.

    На самом деле разработчик приложения сам решает, какие активити будут доступны, а какие нет, указывая для каждой активити в манифесте тэг android:exported равным true или false соответственно. Значение по умолчанию для этого тега ‒ false, т.е. все активити в AndroidManifest.xml, не имеющие этого тега, доступны только внутри приложения.

    Однако, посмотрев на манифест приложения Контакты (например, тут) можно заметить, что у PeopleActivity нет тега android:exported=«true», почему же нам удалось её запустить? Ответ можно найти в официальной документации: если активити содержит какой-либо интент-фильтр, то значением по умолчанию для android:exported будет true.

    Разработчику приложения не нужно беспокоиться о видимости своих activity вне приложения до тех пор, пока в какой-либо активити не появляется intent-filter. Как только вы объявляете intent-filter, спросите себя, готовы ли вы к тому, что эта активити может быть запущена кем-либо еще. Если нет — не забудьте указать <android:exported=«false»>.

    Service

    Видимость сервиса определяется также как и видимость активити, и мы бы не стали уделять сервисам отдельный пункт, если бы не одно “но”. В случае с сервисом, его видимость может завести куда дальше, чем незапланированный старт, как в случае с активити. Если в вашем сервисе предусмотрена возможность биндинга, то этой возможностью могут воспользоваться и другие и получить ссылку на ваш сервис через ServiceConnection. Давайте рассмотрим пример подключения к стандартному сервису воспроизведения музыки.

    Примечание: следующий пример актуален только на достаточно старых версиях плеера. В свежей версии разработчики разобрались с флагом exported и установили его в false :)

    Intent intent = new Intent();
    /* Сервис проигрывания аудио - часть стандартного плеера */
    intent.setClassName("com.android.music", "com.android.music.MediaPlaybackService");
    ServiceConnection conn = new MediaPlayerServiceConnection();
    /* Подключение к сервису */
    bindService(intent , conn, 1);
    
    class MediaPlayerServiceConnection implements ServiceConnection {
        public void onServiceConnected(ComponentName name, IBinder boundService) {
            Log.i("service connection", "Connected! Name: " + name.getClassName());
        }
    
        public void onServiceDisconnected(ComponentName name) {
            Log.i("MediaPlayerServiceConnection", "Disconnected!");
        }
    }
    
    

    После биндинга в переменной boundService будет храниться ссылка на сервис. Далее вы можете воспользоваться java reflection или Android Interface Definition Language (AIDL), чтобы иметь возможность вызывать методы сервиса напрямую, в результате чего можно, например, остановить воспроизведение, если стандартный плеер уже что-то играет.

    Рекомендации в этом случае такие же: установите <android:exported=«false»> для всех сервисов, имеющих какой-либо интент фильтр, если вы не планируете оставить его публичным.

    ContentProvider

    Основное предназначение ContentProvider’а — предоставление данных другим приложениям. Именно поэтому по умолчанию значение тега <android:exported> для этого компонента приложения равно true. Но основное предназначение — не единственное. ContentProvider часто используется:


    Во всех этих случаях вам, скорее всего, не нужно предоставлять данные вне приложения. Если это действительно так, то не забудьте указать провайдеру <android:exported=«false»>.

    BroadCast

    Общение с помощью широковещательных сообщений можно разделить на 3 этапа:
    1. Создание intent’а
    2. Отправка intent’а через Context#sendBroadcast(...)
    3. Получение intent’а всеми зарегистрированными BroadcastReceiver’ами.

    Каждый приёмник (receiver) при регистрации указывает некий IntentFilter, и сообщения будут доставляться этому приёмнику в соответствии с этим фильтром. Как можно догадаться, приёмник сообщения может удовлетворять фильтру и находиться вне нашего приложения. Вы можете добиться приватности рассылаемых сообщений, указав интенту нужный package:

    intent.setPackage(“com.android.example.mypackage”)
    


    или воспользоваться LocalBroadcastManager (доступен в support library), который также не даст улететь сообщению за границы приложения (на самом деле процесса):

    LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
    


    Более того, LocalBroadcastManager позволяет регистрировать приёмники, так что вы можете быть уверены, что получаемые такими приёмниками интенты тоже являются локальными, а не прилетают извне.

    Ресурсы

    Если коротко, то все используемые в приложении ресурсы доступны для чтения вне приложения. Для получения некоторых из них достаточно знать только package name целевого приложения:

    PackageManager pm = getPackageManager();
    /* Иконка приложения Youtube */
    Drawable icon = pm.getApplicationIcon("com.google.android.youtube");
    /* Логотип приложения Youtube */
    Drawable logo = pm.getApplicationLogo("com.google.android.youtube");
    ApplicationInfo applicationInfo = pm.getApplicationInfo("com.google.android.youtube", 0);
    /* Название приложения Youtube */
    CharSequence applicationLabel = pm.getApplicationLabel(applicationInfo);
    


    Если вам известна информация об активити внутри приложения, вы можете проделать подобные операции для каждой из них отдельно.

    Узнав имя конкретного ресурса, можно получить и его:

    Resources r = getPackageManager().getResourcesForApplication("com.google.android.youtube");
    /* Картинка-превью для виджета Youtube
    * youtube_widget_preview - имя файла
    * drawable - директория, в которой находится файл
    * com.google.android.youtube - package name
    */
    int id = r.getIdentifier("youtube_widget_preview", "drawable", "com.google.android.youtube");
    Drawable drawable = r.getDrawable(id);
    ImageView iw = new ImageView(context);
    iw.setImageDrawable(drawable);
    rootView.addView(iw);
    




    Этим способом мы смогли отобразить картинку-превью для виджета Youtube внутри собственного приложения.

    Главная загвоздка с ресурсами в том, что здесь нет способа их скрыть. Хранение приватных данных в ресурсах — плохая идея, но если у вас есть такая необходимость, лучше хранить их в зашифрованном виде.

    В завершении статьи хотелось бы сказать еще несколько слов о файловой системе в Android. Этот вопрос уже хорошо освещен, многие знают, что в Android для каждого приложения создается собственная директория /data/data/«app package name», доступ к которой имеет только само приложение (или группа приложений c одним sharedUserId). В этой директории находятся файлы настроек SharedPreferences, файлы базы данных приложения, кэш и многое другое, однако, не стоит полагаться на защищенность этого хранилища. Файлы в этой директории недоступны только до тех пор, пока:

    • вы создаете файлы с флагом Context.MODE_PRIVATE
    • на устройстве не получен root-доступ
    • приложение не будет обработано каким-либо менеджером backup’ов.

    Это значит, что приватные данные, так же как в случае с ресурсами, необходимо шифровать, даже если они находятся в internal storage.

    Полезные ресурсы по теме:
    Статья Security tips на developer.android.com
    Книга Application Security For The Android Platform от Jeff Six
    True Engineering
    Лаборатория технологических инноваций
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 17

      0
      Интересная статья о #androiddev и не how to, спасибо.
        +2
        В случае сервиса я бы еще указал android:permission. Если сервис должен быть доступен не только вашему приложению — вещь не лишняя.
        Пример:
        <service android:name=".MyService" android:process="example.service" android:exported="true" android:label="@string/app_name" android:permission="com.sec.example.service.ACCESS"> <intent-filter> <action android:name="com.sec.sec.example.service.MyService" /> </intent-filter> </service>
          +1
          Еще рекомендую обратить внимание на поле android:process. Если перед названием процесса поставить ":" он будет доступен только приложению поставляемому с сервисом.
          P.S. Извиняюсь за код, отформатировался криво(
            0
            Да, кастомный permission тоже полезная штука. Большая проблема в том, что проверка permission'ов происходит во время установки приложения и показывается пользователю, а пользователь, как изветсно, ну очень часто даже не читает, что там написано.
              0
              Тут кстати интересный вопрос возникает, который я все никак немогу проверить.

              Есть два приложения:
              Первое приложение декларирует компонент, скажем Activity и защищает его своим кастомным permission.
              Второе приложение использует этот компонент причем честно запрашивая этот permission.

              Допустим первое приложение установлено. При установке второго приложения, я так полагаю, у пользователя спросят, а хочет ли он установить это приложение, показав ему этот самый permission.

              Вопрос в том, что будет, если сначала установится второе приложение, а лишь затем первое?
                0
                Так здесь же описывается, как защитить свое приложение от того, чтобы к нему не подключались другие. Как мне кажется, в этом случае лучше использовать разрешения с уровнем signature. В этом случае, только приложения подписанные вашим ключом смогут получать доступ к компонентам, защищенным данным разрешением.

                Пользователю показываются только разрешения, у которых уровень соответствует dangerous.
                  0
                  Можно поставить protectionLevel=«signature» и тогда никакому другому приложению это разрешение не дадут, даже если оно прописано у него в манифесте. Единственный способ получить разрешение с таким параметром из другого приложения — подписать другое приложение тем же сертификатом, которым подписано главное.
                0
                Интересно, зачем нужны права на доступ? а именно:
                к управлению сетями, личная информация, учетные записи — обычному приложению, допустим тому же официальному Chrome для андроид с PlayMarket
                или FireFox открывает известные учетные записи, проверяет доступ к защищенному хранилищу, управление глобальными параметрами системы, запись звука, полный доступ к интернету управление NFC

                Объясните пожалуйста почему права выдаются по дефолту, не обрабатываются через запрос к пользователю хочет ли он предоставить эти права или делать ли такие запросы в дальнейшем по определенному приложению?
                  0
                  Риторический вопрос. Первую его часть лучше задать авторам конкретных приложений (тот же доступ в сеть часто нужен для рекламных баннеров), а вторую — авторам ОС. :)
                    –1
                    Со второй частью вопроса как раз все понятно: Android старается предоставить возможность по максимуму заменить свои стандартные компоненты. Если бы приложение каждый раз запрашивало доступ к СМС, например, вместо того, что бы однажды и навсегда получить этот доступ (как оно сейчас есть), то невозможно было бы заменить стандартный СМС мессенджер. Он бы попросту каждый раз при получении СМС запрашивал доступ у пользователя, что бы её прочитать.
                      +1
                      Если бы приложение каждый раз запрашивало доступ к СМС, например, вместо того, что бы однажды и навсегда получить этот доступ (как оно сейчас есть), то невозможно было бы заменить стандартный СМС мессенджер. Он бы попросту каждый раз при получении СМС запрашивал доступ у пользователя, что бы её прочитать.
                      Можно ведь запросить один раз (при первом СМС, например) и в дальнейшем не спрашивать. И в каком-то из обновлений 4.2+ так и было, ЕМНИП. А потом возможность просто убрали.
                        0
                        Ну так а чем это будет отличаться от «запросить один раз при установке» как сейчас? :)
                          +1
                          Тем, что некоторые разрешения можно будет давать, а некоторые нет. Например, фонарик просит права на чтение контактов — не давать. При этом ОС может отдать фонарику пустой список контактов, и все будут довольны. Понятно, что при запрете доступа к сети будут страдать и разработчики приложений с рекламой, и сам гугл.
                            +1
                            Вот тут я Вас категорически поддерживаю! Если бы API давал возможность выбирать из разрешений, мир андроида стал бы гораздо лучше
                              0
                              Поищите приложение AppOps. По-моему, оно работает на версиях Android'a 4.2-4.4 и позволяет для разных приложений выбрать некоторые разрешения. Запрещенные разрешения будут эмулироваться, например, если запретить чтения контактов приложению, то ему система будет выдавать пустой лист контактов.
                                0
                                Время идёт, платформа развивается. Интересно, сколько ещё комментариев на хабре сбылось? :)

                                Если вдруг кто-то дойдёт до этого коммента, детали нового api:
                                developer.android.com/training/permissions/requesting
                                habr.com/post/278945
                      +1
                      Хотел бы еще внести маленькое дополнение.

                      Для ContentProvider значение android:exported по умолчанию равно «true» если SDK <= 16 и «false» если SDK >= 17.

                      Если android:targetSdkVersion не объявлен, то берется значение android:minSdkVersion.
                      Если android:minSdkVersion не объявлен, то по умолчанию оно будет восприниматься системой как «1».
                      Таким образом android:exported будет «true» для ContentProvider.

                      Only users with full accounts can post comments. Log in, please.