Hacking Gmail for Privacy and Profit

My goals for Mymail-Crypt for Gmail have always been to allow everyone to be able to use Gmail to simply, and privately send messages. I’ve recently had time to conquer one of the bigger challenges that I’ve only poked at before: stopping automatic draft saving.

However, the whole process was an exercise in backtracking, frustration, and bursts of satisfaction. This post is going to be very technical, and I write it in hopes that someone attempting to do something similar with Gmail or general extension hacking can find something useful. Feel free to reach out to me with any questions.

No, this is not how to read your ex-girlfriend’s email, and no, I don’t actually profit from this extension.

AJAX Attack!

Ok. Let’s make Gmail stop unloading drafts. These are sent via XHR (ajax), so let’s intercept that from our extension and stop them from uploading:

window.XMLHttpRequest.prototype.open = function(){
    debugger;
}

Nothing. That’s weird. I see in the debugger, XHR calls going back and forth, but my debugger call isn’t registering.

Ok I wonder if this is a limitation of Content Script Chrome extensions. I’ve looked at the Content Script Documentation before but, let’s try some experiments:

$('body').append('<script type="text/javascript">
 alert("here");
</script>');

Ok, I get an ever so pleasant alert popup on page load. JS DOM injection Works.

$('body').append('<script type="text/javascript">
window.XMLHttpRequest.prototype.send = function(){debugger;}
</script>');

Doesn’t work. “Isolated Worlds” are in play. This is the limit of the content script sandbox. Time to take another approach.

Replace the composition box

So, if I can’t intercept the actual XHR messages, I think I’m going to have to change something on the page in order to prevent drafts from being visible. My first thought is to look at the composition form textbox. A quick look in the debugger shows that the code looks like this for the textbox:

<body g_editable="true" hidefocus="true" contenteditable="" class="editable LW-avf" id=":2ak" style="min-width:0;"><br></body>

I try replacing the :2ak with my own personal id. However, even after I make this change, the draft still gets uploaded. I try changing a few parent id values. Same response.

Higher in the DOM I find the form that is being submitted. When I rename this id value I get an error. Yay. My Fuzzing broke the page. I use the debugger to pause on all XHR requests. I confirm that the XHR request contents are essentially blank, as I would expect. This is a good sign.

Now, how do I make the page usable again.

Click hijacking

The first thing I try is to write a new jQuery.click() on things that I think I could click on, notably a elements. However, Gmail doesn’t use a lot of a elements. I seemed to sometimes get into some sort of race condition depending on how I was binding .click() events. There seem to be two things at play here:

  1. jQuery by default will make a queue with event handlers, but this can be overridden
  2. (again) Content Script Chrome extensions have limitations on manipulating the host page javascript

Time to pivot, no, not pivot I’m not a startup change my strategy yet again.

ctrl+z

Let’s try copying our form, I can leave the cloned form blank so Gmail thinks there is an empty message.

form.parent().prepend(form.clone());

Initially, this is looking pretty good. I can change the id values and hide the form I don’t want Gmail to see. However, it quickly becomes apparent there is a serious issue with this approach. The text box is not editable. That’s weird. Uggh, part of the form is an iframe. After wasting an embarrassing amount of time trying to force the iframe html value to be what I want, it becomes apparent my approach isn’t working and the content isn’t loaded when I try to clone it. Duh, what if we just wait until it is loaded.

setTimeout(function(){
  var form = $('#canvas_frame').contents().find('[class="fN"] &gt; form').first();
  form.parent().prepend(form.clone());
}, 500);

Alright, this is looking good. I can edit the text as I would expect. Uggh, new problem, I’m stuck in some sort of weird state where either the drafts are continuing to save or I’m hitting my new favorite Gmail 707 error. I proceed to fiddle with this, changing exactly how I insert or what I do, for quite some time. It continues to not work. Awesome.

Take a break (always undervalued).

ctrl+z

In fiddling with the cloning, I’m somehow slowly drawn back to my previous strategy of just renaming the composition form. Ok, so the major issue with this is that whenever I click any link I get a 707 error unless the form has the proper id. Let’s take a step back and see if somehow we can take advantage of the hierarchical structure of the DOM. What if I bind a mousedown on the main “canvas_frame” iframe object?

$('#canvas_frame').contents().mousedown(function(){
  alert('I'm CLICKING');
});

Boom. This works exactly as I want it to. Anywhere I click, I’m getting the alert. Now let’s try to refine this sledge-hammer approach to make it useful. I target the left hand side of the Gmail window:

Rather than a 707 error, I’m going to go ahead and assume you want to navigate away. If you don’t want drafts to autosave, navigating away means these should not be saved. So let’s rebind this left hand side

$('#canvas_frame').contents().find('[class="nH oy8Mbf nn aeN"]').mousedown(clearAndSave);
function clearAndSave(){
    $('#canvas_frame').contents().find('#gCryptForm').find('iframe').contents().find('body').text('');
    saveDraft();
}
function saveDraft(){
    var form = $('#canvas_frame').contents().find('#gCryptForm');
    form.attr('id', formId);
    $('#canvas_frame').contents().find('div[class="dW E"] &gt; :first-child &gt; :nth-child(2)').click();
}

This works well. I similarly blank out other navigation links. To save drafts, I bind the appropriate button to call the saveDraft function. To send a message I again save a draft and fake click the send button. jQuery provides the powerful ability to generically select the Send button:

$('#canvas_frame').contents().find('div[class="dW E"] > :contains("Send")');

General Approach

You may have notice that I search Gmail page based on class. In my experience (too many hours), these are less likely to change between iterations. I try to make my jQuery selections as generic as possible but still get the exact element I want — it’s often hard to find the appropriate balance.

There’s some other craftiness in my solution, but this article is already far longer than I had anticipated, and shows the points I consider most important.

Thoughts

It works! The Gmail interface is not intended to be manipulated by people. Figuring out how the complex functionality of the application is difficult and takes a lot of guess and check.

I’ve added an experimental option to disable draft autosaving in the latest version of Mymail-Crypt for Gmail (source), (Chrome Web Store extension) using the approach described here.

7 thoughts on “Hacking Gmail for Privacy and Profit

  1. Hi Sean,

    I am having a problem getting Mymail-Crypt to accept a friend’s public key and the plugin is saying “Mymail-Crypt for Gmail was unable to read this key. It would be great if you could contact us so we can help figure out what went wrong.” The only thing it doesn’t say is how to get the key to you to figure out what’s wrong. If you mail me I’ll file attach it to you.

    Terence.

  2. Concern about MyMail-crypt. It is critical that the private key be protected. However, when I generated a key and said “show private key”, it displayed it (no password required to access it). This means that if someone steals my notebook computer, they can get my private key – definitely NOT GOOD! I am not that familiar with OpenPGP but have quite a bit of experience with PGP and it encrypts the private key and it can’t be decrypted/displayed without entering the password.

    Also, I tried the encrypted/sign and encrypted/don’t sign option and they worked fine. However, it doesn’t appear that the “sign only” option works – when I send a self addressed message, I get no confirmation that the signature is valid.

    Your response will be greatly appreciated. MyMail-crypt is very easy to use but I can’t use it if the private key is easily compromised.

    1. Al — I hope I can address your concerns.

      When you are seeing the private key displayed like that it is quite complex, it contains the actual private key exponents as you might guess, but it is also encoded using your your password. It uses what it dubs a string to key identifier (s2k) which is a way of using your passphrase to protect the vital information in your private key.

      The point is that even if your key is compromised, the contents of it are useless without your passphrase. Mymail-crypt doesn’t store the exponents by themselves, only the key as it’s been encrypted with your passphrase.

      If you’re interested further in understanding S2K this you can read: http://tools.ietf.org/html/rfc4880#section-3.7

      Of course, if your computer is compromised your key is still vulnerable to being taken via brute forcing the password, in exactly the same manner as PGP/GPG any other program that locally stores your private key.

      In regards to signatures, not all types of signatures are currently supported, we are working on this as part of OpenPGP.js but it is not totally complete yet.

      Hope this helps your understanding, please reach out again if you have any other questions.

Leave a Reply

Your email address will not be published. Required fields are marked *