Подтвердить что ты не робот

Обмен через Seekable Pipe или Stream с другим Android-приложением?

Множество действий Intent, таких как ACTION_VIEW, возьмите Uri, указывая на контент, над которым должно выполняться действие. Если содержимое поддерживается файлом - указывает ли Uri прямо на файл или на ContentProvider, обслуживающий файл ( см. FileProvider) - это обычно работает.

Существуют сценарии, в которых разработчики не хотят, чтобы контент находился в файле для совместного использования с другими приложениями. Один из распространенных сценариев для шифрования: дешифрованные данные должны находиться в ОЗУ, а не на диске, чтобы свести к минимуму риск того, что кто-то получит данные с расшифровкой.

Мое классическое решение для обмена с ОЗУ - использовать ParcelFileDescriptor и createPipe(). Однако, когда действие, отвечающее на ACTION_VIEW (или что-то другое), получает InputStream на этом канале, результирующий поток ограничен по сравнению с потоками, которые вы получаете, когда ContentProvider обслуживает содержимое из файла. Например, это примерное приложение отлично работает с Adobe Reader и выдает сообщение об ошибке QuickOffice.

Основываясь на , я полагаю, что createPipe() действительно создает канал, и что не доступны для поиска. В результате клиенты, которые пытаются "перемотать назад" или "быстро переправить", столкнулись с проблемами.

Я ищу надежное решение для совместного использования содержимого в памяти с сторонним приложением, которое обходит это ограничение. В частности:

  • Он должен использовать синтаксис Uri, который, вероятно, будет выполняться клиентскими приложениями (т.е. ACTION_VIEW исполнителями); решения, которые включают в себя что-то тупое, что клиентские приложения вряд ли узнают (например, передают такие-то с помощью Intent extra), не квалифицируются

  • Данные для совместного использования не могут быть записаны в файл как часть совместного использования (конечно, клиентское приложение может завершить сохранение полученных байтов на диск, но пусть игнорирует этот риск на данный момент)

  • В идеале это не связано с тем, что приложение хочет поделиться данными, открывающими ServerSocket или иным образом усугубляя риски безопасности

Возможные предлагаемые идеи включают:

  • Как можно перенастроить createPipe(), что приводит к поиску трубки

  • Некоторый способ использования основанного на сокетах FileDescriptor, который приводит к поисковой трубе

  • Какой-то RAM-диск или что-то еще, что похоже на файл для остальной части Android, но не стойкий

Ключевым критерием, если хотите, рабочего решения является то, что я могу получить PDF файл из ОЗУ, который QuickOffice может читать.

Любые предложения?

Спасибо!

4b9b3361

Ответ 1

Вы создали очень сложную комбинацию требований.

Давайте рассмотрим ваши идеи для решений:

Возможные предлагаемые идеи включают:

  • Как можно перенастроить createPipe(), что приводит к поиску трубки

  • Как использовать файл FileDescriptor на основе сокетов, который приводит к поиску трубки

  • Какой-то RAM-диск или что-то еще, что похоже на файл для остальной части Android, но не стойкий

Первый не будет работать. Эта проблема заключается в том, что примитив трубы, реализованный ОС, принципиально не доступен для поиска. Причина заключается в поддержке поиска, которая потребует от ОС буферизации всего содержимого "до" содержимого до тех пор, пока конец считывания не закроется. Это невозможно реализовать, если вы не установите ограничение на количество данных, которые могут быть отправлены через трубу.

Второй тоже не будет работать, по той же причине. Сокеты уровня ОС не доступны для поиска.

На одном уровне конечная идея (файловая система RAM) работает, по модулю, что такая возможность поддерживается ОС Android. (В любом случае, файл Ramfs доступен для поиска.) Однако поток файлов не является каналом. В частности, поведение в отношении конца файла отличается для потока файлов и канала. И получение потока файлов, чтобы он выглядел как поток канала с точки зрения читателя, повлек бы за собой некоторый специальный код с этой стороны. (Проблема аналогична проблеме запуска tail -f в файле журнала...)


К сожалению, я не думаю, что есть другой способ получить файловый дескриптор, который ведет себя как труба по отношению к концу файла, а также доступен для поиска... за исключением радикальной модификации операционной системы.

Если вы можете изменить приложение, которое читает из потока, вы можете обойти это. Это исключается из-за того, что fd необходимо прочитать и запросить QuickOffice, который (я полагаю) вы не можете изменить. (Но если вы можете изменить приложение, есть способы сделать эту работу...)


Кстати, я думаю, что у вас возникнут некоторые проблемы с этими требованиями в Linux или Windows. И они не являются специфичными для Java.


UPDATE

