Как я могу увидеть скрытые данные приложения на Google Диске?

У меня есть приложение для Android, в котором мои заметки хранятся в скрытых данных приложения. Я хочу экспортировать свои заметки, поэтому вопрос прост:

Как я могу получить доступ к скрытым данным приложения на Google Диске для определенного приложения?


person bizzz    schedule 03.04.2014    source источник
comment
на самом деле WhatsApp использует этот метод для резервного копирования данных   -  person Patrick Mutwiri    schedule 07.11.2015
comment
Кроме того, если вы не хотите видеть сами фактические данные приложения, а просто видите список приложений, в которых хранятся данные (вместе с размером и датой последней резервной копии), вы обнаружите, что приложение Android Drive и веб-интерфейс Drive предложить это, с небольшим поиском. Через сайт вы открываете меню настроек, а затем выбираете «Управление приложениями». В приложении «Диск» вы можете выбрать «Резервные копии» в левом меню, выбрать устройство, затем «Данные приложения», и вы увидите список приложений и информацию.   -  person G_H    schedule 03.10.2019


Ответы (4)


Действительно, Google не позволяет вам напрямую обращаться к этой скрытой папке данных приложения.

Но если вы можете получить идентификатор клиента / секрет клиента / цифровую подпись приложения, которые используются для аутентификации на серверах Google, тогда да, вы можете в основном эмулировать приложение и получать доступ к скрытым данным на вашем Google Диске с помощью Drive API .

Как это работает в Android

Обычно, когда приложению Android требуется доступ к Google API (например, к Диску, Игры или вход через Google — поддерживаются не все) он взаимодействует с клиентской библиотекой сервисов Google Play., который, в свою очередь, получает токен доступа от Google от имени приложения. Затем этот токен доступа отправляется с каждым запросом к API, чтобы Google знал, кто его использует и что ему разрешено делать с вашей учетной записью (OAuth 2.0). Чтобы получить этот токен доступа в первый раз, сервис Google Play отправляет запрос HTTPS POST на адрес android.clients.google.com/auth со следующими полями (вместе с другими данными):

  • Token — «мастер-токен», который идентифицирует учетную запись Google и, по сути, обеспечивает полный доступ к ней.
  • app — имя пакета приложения, например com.whatsapp
  • client_sig – цифровая подпись приложения (отправляется как SHA1).
  • device – идентификатор Android устройства.
  • service – области (разрешения), которые требуется приложению.

Поэтому, прежде чем мы сможем начать использовать Drive API от имени конкретного приложения, нам нужно знать его подпись и основной токен нашей учетной записи. К счастью, подпись можно легко извлечь из файла .apk:

