82

I have an Android app that stores my notes in hidden app data. I want to export my notes so the question is simple:

How can I access the hidden app data in Google Drive for a specific app?

Simón
  • 456
  • 8
  • 23
bizzz
  • 1,795
  • 2
  • 19
  • 21
  • 5
    actually whatsapp is using this method to backup up data – Patrick Mutwiri Nov 07 '15 at 10:53
  • As an aside, if you don't wish to see the actual app data itself but just see a list of which apps stored data (along with the size and last backup date) you'll find that the Android Drive app and Drive web UI offer this, with a bit of searching. Via the site you open the settings menu and then choose "Manage Apps". Via the Drive app you can select Backups in the left menu, select a device, then App data and you'll get to see the list of apps and info. – G_H Oct 03 '19 at 14:15

5 Answers5

73

Indeed, Google does not let you access this hidden app-data folder directly.

But, if you can get your hands on the app's client ID/client secret/digital signature that is used for authentication against Google's servers - then yes, you can basically emulate the app and access the hidden data in your Google Drive using the Drive API.

How it works in Android

Usually, when an android application wants to access a Google API (such as Drive, Games or Google Sign-In - not all are supported) it communicates with the Google Play services client library, which in turn obtains an access token from Google on behalf of the app. This access token is then sent with each request to the API, so that Google knows who is using it and what he is allowed to do with your account (OAuth 2.0). In order to get this access token for the first time, the Google Play service sends an HTTPS POST request to android.clients.google.com/auth with these fields (along with other details):

  • Token - a "master token" which identifies the Google account and basically allows full access to it
  • app - the application package name, such as com.whatsapp
  • client_sig - the application's digital signature (sent as SHA1)
  • device - the device's Android ID
  • service - the scopes (permissions) that the app wants to have

So before we can start using the Drive API in the name of a specific app, we need to know its signature and our account's master token. Fortunately, the signature can be easily extracted from the .apk file:

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

The next thing we need is the master token. This special token is normally received and stored on the device when a new google account is added (for example, when first setting up the phone), by making a similar request to the same URL. The difference is that now the app that's asking for permissions is the Play services app itself (com.google.android.gms), and Google is also given additional Email and Passwd parameters to log in with. If the request is successful, we will get back our master token, which could then be added to the user's app request.

You can read this blogpost for more detailed information about the authentication process.

Putting it all together

Now, we can write a code for authentication using these two HTTP requests directly - a code that can browse any app's files with any Google account. Just choose your favorite programming language and client library. I found it easier with 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;
}

And finally, the results -

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

NOTE: This is an unofficial, hacky solution, and so it might have a few problems. For example, the access token is alive only for one hour, after which it won't be refreshed automatically.

Tomer
  • 3,149
  • 23
  • 26
  • Great hack! However the API has changed a bit, it doesn't like the `fields` and `pagesize` parameters anymore(Even though the [documentation](https://developers.google.com/drive/v3/web/appdata#inserting_a_file_into_the_application_data_folder) still uses them) and the `getFiles` method is not available anymore. But when I use `print_r($results)` I see the data. – Noir Apr 22 '16 at 11:32
  • Noir - It appears to work for me - which version of the API are you using? @Tomer I get similar files - just curious where the keyfile is? Confused how you'd be able to restore from Google Drive without this? – victorhooi May 14 '16 at 00:27
  • @victorhooi As I understand it, in the case of WhatsApp the key file is stored inside an internal directory on the device (`/data/data/com.whatsapp/files/key`) but WhatsApp is still supposed to be able to restore this database on a new phone, so my guess is that it is also stored on their servers. – Tomer May 14 '16 at 08:24
  • 3
    I used the example here and some example code in the google drive documentation to create a Python version of this if anyone is interested. http://codebjournal.mattdyer.us/2016/11/getting-hidden-app-data-from-your.html – Matt Dyer Nov 06 '16 at 01:21
  • 11
    After the update of March 2017, this method produces: An error occured while trying to log in: Error=BadAuthentication Can you update your code with a working example please? – Vasilis Lemonidis Aug 26 '17 at 14:36
  • 17
    It would be interesting to see how this holds up against the GDPR. This data was uploaded by me and belongs to me, yet Google randomly keeps me from seeing or retrieving it. – Daniel Saner Aug 06 '18 at 21:25
  • 1
    Is it possible to read appdata in iOS written by Android app? – Safeer Nov 05 '18 at 06:23
  • 2
    @vasilis-lemonidis It's possible to get around the Error=BadAuthentication, please see my answer below https://stackoverflow.com/a/64124345/5656721 – tifssoft Sep 29 '20 at 18:26
  • If you have a rooted Android device, you can also intercept data as it's uploaded with something like [HTTP Toolkit](https://httptoolkit.tech/). – hacker1024 Feb 09 '21 at 23:42
9

A working example as of September 2020

Note: this is actually an addition for Tomer's answer

