6

I am having trouble granting "reverse permissions" for apps that I wish to provide with sensitive data in a controlled manner.

My application is a time tracker and, because the time-tracking log could be considered personal information, I have created a permission for accessing it and assigned it the android.permission- group.PERSONAL_INFO permission group.

To export the time log from the phone I am adding the ability to send the log as an email attachment. The attachment is generated by a content provider that is protected by my newly added permission. My code for sending the email looks like this:

   String email = "someone@example.com";
   Uri uri = TimeLog.CSVAttachment.CONTENT_URI;
   Intent i = new Intent(Intent.ACTION_SEND, uri);
   i.setType("text/csv");
   i.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
   i.putExtra(Intent.EXTRA_SUBJECT, "Time log");
   i.putExtra(Intent.EXTRA_TEXT, "Hello World!");
   i.putExtra(Intent.EXTRA_STREAM, uri);
   i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
   startActivity(i);

When running it on my HTC phone, I get a pop-up choice between Gmail and HTC mail. Choosing Gmail, I get this exception in the Gmail app:

ERROR/AndroidRuntime(8169): Caused by: java.lang.SecurityException:
Permission Denial: reading com.mycompany.timelog.TimeLog uri
content://com.mycompany.timelog/csv_attachment from pid=8169,
uid=10035 requires com.mycompany.timelog.permission.READ_TIME_LOG

I do have android:grantUriPermissions="true" set on my provider but that's not helping. I have a theory about why this happens. I had expected FLAG_GRANT_READ_URI_PERMISSION to give Gmail the right to access my content provider, but I think what really happens is that this permission is granted to com.android.internal.app.ResolverActivity because there is more than one match for the Intent and Android creates a wrapper activity for displaying the choice to the user.

So, I've tried hard-coding this into my app just for testing:

   grantUriPermission("com.google.android.gm", uri,
       Intent.FLAG_GRANT_READ_URI_PERMISSION);

This allows Gmail to display the email correctly and I can press "Send". Unfortunately, after GMail has closed I get this exception in com.google.process.gapps:

ERROR/AndroidRuntime(7617): java.lang.SecurityException: Permission Denial: reading com.mycompany.timelog.TimeLog uri content://com.mycompany.timelog/csv_attachment from pid=7617, uid=10011 requires com.mycompany.timelog.permission.READ_TIME_LOG

Note that this is coming from a different PID and UID. This is because the actual call to openAssetFile happens from some sync provider component that belongs to a different package (com.google.android.googleapps?).

While I had some hope of eventually finding a way to grant permissions to the final receiver of my ACTION_SEND intent, the fact that the call to openAssetFile happens from some entirely different and practically unrelated package leaves me baffled as to how permission granting is supposed to work.

So ultimately my question is, given that the log is sensitive data, how would I allow it to be emailed as an attachment while honoring the privacy of the user (e.g. without making the attachment world readable)?

Nermeen
  • 15,883
  • 5
  • 59
  • 72
flodin
  • 5,215
  • 4
  • 26
  • 39
  • What do you mean by `making the attachment world readable`? – Peter Knego Nov 14 '10 at 21:52
  • I mean not restricting it to specific permissions, hence allowing any app to access the URI. Or, say, putting it on the SD card. – flodin Nov 14 '10 at 22:08
  • 1
    Great use of permissions. Though would it be feasible (for the recipient) to use public-key encryption to first encrypt the email attachment before it leaves your content provider? Or build in the ability to send the email directly from your app? – Christopher Orr Nov 25 '10 at 23:03
  • @Christopher That's an innovative workaround. But I think it would complicate things too much for the end user who might never have dealt with public-key encryption before. Sending the email myself would work but then I would lose the ability to do things like exporting the log to my dropbox account. – flodin Nov 28 '10 at 19:34
  • Hi, I am facing the same problem. have you resolved it? – Vasu Aug 17 '11 at 10:24
  • @Kailash No. What I did was limit access by default but before the user shares his log, he must confirm that the log is made public to all apps. It can me made private again by unchecking the option in the preferences. – flodin Aug 24 '11 at 12:23

3 Answers3

4

Dear people from the future,

It seems even google itself solves this problem in another way which i stumbled upon while trying to solve this same problem.

If you look at com.android.contacts.detail.ContactLoaderFragment you find in the method private Uri getPreAuthorizedUri(Uri uri):

mContext.getContentResolver().call(
            ContactsContract.AUTHORITY_URI,
            ContactsContract.Authorization.AUTHORIZATION_METHOD,
            null,
            uriBundle);

Which resolves to com.android.providers.contacts.ContactsProvider2 where a similar call method add the uri to a map mPreAuthorizedUris which is used in the query/update/...-methods.

The return value of that call is put in the Intent and then used.

  • Thanks, this seems like a smart solution. Generate some cryptographically secure cookie with a reasonable expiration time and pass that to the email app. – flodin Jan 24 '12 at 15:41
1

This is a nice approach, unfortunately as you are seeing there are probably a number of issues you will run in to in the framework that prevent you from doing it. There are currently some issues with granting uri permissions that make that functionality less useful than it should be (many of these will be addressed in Gingerbread), and on top of that Gmail just doesn't seem to expect this to happen and deal with retaining the granted permission as long as it needs to.

How large is this data? If it isn't too large, how about just including it directing in the Intent as a string?

hackbod
  • 90,665
  • 16
  • 140
  • 154
  • With an estimate of ten years of usage with 10 entries per day, I'd say about 1 to 2 MB of data. What is the maximum size of an intent? Do you believe that what I describe will be possible to do on gingerbread? – flodin Nov 15 '10 at 12:29
  • As a rough cut, >256K is too big. I suspect this still won't be possible in GB, because the app will probably still not be able to handle receiving this intent when it only temporarily has permission to access the data. I suspect most other email apps won't handle it, either. – hackbod Nov 15 '10 at 19:07
  • 1
    Is it really likely that the user will be exporting ten years of ten entries per day at once? I'm sure this permissions problem will be solved in ten years ;) Alternatively, isn't it likely that this time data is highly compressible and you could attach a .gz or .zip file rather than a .csv? – Christopher Orr Nov 25 '10 at 23:08
0

The data of your app can not be accessed by other apps. Simply use a private storage. You have several options:

  1. Sava timetracking data to database. App database is only visible to the app that created it. Other apps will not be able to see this data. When sending an email just query the database and create a text attachment.

  2. Create a private file via Context.openFileOutput().

Peter Knego
  • 79,991
  • 11
  • 123
  • 154
  • I don't think you understood the question because you are telling me to do what I'm already doing. The problem is that the text attachment becomes accessible to any app on the phone, so it's a security hole. – flodin Nov 15 '10 at 12:32
  • How does an attachment became available to any app? Afaik, it's accessible only by the app that created it (your) and the email app chosen by user. Am I missing something here? – Peter Knego Nov 15 '10 at 12:48
  • Well yes, if I create a private file with Context.openFileOutput() then the email app will be unable to read it. If, instead, I use the MODE_WORLD_READABLE flag, then any app on the phone will be able to read it. I need a way to restrict access so that only the email app can access it. – flodin Nov 15 '10 at 18:36