shell> unzip whatsapp.apk META-INF/*
       Archive:  whatsapp.apk
          inflating: META-INF/MANIFEST.MF    
          inflating: META-INF/WHATSAPP.SF    
          inflating: META-INF/WHATSAPP.DSA
shell> cd META-INF
shell> keytool -printcert -file WHATSAPP.DSA   # can be CERT.RSA or similar
       .....
       Certificate fingerprints:
       SHA1: 38:A0:F7:D5:05:FE:18:FE:C6:4F:BF:34:3E:CA:AA:F3:10:DB:D7:99
       Signature algorithm name: SHA1withDSA
       Version: 3

Следующее, что нам нужно, это мастер-токен. Этот специальный токен обычно получают и сохраняют на устройстве при добавлении новой учетной записи Google (например, при первой настройке телефона), делая аналогичный запрос на тот же URL-адрес. Разница в том, что теперь приложение, которое запрашивает разрешения, является самим приложением сервисов Play (com.google.android.gms), а Google также предоставляет дополнительные параметры Email и Passwd для входа в систему. Если запрос будет успешным, мы вернем наш основной токен, который затем можно будет добавить в запрос пользовательского приложения.

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

Собираем все вместе

Теперь мы можем написать код для аутентификации, используя эти два HTTP-запроса напрямую — код, который может просматривать файлы любого приложения с любой учетной записью Google. Просто выберите свой любимый язык программирования и клиентскую библиотеку. Мне было проще использовать PHP:

require __DIR__ . '/vendor/autoload.php'; // Google Drive API

// HTTPS Authentication
$masterToken = getMasterTokenForAccount("[email protected]", "your_password");
$appSignature = '38a0f7d505fe18fec64fbf343ecaaaf310dbd799';
$appID = 'com.whatsapp';
$accessToken = getGoogleDriveAccessToken($masterToken, $appID, $appSignature);

if ($accessToken === false) return;

// Initializing the Google Drive Client
$client = new Google_Client();
$client->setAccessToken($accessToken);
$client->addScope(Google_Service_Drive::DRIVE_APPDATA);
$client->addScope(Google_Service_Drive::DRIVE_FILE);
$client->setClientId("");    // client id and client secret can be left blank
$client->setClientSecret(""); // because we're faking an android client
$service = new Google_Service_Drive($client);

// Print the names and IDs for up to 10 files.
$optParams = array(
    'spaces' => 'appDataFolder',
    'fields' => 'nextPageToken, files(id, name)',
    'pageSize' => 10
);
$results = $service->files->listFiles($optParams);

if (count($results->getFiles()) == 0) 
{
    print "No files found.\n";
} 
else 
{
    print "Files:\n";
    foreach ($results->getFiles() as $file) 
    {
        print $file->getName() . " (" . $file->getId() . ")\n";
    }
}

/*
$fileId = '1kTFG5TmgIGTPJuVynWfhkXxLPgz32QnPJCe5jxL8dTn0';
$content = $service->files->get($fileId, array('alt' => 'media' ));
echo var_dump($content);
*/