Things changed since Tomer's original answer was posted. Currently, to get the master token and avoid the Error=BadAuthentication, you need two things:

  • Replace Passwd field with EncryptedPasswd and encrypt its value by RSA with google public key (the exact technique was reversed by some guy) - this can be done using phpseclib.
  • Make HTTPS connection to Google server with the same SSL/TLS options as in one of the supported Android systems. This includes TLS versions and exact list of supported ciphers in right order. If you change the order or add/remove ciphers you'll get Error=BadAuthentication. It took me a whole day to figure this out... Luckily, PHP >=7.2 comes with openssl-1.1.1 that has all the necessary ciphers to emulate Android 10 client.

So here is rewriten getMasterTokenForAccount() function that sets the ciphers and uses EncryptedPasswd instead of plain Passwd. And below is encryptPasswordWithGoogleKey() implementation that does the encryption.

phpseclib is necessary and can be installed with 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}"), '+/', '-_');
}
tifssoft
  • 216
  • 2
  • 4
  • Hmm - I can't even get `getMasterTokenForAccount` to return a valid token, all I get is BadAuthentication. I have tried generated an actual deviceID but this didn't help... – ChrisW Nov 08 '20 at 21:11
  • @ChrisW I guess your cipher set doesn't match the one from android device. What is your environment? Is it Debian 9/10 or Windows 10? – tifssoft Nov 10 '20 at 11:15
  • Neither! Mac, running PHP / composer installed via Brew. I did find a python library that generates an actual device on the fly (https://github.com/egbertbouman/APKfetch/blob/master/apkfetch.py) but I'm still having problems with BadAuthentication when I try and get a service token for an app – ChrisW Nov 11 '20 at 00:07
  • 1
    @ChrisW I didn't check it with Mac. It's highly probable that openssl from brew has different ciphers built in. You can investigate this case by changing request method to GET and URL to https://clienttest.ssllabs.com:8443/ssltest/viewMyClient.html - save the result to a .html file and compare the cipher list with the one from environment where it works (Android 10 or Debian 10 or official php build for Windows). – tifssoft Nov 12 '20 at 17:26
  • 1
    As of writing, running the code fails and exits with message: An error occured while trying to log in: Error=NeedsBrowser Url=https://accounts.google.com/signin/continue?sarp=1&scc=1&continue=https://accounts.google.com/o/android/auth?hl%3Den%26xoauth_display_name%3DAndroid%2BLogin%2BService%26source%3DAndroid%2BLogin&plt=AKgnsbtUuhgTwPVjXmBWoeeDYH1UBcUxXz4_YuyeRCNfvXVRQlbmnYMF_94OEPwMGqGtWjcDJ_nDKi90GJC3oTmDfKMocw_5busbZ1hSqUpNb9fAwqL3AHKItbvPLCWQto7dR0X7u0uf ErrorDetail=To access your account, you must sign in on the web. Touch Next to start browser sign-in. – davidchoo12 Aug 08 '21 at 17:05
7

The user cannot directly access data in the hidden app folders, only the app can access them. This is designed for configuration or other hidden data that the user should not directly manipulate. (The user can choose to delete the data to free up the space used by it.)

The only way the user can get access to it is via some functionality exposed by the specific app.

Cheryl Simon
  • 46,552
  • 15
  • 93
  • 82
  • 5
    But can't I somehow emulate the app in question and get the data? – bizzz Apr 05 '14 at 10:59
  • 46
    You can not see something that is in your own account of Google Drive is a complete nonsense! Google Drive could not offer this feature ever. :-/ – Eduardo Nov 12 '15 at 21:55
  • 5
    According to documentation `https://developers.google.com/drive/v3/web/appdata` you can access, download and manipulate the files if you want to. Just not though the normal Google Drive UI. – N0thing Aug 12 '16 at 18:24
  • 2
    @N0thing it looks like that API can only be used to access the folder from the app itself. You can't access data from another app. – Quantum7 Dec 11 '17 at 10:46
  • @Cheryl Simon How do you delete the data? If I can't view the data, I can't delete it. Trying to see how and not having any luck. – Johnathon Sullinger Mar 16 '20 at 02:16
  • 1
    @JohnathonSullinger Go to drive.google.com, then click the gear button at the upper right, and select Settings in the menu. In the pop up window, select the Manage Apps tab. In the Options menu for the app in question there should be a "Delete hidden app data" menu. – Merk May 29 '20 at 01:51
  • 3
    Who are they to tell me what I should or shouldn't directly manipulate? Recommending against it is one thing, but it's my data, so if I want to take the risk of possibly breaking something, I should be able to. – flarn2006 May 03 '22 at 16:39
-2
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) {

                }
            });


}
HK boy
  • 1,398
  • 11
  • 17
  • 25
Monica
  • 1
-3

to get all the file in app data try the code

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();
                    }
                });

    }

also you can download the content of file bye the following code

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

                    }
                });


    }