Trevor Foskett

Sr. Solutions Engineer @ Virtru

How to Share Google Docs Securely with a Google Apps Script

Add Additional Data Protections to G Suite with the Virtru SDK

I’m not a developer. I know a little JavaScript and Python — enough to write a couple of basic scripts to help with some of my more mundane tasks — but I certainly don’t have the skills or know-how to write actual software. What I do know, however, are my customers and the challenges they face in keeping their data secure. So when we released the Virtru SDK, touting its ease of use, I was skeptical. How hard is this going to be for my customers to integrate into their existing workflows? Could someone with my limited coding skills build something with this? Let’s find out!
TL; DR: Yes. It actually is really easy to add the Virtru SDK to your project, as I’ll demonstrate through the construction of a “Protect & Share” Google Docs add-on. See the completed project here.

Identifying a Project

To find a project that would both add value to my current workflows and demonstrate the functions of the Virtru SDK, I asked myself three questions:

What are my most common workflows?

Well, we’re a G Suite shop, so let’s start there. I spend a lot of time in Google Docs. Whether taking notes, writing reports, developing customer-facing documentation, or drafting this very blog post, it’s a rare day that I don’t work in at least one Google Doc. And since these documents can be internal-only or customer-facing, I’m always trying to figure out how best to share them.

Could these workflows use additional layers of data protection?

Google has some great protections and controls for sharing Docs content with other G Suite users, but the experience sharing outside the Google ecosystem has always been lacking. If you want to share a Doc with a non-Google user, you’re forced to make your data semi-public on the internet with no guarantee that it’ll only be accessed by authorized parties. Even for my least-sensitive documents, I’m not thrilled with this requirement; for my most sensitive content, it’s a showstopper.

What’s the lowest barrier to entry?

When I think “add features to G Suite”, I typically think “Chrome Extension”. But recently I’ve been experimenting with Google Apps Script, and it’s clearly the more compelling solution for this use case. The code runs directly within G Suite, so I won’t have to build and package an extension. It’s JavaScript & HTML-based, so it will support Virtru’s browser JS SDK. And it’s designed to do exactly what I ultimately want to do: build add-ons to extend Google Sheets, Docs, & Slides.

Goal

To build a Google Docs add-on that will allow me to generate a secure, encrypted copy of my Google Docs content and share it privately with authorized users. To that end, I’ll need to add the following functions to the Docs UI:
- Capture Google Doc content in a PDF.
- Encrypt that PDF with policy controls set by the user such as document watermarking, expiration date, and disable re-sharing.
- Download the encrypted PDF, OR
- Send the encrypted PDF as an email attachment.
The first, third, and fourth functions above are all straightforward and easily accomplished with the tools available in Google Apps Script. The only new functionality I need to add is to encrypt the document and apply my access controls.

Building It

When In Doubt, Copy & Paste

With zero experience writing a Docs add-on, I’ve decided to simply copy this sample translation project from Google’s own documentation, rip out the parts I didn’t need, then add my own code.
A basic add-on has two parts: server-side code running within the Google Apps Script environment — ‘Code.gs’ — and client-side code running directly on the page — ‘virtruSidebar.html’. I want to encrypt client-side, so I can copy some sample code from the Virtru Developer Hub’s browser JS quick-start into my client-side HTML file to import the Virtru SDK and styling:
<head>
    <link href="https://sdk.virtru.com/js/latest/auth-widget/index.css" rel="stylesheet"/>
    <script src="https://sdk.virtru.com/js/latest/auth-widget/index.js"></script>
    <script src="https://sdk.virtru.com/js/latest/virtru-sdk.min.js"></script>
</head>
Above: virtruSidebar.html - Adding Virtru SDK & styling to client-side HTML.
Next, I need to add the element that will actually perform the encryption step — the Virtru ‘client’. Again, the browser JS quick-start has some helpful code I can copy to generate the client:
<body>
  <div id="virtru-auth-widget-mount"></div>
  <script type="text/javascript">
    async function afterAuth(email) {
      // Run all client code from here. 
      // This will only be called when the user is successfully authenticated.
      const client = new Virtru.Client({email});
      const yourString = prompt('Type a sting to encrypt: ', 'Hello, world!');
      const encryptParams = new Virtru.EncryptParamsBuilder()
        .withStringSource(yourString)
        .withDisplayFilename('hello.txt')
        .build();
      const ct = await client.encrypt(encryptParams);
      await ct.toFile('hello.html');
    }
    // Set up the auth widget.
    Virtru.AuthWidget('virtru-auth-widget-mount', {afterAuth});
  </script>
