【问题标题】:How can I see hidden app data in Google Drive?如何在 Google Drive 中查看隐藏的应用数据?
【发布时间】:2014-05-14 22:55:38
【问题描述】:

我有一个 Android 应用程序,可以将我的笔记存储在 hidden app data 中。我想导出我的笔记,所以问题很简单:

如何访问特定应用的 Google 云端硬盘中隐藏的应用数据?

【问题讨论】:

  • 其实whatsapp就是用这种方法备份数据
  • 顺便说一句,如果您不想查看实际的应用程序数据本身,而只想查看哪些应用程序存储了数据的列表(以及大小和上次备份日期),您会发现Android Drive 应用程序和 Drive web UI 提供了这一点,并进行了一些搜索。通过该站点,您可以打开设置菜单,然后选择“管理应用程序”。通过云端硬盘应用,您可以在左侧菜单中选择备份,选择设备,然后选择应用数据,您将看到应用和信息列表。

标签: google-drive-api


【解决方案1】:

确实,Google 不允许您直接访问这个隐藏的 app-data 文件夹。

但是,如果您能够获得用于针对 Google 服务器进行身份验证的应用程序的客户端 ID/客户端密码/数字签名 - 那么是的,您基本上可以模拟该应用程序并使用以下方法访问您的 Google 云端硬盘中的隐藏数据Drive API。

它在 Android 中的工作原理

通常,当 android 应用程序想要访问 Google API(例如 Drive、游戏或 Google 登录 - 并非全部都受支持)时,它会与 Google Play services client library 通信,而 Google Play services client library 反过来会从Google 代表该应用程序。然后这个访问令牌随每个请求发送到 API,以便 Google 知道谁在使用它以及允许他对您的帐户做什么 (OAuth 2.0)。为了首次获取此访问令牌,Google Play 服务向 android.clients.google.com/auth 发送了一个 HTTPS POST 请求,其中包含这些字段(以及其他详细信息):

  • Token - 一个“主令牌”,用于识别 Google 帐户并基本上允许对其进行完全访问
  • app - 应用程序包名,如com.whatsapp
  • client_sig - 应用程序的数字签名(作为 SHA1 发送)
  • device - 设备的Android ID
  • service - 应用想要拥有的scopes(权限)

因此,在我们开始以特定应用的名义使用 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 还获得了额外的 EmailPasswd 参数来登录。如果请求成功,我们将取回我们的主令牌,然后可以将其添加到用户的应用请求中。

您可以阅读this blogpost 了解有关身份验证过程的更多详细信息。

把它们放在一起

现在,我们可以直接使用这两个 HTTP 请求编写用于身份验证的代码 - 该代码可以使用任何 Google 帐户浏览任何应用程序的文件。只需选择您最喜欢的编程语言和client library。我发现PHP 更容易:

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

