如何在Google云端硬盘中查看隐藏的应用数据?

时间:2014-04-03 08:35:43

标签: google-drive-api

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

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

5 个答案:

答案 0 :(得分:50)

确实,Google不允许您直接访问此隐藏的应用数据文件夹。

但是,如果您可以使用应用程序的客户端ID /客户端密码/数字签名(用于对Google服务器进行身份验证),那么您可以基本上模拟应用和访问使用云端硬盘API在您的Google云端硬盘中隐藏数据。

它在Android中的运作方式

通常,当Android应用程序想要访问Google API时(例如Drive,游戏或Google登录 - 并非所有人都受支持),它会与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帐户时(例如,首次设置手机时),通过向同一网址发出类似请求,通常会在设备上接收并存储此特殊令牌。不同的是,现在要求权限的应用是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解决方案,因此可能会遇到一些问题。例如,访问令牌仅存活一小时,之后不会自动刷新。

答案 1 :(得分:8)

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

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

答案 2 :(得分:4)

截至2020年9月的工作示例

注意:这实际上是Tomer's answer

的附加内容

Tomer's original answer发布以来,情况发生了变化。 当前,要获取主令牌并避免使用Error=BadAuthentication,您需要做两件事:

  • Passwd字段替换为EncryptedPasswd,并使用RSA公共密码通过RSA加密其值(确切的技术是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 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}"), '+/', '-_');
}

答案 3 :(得分:0)

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

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

                    }
                });


    }

答案 4 :(得分:-1)

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

                }
            });


}