</body>
Above: virtruSidebar.html - Loading default Virtru client.
This is a good start, but as-is, this client is configured to accept a simple string input and output an encrypted .txt file; I need to take a PDF as input and output an encrypted PDF before this will actually be useful.

Generating a PDF

As noted above, most of what this add-on will do can be accomplished with the native tools available in Google Apps Script, including the generation of a PDF from Google Docs content. First, I’ll add some server-side code to generate a blob from the current document and format as a base-64 string for easy transport to the client:
function createPDF() {
  var docBlob = DocumentApp.getActiveDocument().getBlob();
  docBlob.setName(doc.getName() + '.pdf');
  var blobB64 = Utilities.base64Encode(docBlob.getBytes());
  return blobB64;
}
Above: Code.gs - Creating PDF blob from Google Doc content.
And then a client-side function to call the above server-side function and return the document data to the client:

   /*      
    * Runs server-side function to return the contents
    * of the document in base64 format.
    *
    * @return {string} Base 64'd document content.
    */    
         function genPDF() {
           return new Promise(function(resolve, reject) {
             google.script.run
               .withSuccessHandler(function(blobB64) {
                 resolve(blobB64);
               })
               .createPDF();
           });
         }
Above: virtruSidebar.html - Calling the server-side function to generate pdf blob & transport to client.

Customizing the Quick Start

In order to encrypt a file rather than a simple text string, I’ll make two adjustments to the Virtru client:
Change the data source for encryption from string to array buffer. With an array buffer input, the client can accept any file type so long as it has been appropriately converted beforehand.Change the output type to ensure the final encrypted file will render as and decrypt to PDF.virtruSidebar.html: Updating the client to accept arrayBuffer and output pdf.tdf3.html.
    async function afterAuth(email) {
      // Run all client code from here. 
      // This will only be called when the user is successfully authenticated.
      const client = new Virtru.Client({email});
      const yourString = prompt('Type a sting to encrypt: ', 'Hello, world!');
      const encryptParams = new Virtru.EncryptParamsBuilder()
        .withArrayBufferSource(arrayBuffer)    // Change input to accept arrayBuffer
        .withDisplayFilename('hello.pdf')      // Change display filename to reflect PDF
        .build();
      const ct = await client.encrypt(encryptParams);
      await ct.toFile('hello.pdf.tdf3.html');  // Change output file extension to pdf.tdf3.html
    }
    // Set up the auth widget.
    Virtru.AuthWidget('virtru-auth-widget-mount', {afterAuth});
Above: virtruSidebar.html - Updating the client to accept arrayBuffer and output pdf.tdf3.html.
At this point, the add-on can generate an encrypted copy of the Google Doc. Great! However, it lacks any access controls or sharing options. By default, the document owner is the only authorized user. Let’s change that.

Adding Access Controls

Each data object protected by Virtru encryption is associated with a policy that dictates the authorized recipients and access controls such as expiration date & watermarking. Based on the data owner’s inputs, I can construct a policy object to pass to the encryption client. In the example below, user input is captured by a series of checkbox elements, but you can use whatever mechanism you like:
   /*
    * Builds policy according to user inputs. 
    *
    * @param {Array}   authUsers  The list of authorized users for this piece of content.
    * @return {Policy}            The policy for this piece of content.
    */    
         function buildPolicy() {
           var policy = new Virtru.PolicyBuilder();
   
           if ($('#watermark-toggle').is(":checked")) {
             policy.enableWatermarking();
           } 
           if ($('#disable-reshare-toggle').is(":checked")) {
             policy.disableReshare();    
           } 
           if ($('#expiration-toggle').is(":checked")) {
             if ($('#one-hour-expire').is(":checked")) {
               var expTime = (60*60);    // Expiration time is set in "seconds from now"
               console.log(expTime);
             }
             if ($('#one-day-expire').is(":checked")) {
               var expTime = (60*60*24);
               console.log(expTime);
             }
             if ($('#one-week-expire').is(":checked")) {
               var expTime = (60*60*24*7);
               console.log(expTime);
             }
             if ($('#one-month-expire').is(":checked")) {
               var expTime = (60*60*24*7*4);
               console.log(expTime);
             }
             policy.enableExpirationDeadlineFromNow([expTime]);
           } 
           return policy.build();
         }
