Об открытости данных в 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
    EastBanc Technologies 64,54
    Компания
    Поделиться публикацией
    Комментарии 16
    • 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 и позволяет для разных приложений выбрать некоторые разрешения. Запрещенные разрешения будут эмулироваться, например, если запретить чтения контактов приложению, то ему система будет выдавать пустой лист контактов.
                  • +1
                    Хотел бы еще внести маленькое дополнение.

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

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

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое