确实,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 还获得了额外的 Email 和 Passwd 参数来登录。如果请求成功,我们将取回我们的主令牌,然后可以将其添加到用户的应用请求中。
您可以阅读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 的解决方案,因此可能存在一些问题。例如,访问令牌仅存活一小时,之后将不会自动刷新。