Above: virtruSidebar.html - Creating an access control policy based on user input checkboxes.
The policy is then passed to the encryption client. Authorized users can be included in the policy object itself, or added as an additional encryption parameter {array} as shown here:
 const encryptParams = new Virtru.EncryptParamsBuilder()
  .withArrayBufferSource(arrayBuffer)
  .withDisplayFilename(`${docTitle}.pdf`)
  .withPolicy(policy)
  .withUsersWithAccess(authorizedUsers)
  .build();
Above: virtruSidebar.html - Adding the policy object and authorized users to the encrypt client.

Sending an Email

If I want to email the encrypted file to the authorized users rather than download it, I need to change the code a bit. Instead of encrypting directly to a file, I’ll encrypt to a string. That string includes the encrypted file ciphertext and the HTML data that allows a user to open it in the browser:
           const client = new Virtru.Client({email});
           const encryptParams = new Virtru.EncryptParamsBuilder()
             .withArrayBufferSource(arrayBuffer)
             .withDisplayFilename(`${docTitle}.pdf`)
             .withPolicy(policy)
             .withUsersWithAccess(authorizedUsers)
             .build();
           const ct = await client.encrypt(encryptParams);
           var ctString = await ct.toString();  // Encrypt to string rather than to file
          
           // Run server-side function to generate an email
           // to the list of authorized users and include
           // the HTML generated above as an attachment. 
           var userMessage = $('#email-body').val().replace(/\n/g, '<br/>');
                      // Take user input from a field in the sidebar and preserve line breaks
           google.script.run.sendEmail(ctString, authorizedUsers, userMessage);
Above: virtruSidebar.html - Updating the client to send encrypted content server-side for email generation.
This string can then be passed to a server-side function to create an email with the encrypted file attached:

function sendEmail(cipherText, recipients, userMessage) {

  // Get email address of file owner and assign attachment title.
  var fileOwner = Session.getActiveUser().getEmail();
  var fileName = DocumentApp.getActiveDocument().getName() + ".pdf.tdf3.html";
  
  // Provide a basic email body for recipients who do not support HTML.
  var emailBody = fileOwner + " has shared the encrypted file " + fileName + 
      " with you.\r\n\r\nIt\'s attached below; please download to open in" + 
      " Virtru\'s Secure Reader.";
  
  // Assign values to variables in emailHTML.html template.
  var htmlContent = HtmlService.createTemplateFromFile('emailHTML');
  htmlContent.fileOwner = fileOwner;
  htmlContent.fileName = fileName;
  htmlContent.userMessage = userMessage;
  
  // Create subject line based on filename and owner email address. 
  var subject = fileOwner + ' has shared a secure file: "' + fileName + '"';
  
  // Convert ciphertext string to HTML blob.
  var blob = Utilities.newBlob(cipherText, 'text/html', fileName);
  
  // Send the email with the tdf.html blob as attachment.
  MailApp.sendEmail(recipients, subject, emailBody, {
    name: fileOwner,
    attachments: [blob],
    htmlBody: htmlContent.evaluate().getContent()
  });
}
Above: Code.gs - Server-side function to generate and send an email with encrypted attachment.

Tying It All Together

With all the components working as needed, the final step is to make this into something a human can actually use. I added some toggles, buttons, text input fields, and a little jQuery to allow users to perform the following actions:
Choose “Encrypt & Download” or “Encrypt & Email”.Add authorized users.Add access controls.Include a custom message to email recipients.
This actually accounted for the majority of code that I wrote for this project. The protection-specific pieces — encrypting the file and adding access controls — account for a very small portion of the add-on. And of that portion, I copied and pasted most of it!
The entire project is available on GitHub, where you can take a look at all the additional elements I added. Here’s the full add-on in action:
Encrypt & Download:
Above: Encrypting, downloading, and previewing the document.
Encrypt & Email:
Above: Encrypting, emailing, and accessing the document.

Food for Thought

This ended up being easier to implement than I thought it would be. In fact, the steepest learning curve for me was figuring out where Google Apps Script and vanilla JavaScript differ and how they best interact. Adding encryption to the add-on was almost an afterthought. So while this project may not be directly applicable to you or your organization, it should illustrate how quickly you can integrate data protection into your own workflows.
With this under my belt, I think I’ll try to develop some additional functions or new add-ons to see how else I can leverage Google Apps Script in my day-to-day. Please let me know in the comments if you have any ideas for next projects or additional features!

About Me

I’m a Solutions Engineer at Virtru, where I work with our customers to identify areas of their environments where additional data protections are needed, and develop solutions using Virtru’s Data Protection Platform & SDKs to meet those needs. Our developer platform is built on our SaaS Key and Policy Infrastructure to support data security using the open standard TDF3, audit, and control.

Tags

Topics of interest