Здесь были различные интересные комментарии, и я хочу обратиться к некоторым из них здесь:

  • ОП объяснил прецедент, который мотивирует его вопрос. В принципе, ему нужна схема, в которой данные, проходящие через "канал" между приложениями, не будут уязвимы в том случае, если пользовательское устройство будет украдено (или конфисковано), пока приложения фактически запущены.

    Достижимо ли это?

    • В теории нет. Если постулировать высокую степень технической сложности (и методы, о которых общественность может не знать о...), тогда "плохие парни" могут проникнуть в ОС и прочитать данные из общей памяти, пока "канал" останется активным.

    • Я сомневаюсь, что такие атаки (на данный момент) возможны на практике.

    • Однако, даже если предположить, что "канал" ничего не записывает на "диск", все еще могут быть следы канала в памяти: например,

      • все еще смонтированные RAMfs или все еще активные сегменты разделяемой памяти или

      • остатки предыдущих RAMfs/разделяемой памяти.

      Теоретически эти данные могут быть теоретически получены при условии, что "плохой парень" не включает и не перезагружает устройство.

  • Было высказано предположение, что ashmem можно использовать в этом контексте:

    • Проблема отсутствия публичных Java-API может быть решена (например, написанием сторонних API-интерфейсов)

    • Настоящим камнем преткновения является необходимость в потоковом API. Согласно документам "ashmem", у них есть файловый API. Но я думаю, что это просто означает, что они соответствуют модели "файловый дескриптор". Эти FD могут передаваться из одного приложения в другое (через fork/exec), и вы используете "ioctl" для работы с ними. Но нет никаких указаний на то, что они реализуют "чтение" и "запись"... не говоря уже о "поиске".

    • Теперь вы могли бы реализовать поток чтения/записи/поиска поверх ashmem, используя собственные и Java-библиотеки на обоих концах канала. Но оба приложения должны быть "осведомлены" об этом процессе, возможно, до уровня предоставления параметров командной строки для настройки канала.

    Эти проблемы также относятся к старому стилю shmem... за исключением того, что настройка канала, вероятно, сложнее.

  • Другой потенциальный вариант - использовать RAM fs.

    • Это проще реализовать. Файлы в RAMfs будут вести себя как "обычные" файлы; при открытии приложения вы получаете файловый дескриптор, который можно читать, писать и искать... в зависимости от того, как он был открыт. И (я думаю), вы должны иметь возможность передавать запрашиваемый FD для файла RAMfs через fork/exec.

    • Проблема заключается в том, что оперативная система RAMfs должна быть "смонтирована" для ее использования. Пока он монтируется, другое (привилегированное) приложение также может открывать и читать файлы. И ОС не позволит вам размонтировать RAMfs, в то время как у какого-либо приложения есть открытые fds для файлов RAMfs.

    • Существует (гипотетическая) схема, которая частично смягчает сказанное выше.

      • Исходное приложение создает и монтирует "private" RAMfs.
      • Исходное приложение создает/открывает файл для чтения/записи, а затем отключает его.
      • Исходное приложение записывает файл, используя fd из открытого.
      • Исходное приложение forks/запускает приложение-приемник, передавая fd.
      • Приложение раковины читает из (по-моему) все еще находящегося в поиске fd, ища по мере необходимости.
      • Когда исходное приложение замечает, что процесс приложения приемника (дочернего) завершен, он размонтирует и уничтожает RAMfs.

      Это не требует модификации приложения чтения (приемника).

      Однако третье (привилегированное) приложение все же потенциально может попасть в RAMfs, найти несвязанный файл в памяти и прочитать его.

Однако, пересматривая все вышеизложенное, наиболее практичным решением по-прежнему является изменение приложения чтения (приемника) для чтения всего входного потока в byte[], затем откройте ByteArrayInputStream для буферизованных данных, Основное приложение может искать и reset по своему усмотрению.

Ответ 2

Это не общее решение вашей проблемы, но открытие PDF в QuickOffice для меня работает со следующим кодом (на основе вашего примера):

@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
    try {
        byte[] data = getData(uri);
        long size = data.length;
        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
        new TransferThread(new ByteArrayInputStream(data), new AutoCloseOutputStream(pipe[1])).start();
        return new AssetFileDescriptor(pipe[0], 0, size);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
};

private byte[] getData(Uri uri) throws IOException {
    AssetManager assets = getContext().getResources().getAssets();
    InputStream is = assets.open(uri.getLastPathSegment());
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    copy(is, os);
    return os.toByteArray();
}

private void copy(InputStream in, OutputStream out) throws IOException {
    byte[] buf = new byte[1024];
    int len;
    while ((len = in.read(buf)) > 0) {
        out.write(buf, 0, len);
    }
    in.close();
    out.flush();
    out.close();
}

@Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
    if (projection == null) {
        projection = new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
    }

    String[] cols = new String[projection.length];
    Object[] values = new Object[projection.length];
    int i = 0;
    for (String col : projection) {
        if (OpenableColumns.DISPLAY_NAME.equals(col)) {
            cols[i] = OpenableColumns.DISPLAY_NAME;
            values[i++] = url.getLastPathSegment();
        }
        else if (OpenableColumns.SIZE.equals(col)) {
            cols[i] = OpenableColumns.SIZE;
            values[i++] = AssetFileDescriptor.UNKNOWN_LENGTH;
        }
    }

    cols = copyOf(cols, i);
    values = copyOf(values, i);

    final MatrixCursor cursor = new MatrixCursor(cols, 1);
    cursor.addRow(values);
    return cursor;
}

private String[] copyOf(String[] original, int newLength) {
    final String[] result = new String[newLength];
    System.arraycopy(original, 0, result, 0, newLength);
    return result;
}

private Object[] copyOf(Object[] original, int newLength) {
    final Object[] result = new Object[newLength];
    System.arraycopy(original, 0, result, 0, newLength);
    return result;
}

Ответ 3

Я экспериментировал с кодом @josias. Я обнаружил, что некоторые из вызовов query(...) пришли с проекцией _data. Включение данных для этого столбца и установка фактической длины означает, что больше файлов можно открыть в других приложениях. Всегда включая _data, даже если нет в пропущенном проецировании, позволяет открывать еще больше типов файлов.

Вот что я закончил:

private static final String[] PROJECTION = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, "_data"};

@Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {

    byte[] data = getData(mSourcePath, url);

    final MatrixCursor cursor = new MatrixCursor(PROJECTION, 1);

    cursor.newRow()
        .add(url.getLastPathSegment())
        .add(data.length)
        .add(data);

    return cursor;
}