function getGoogleDriveAccessToken($masterToken, $appIdentifier, $appSignature)
{
    if ($masterToken === false) return false;

    $url = 'https://android.clients.google.com/auth';
    $deviceID = '0000000000000000';
    $requestedService = 'oauth2:https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file';
    $data = array('Token' => $masterToken, 'app' => $appIdentifier, 'client_sig' => $appSignature, 'device' => $deviceID, 'google_play_services_version' => '8703000', 'service' => $requestedService, 'has_permission' => '1');

    $options = array(
        'http' => array(
            'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close",
            'method' => 'POST',
            'content' => http_build_query($data),
            'ignore_errors' => TRUE,
            'protocol_version'=>'1.1',
             //'proxy' => 'tcp://127.0.0.1:8080', // optional proxy for debugging
             //'request_fulluri' => true
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    if (strpos($http_response_header[0], '200 OK') === false) 
    { 
        /* Handle error */
        print 'An error occured while requesting an access token: ' . $result . "\r\n";
        return false;
    }

    $startsAt = strpos($result, "Auth=") + strlen("Auth=");
    $endsAt = strpos($result, "\n", $startsAt);
    $accessToken = substr($result, $startsAt, $endsAt - $startsAt);

    return "{\"access_token\":\"" . $accessToken . "\", \"refresh_token\":\"TOKEN\", \"token_type\":\"Bearer\", \"expires_in\":360000, \"id_token\":\"TOKEN\", \"created\":" . time() . "}";
}

function getMasterTokenForAccount($email, $password) 
{
    $url = 'https://android.clients.google.com/auth';
    $deviceID = '0000000000000000';
    $data = array('Email' => $email, 'Passwd' => $password, 'app' => 'com.google.android.gms', 'client_sig' => '38918a453d07199354f8b19af05ec6562ced5788', 'parentAndroidId' => $deviceID);

    $options = array(
        'http' => array(
            'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close",
            'method' => 'POST',
            'content' => http_build_query($data),
            'ignore_errors' => TRUE,
            'protocol_version'=>'1.1',
             //'proxy' => 'tcp://127.0.0.1:8080', // optional proxy for debugging
             //'request_fulluri' => true
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    if (strpos($http_response_header[0], '200 OK') === false) 
    { 
        /* Handle error */
        print 'An error occured while trying to log in: ' . $result . "\r\n";
        return false;
    }

    $startsAt = strpos($result, "Token=") + strlen("Token=");
    $endsAt = strpos($result, "\n", $startsAt);
    $token = substr($result, $startsAt, $endsAt - $startsAt);

    return $token;
}

И, наконец, результаты -

Files:
gdrive_file_map (1d9QxgC3p4PTXRm_fkAY0OOuTGAckykmDfFls5bAyE1rp)
Databases/msgstore.db.crypt9    (1kTFG5TmgIGTPJuVynWfhkXxLPgz32QnPJCe5jxL8dTn0)
16467702039-invisible (1yHFaxfmuB5xRQHLyRfKlUCVZDkgT1zkcbNWoOuyv1WAR)
Done.

ПРИМЕЧАНИЕ. Это неофициальное, хакерское решение, поэтому оно может иметь несколько проблем. Например, токен доступа активен только в течение одного часа, после чего он не будет обновляться автоматически.

person Tomer    schedule 07.04.2016
comment
Отличный хак! Однако API немного изменился, ему больше не нравятся параметры fields и pagesize (хотя документация по-прежнему использует их), а метод getFiles больше недоступен. Но когда я использую print_r($results), я вижу данные. - person Noir; 22.04.2016
comment
Noir. Похоже, у меня работает. Какую версию API вы используете? @Tomer Я получаю похожие файлы - просто любопытно, где находится ключевой файл? Не знаете, как без этого восстановить данные с Google Диска? - person victorhooi; 14.05.2016
comment
@victorhooi Насколько я понимаю, в случае WhatsApp файл ключа хранится во внутреннем каталоге на устройстве (/data/data/com.whatsapp/files/key), но WhatsApp по-прежнему должен иметь возможность восстановить эту базу данных на новом телефоне, поэтому я предполагаю, что это также хранится на их серверах. - person Tomer; 14.05.2016
comment
Я использовал пример здесь и некоторый пример кода в документации Google Drive, чтобы создать версию Python, если кому-то интересно. codebjournal.mattdyer.us/2016/11/ - person Matt Dyer; 06.11.2016
comment
После обновления от марта 2017 года этот метод выдает: Произошла ошибка при попытке входа в систему: Error=BadAuthentication Можете ли вы обновить свой код рабочим примером, пожалуйста? - person Vasilis Lemonidis; 26.08.2017
comment
Было бы интересно посмотреть, как это соотносится с GDPR. Эти данные были загружены мной и принадлежат мне, но Google случайным образом не позволяет мне их увидеть или получить. - person Daniel Saner; 07.08.2018
comment
Можно ли читать данные приложения в iOS, написанные приложением Android? - person Safeer; 05.11.2018
comment
@vasilis-lemonidis Можно обойти Error=BadAuthentication, см. мой ответ ниже stackoverflow.com/a/64124345/5656721 - person tifssoft; 29.09.2020

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

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

person Cheryl Simon    schedule 03.04.2014
comment
Но нельзя ли как-то эмулировать рассматриваемое приложение и получить данные? - person bizzz; 05.04.2014
comment
Вы не можете видеть то, что находится в вашем собственном аккаунте Google Диска, это полная ерунда! Google Диск никогда не мог предложить эту функцию. :-/ - person Eduardo; 13.11.2015
comment
В соответствии с документацией https://developers.google.com/drive/v3/web/appdata вы можете получить доступ, загрузить и управлять файлами, если хотите. Только не через обычный пользовательский интерфейс Google Диска. - person N0thing; 12.08.2016
comment
@ N0thing похоже, что этот API можно использовать только для доступа к папке из самого приложения. Вы не можете получить доступ к данным из другого приложения. - person Quantum7; 11.12.2017
comment
@ Шерил Саймон Как вы удаляете данные? Если я не могу просмотреть данные, я не могу их удалить. Пытаюсь понять как и не повезло. - person Johnathon Sullinger; 16.03.2020
comment
@JohnathonSullinger Перейдите на drive.google.com, затем нажмите кнопку с изображением шестеренки в правом верхнем углу и выберите «Настройки» в меню. Во всплывающем окне выберите вкладку «Управление приложениями». В меню «Параметры» для рассматриваемого приложения должно быть меню «Удалить скрытые данные приложения». - person Merk; 29.05.2020

Рабочий пример по состоянию на сентябрь 2020 г.

Примечание. На самом деле это дополнение к ответу Томера

Ситуация изменилась с тех пор, как был опубликован исходный ответ Томера. В настоящее время, чтобы получить главный токен и избежать Error=BadAuthentication, вам нужны две вещи:

  • Замените поле Passwd на EncryptedPasswd и зашифруйте его значение с помощью RSA с открытым ключом Google (точный метод был заменено кем-то) — это можно сделать с помощью phpseclib.
  • Установите HTTPS-подключение к серверу Google с теми же параметрами SSL/TLS, что и в одной из поддерживаемых систем Android. Сюда входят версии TLS и точный список поддерживаемых шифров в правильном порядке. Если вы измените порядок или добавите/удалите шифры, вы получите Error=BadAuthentication. Мне потребовался целый день, чтобы понять это... К счастью, PHP ›=7.2 поставляется с openssl-1.1.1, в котором есть все необходимые шифры для эмуляции клиента Android 10.

Итак, здесь переписана функция getMasterTokenForAccount(), которая устанавливает шифры и использует EncryptedPasswd вместо обычного Passwd. А ниже encryptPasswordWithGoogleKey() реализация, выполняющая шифрование.

phpseclib необходим и может быть установлен с композитором: composer require phpseclib/phpseclib:~2.0

function getMasterTokenForAccount($email, $password) 
{
    $url = 'https://android.clients.google.com/auth';
    $deviceID = '0000000000000000';
    $data = array('Email' => $email, 'EncryptedPasswd' => encryptPasswordWithGoogleKey($email, $password), 'app' => 'com.google.android.gms', 'client_sig' => '38918a453d07199354f8b19af05ec6562ced5788', 'parentAndroidId' => $deviceID);

    $options = array(
        'ssl' => array(
            'ciphers' => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS'),
        'http' => array(
            'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close",
            'method' => 'POST',
            'content' => http_build_query($data),
            'ignore_errors' => TRUE,
            'protocol_version'=>'1.1',
             //'proxy' => 'tcp://127.0.0.1:8080', // optional proxy for debugging
             //'request_fulluri' => true
        )
    );
    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);
    if (strpos($http_response_header[0], '200 OK') === false) 
    { 
        /* Handle error */
        print 'An error occured while trying to log in: ' . $result . "\r\n";
        return false;
    }

    $startsAt = strpos($result, "Token=") + strlen("Token=");
    $endsAt = strpos($result, "\n", $startsAt);
    $token = substr($result, $startsAt, $endsAt - $startsAt);

    return $token;
}

function encryptPasswordWithGoogleKey($email, $password)
{
    define('GOOGLE_KEY_B64', 'AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0QRNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==');

    $google_key_bin = base64_decode(GOOGLE_KEY_B64);
    $modulus_len = unpack('Nl', $google_key_bin)['l'];
    $modulus_bin = substr($google_key_bin, 4, $modulus_len);
    $exponent_len = unpack('Nl', substr($google_key_bin, 4 + $modulus_len, 4))['l'];
    $exponent_bin = substr($google_key_bin, 4 + $modulus_len + 4, $exponent_len);
    $modulus = new phpseclib\Math\BigInteger($modulus_bin, 256);
    $exponent = new phpseclib\Math\BigInteger($exponent_bin, 256);

    $rsa = new phpseclib\Crypt\RSA();
    $rsa->loadKey(['n' => $modulus, 'e' => $exponent], phpseclib\Crypt\RSA::PUBLIC_FORMAT_RAW);
    $rsa->setEncryptionMode(phpseclib\Crypt\RSA::ENCRYPTION_OAEP);
    $rsa->setHash('sha1');
    $rsa->setMGFHash('sha1');
    $encrypted = $rsa->encrypt("{$email}\x00{$password}");

    $hash = substr(sha1($google_key_bin, true), 0, 4);
    return strtr(base64_encode("\x00{$hash}{$encrypted}"), '+/', '-_');
}
person tifssoft    schedule 29.09.2020
comment
Хм - я даже не могу заставить getMasterTokenForAccount вернуть действительный токен, все, что я получаю, это BadAuthentication. Я попытался сгенерировать фактический идентификатор устройства, но это не помогло... - person ChrisW; 09.11.2020
comment
@ChrisW Думаю, ваш набор шифров не совпадает с набором на устройстве Android. Каково ваше окружение? Это Debian 9/10 или Windows 10? - person tifssoft; 10.11.2020
comment
Ни один! Mac, работающий с PHP/композитором, установленным через Brew. Я нашел библиотеку python, которая создает реальное устройство на лету (github.com /egbertbouman/APKfetch/blob/master/apkfetch.py), но у меня все еще возникают проблемы с BadAuthentication, когда я пытаюсь получить токен службы для приложения. - person ChrisW; 11.11.2020
comment
@ChrisW Я не проверял это с Mac. Весьма вероятно, что в openssl из brew встроены другие шифры. Вы можете исследовать этот случай, изменив метод запроса на GET и URL на clienttest.ssllabs.com:8443/ssltest/viewMyClient.html — сохраните результат в файл .html и сравните список шифров со списком из среды, в которой он работает (Android 10 или Debian 10 или официальная сборка php для Windows). - person tifssoft; 12.11.2020

чтобы получить весь файл в данных приложения, попробуйте код

private void listFiles() {
        Query query =
                new Query.Builder()
                        .addFilter(Filters.or(Filters.eq(SearchableField.MIME_TYPE, "text/html"),
                                Filters.eq(SearchableField.MIME_TYPE, "text/plain")))
                        .build();
        getDriveResourceClient()
                .query(query)

                .addOnSuccessListener(this,

                        new OnSuccessListener<MetadataBuffer>() {
                            @Override
                            public void onSuccess(MetadataBuffer metadataBuffer) {
                                //mResultsAdapter.append(metadataBuffer);

                                for (int i = 0; i <metadataBuffer.getCount() ; i++) {
                                    retrieveContents(metadataBuffer.get(i).getDriveId().asDriveFile());
                                }
                            }
                        }

                )
                .addOnFailureListener(this, new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        Log.e(TAG, "Error retrieving files", e);
                        MainActivity.this.finish();
                    }
                });

    }

также вы можете скачать содержимое файла с помощью следующего кода

public void retrieveContents(DriveFile file) {

        Task<DriveContents> openFileTask =
                getDriveResourceClient().openFile(file, DriveFile.MODE_READ_ONLY);



        openFileTask.continueWithTask(new Continuation<DriveContents, Task<Void>>() {
            @Override
            public Task<Void> then(@NonNull Task<DriveContents> task) throws Exception {
                DriveContents contents = task.getResult();

                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(contents.getInputStream()))) {
                    StringBuilder builder = new StringBuilder();
                    String line;
                    while ((line = reader.readLine()) != null) {
                        builder.append(line).append("\n");
                    }

                    Log.e("result ", builder.toString());
                }

                Task<Void> discardTask = MainActivity.this.getDriveResourceClient().discardContents(contents);
                // [END drive_android_discard_contents]
                return discardTask;
            }
        })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {

                    }
                });


    }
person Chayon Ahmed    schedule 10.11.2018