// HTTPS Authentication
$masterToken = getMasterTokenForAccount("your_username@gmail.com", "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.

注意:这是一个非官方的、hacky 的解决方案,因此可能存在一些问题。例如,访问令牌仅存活一小时,之后将不会自动刷新。

【讨论】:

  • 如果有人感兴趣,我使用此处的示例和谷歌驱动文档中的一些示例代码来创建它的 Python 版本。 codebjournal.mattdyer.us/2016/11/…
  • 2017年3月更新后,此方法产生:尝试登录时发生错误:Error=BadAuthentication 你能用一个工作示例更新你的代码吗?
  • 看看这如何对抗 GDPR 会很有趣。此数据由我上传并属于我,但 Google 会随机阻止我查看或检索它。
  • Android 应用编写的 iOS 中是否可以读取 appdata?
  • @vasilis-lemonidis 可以绕过Error=BadAuthentication,请看我下面的回答stackoverflow.com/a/64124345/5656721
【解决方案2】:

用户不能直接访问隐藏的应用文件夹中的数据,只有应用可以访问它们。这是为用户不应直接操作的配置或其他隐藏数据而设计的。 (用户可以选择删除数据以释放其使用的空间。)

用户可以访问它的唯一方法是通过特定应用程序公开的某些功能。

【讨论】:

  • 但我不能以某种方式模拟有问题的应用程序并获取数据吗?
  • 您在自己的 Google Drive 帐户中看不到的东西完全是一派胡言! Google 云端硬盘无法提供此功能。 :-/
  • 根据文档https://developers.google.com/drive/v3/web/appdata,您可以根据需要访问、下载和操作文件。只是不是普通的 Google Drive UI。
  • @N0thing 看起来该 API 只能用于从应用程序本身访问文件夹。您无法从其他应用访问数据。
  • @JohnathonSullinger 访问 drive.google.com,然后点击右上角的齿轮按钮,然后在菜单中选择设置。在弹出窗口中,选择管理应用程序选项卡。在相关应用的选项菜单中应该有一个“删除隐藏的应用数据”菜单。
【解决方案3】:

截至 2020 年 9 月的工作示例

注意:这实际上是对Tomer's answer的补充

自从Tomer's original answer 发布后情况发生了变化。 目前,要获得主令牌并避免Error=BadAuthentication,您需要两件事:

  • Passwd 字段替换为EncryptedPasswd 并通过RSA 使用google 公钥加密其值(确切的技术是reversed by some guy)- 这可以使用phpseclib 来完成。
  • 使用与支持的 Android 系统之一相同的 SSL/TLS 选项与 Google 服务器建立 HTTPS 连接。这包括 TLS 版本和正确顺序的受支持密码的确切列表。如果您更改顺序或添加/删除密码,您将获得Error=BadAuthentication。我花了一整天的时间才弄清楚这件事...... 幸运的是,PHP >=7.2 附带了 openssl-1.1.1,它具有模拟 Android 10 客户端所需的所有密码。

所以这里重写了getMasterTokenForAccount() 函数,它设置密码并使用EncryptedPasswd 而不是普通的Passwd。下面是进行加密的encryptPasswordWithGoogleKey() 实现。

phpseclib 是必须的,可以用composer安装: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}"), '+/', '-_');
}

【讨论】:

  • 嗯 - 我什至无法让 getMasterTokenForAccount 返回有效令牌,我得到的只是 BadAuthentication。我已经尝试生成一个实际的 deviceID 但这没有帮助...
  • @ChrisW 我猜您的密码集与安卓设备中的密码集不匹配。你的环境是什么?是 Debian 9/10 还是 Windows 10?
  • 没有! Mac,运行通过 Brew 安装的 PHP/composer。我确实找到了一个可以动态生成实际设备的 python 库 (github.com/egbertbouman/APKfetch/blob/master/apkfetch.py),但是当我尝试获取应用程序的服务令牌时,我仍然遇到 BadAuthentication 问题
  • @ChrisW 我没有用 Mac 检查它。来自 brew 的 openssl 很可能内置了不同的密码。您可以通过将请求方法更改为 GET 并将 URL 更改为 clienttest.ssllabs.com:8443/ssltest/viewMyClient.html 来调查这种情况 - 将结果保存到 .html 文件并将密码列表与来自环境的密码列表进行比较它可以工作(Android 10 或 Debian 10 或 Windows 的官方 php 版本)。
  • 在撰写本文时,运行代码失败并退出并显示消息:尝试登录时发生错误:Error=NeedsBrowser Url=accounts.google.com/signin/…ErrorDetail=要访问您的帐户,您必须登录网络。触摸下一步开始浏览器登录。
【解决方案4】:

要获取应用数据中的所有文件,请尝试代码

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) {

                    }
                });


    }

【讨论】:

    【解决方案5】:
    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) {
    
                    }
                });
    
    
    }
    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) {
    
                    }
                });
    
    
    }
    

    【讨论】:

      猜你喜欢
      • 2015-02-25
      • 1970-01-01
      • 2019-08-17
      • 1970-01-01
      • 2013-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多