2020-02-23 11:06 — By Erik van Eykelen

Passing Data to and From an Extension Sandbox

Recently I worked on a browser extension which uses Libsodium to decrypt encrypted data on a web page. I quickly discovered that it is not wise to simply include sodium.js in the extension because Libsodium uses eval(). The elevated content security policy settings to allow eval() would lead to a rejection of the extension during Google's review process of the extension.

Luckily there's a workaround for this problem and it's called sandboxing.

In this post I describe how a browser extension can process web page content inside a sandboxed script, and what is needed to shuttle data from a web page (A), to the "safe" part of the extension (B), then to the "unsafe" part (C), and then traverse back again from C, to B, and finally to A.

The following files are discussed:

  • content.js
  • background.html which loads background.js
  • manifest.json
  • sandbox.html which loads sandbox.js

Let's begin with manifest.json:

{
  "name": "Extension",
  "version": "0.1",
  "description": "This is an example extension",
  "manifest_version": 2,
  "browser_action": {
    "default_icon": {
      "16": "images/16.png",
      "48": "images/48.png",
      "128": "images/128.png"
    },
    "default_title": "Example extension"
  },
  "background": {
    "page": "background.html",
    "persistent": false
  },
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content.js"
      ]
    }
  ],
  "sandbox": {
    "pages": [
      "sandbox.html"
    ]
  }
}

If you're familiar with an extension's manifest file you'll see it's nothing special. Note that all three files we're going to discuss next are referenced: background.html, content.js, and sandbox.html.

The background.html file loads background.js and contains an iframe:

<!doctype html>
<html>
  <head><script src="background.js"></script></head>
  <body>
    <iframe id="extension-iframe" src="sandbox.html"></iframe>
  </body>
</html>

The iframe loads sandbox.html, the file which you can use to load scripts that may not be loaded inside the extension itself due to aforementioned content security policies. The sandbox is where I load the Libsodium JavaScript script file (for the use case I described in the preface).

First, let's look at content.js. This script is executed on every page load. Content scripts are cool because they're able to access the content of web pages and even make changes to the DOM:

// Send data via message to background.js
chrome.runtime.sendMessage({ hello: 'from contentjs' }, function (response) {
  console.log('contentjs')
  console.log(response)
})

The content.js script uses sendMessage() to send { hello: 'from contentjs' } to an event listener defined in background.js:

// Listen to messages sent by content.js
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
  // iframe is defined by background.html
  var iframe = document.getElementById('extension-iframe')

  // Send message to script living inside iframe
  iframe.contentWindow.postMessage(msg, '*')

  // Listen to messages sent by script living inside iframe
  window.addEventListener('message', function (event) {
    console.log('backgroundjs')
    // Reply to content.js
    sendResponse(event.data)
  }, { once: true })

  return true // Indicates async sendResponse behavior
})

Step by step:

  • The background.js script listens to messages using onMessage.addListener() sent by content.js.
  • Messages originating from content.js are passed on to the sandbox by calling postMessage().
  • In order to listen for messages returned by the sandbox another event listener is set up using window.addEventListener().
  • The { once: true } flag ensures this listener is only used once by automatically "cleaning it up" after receiving a response from the sandbox. By default this listener would have stuck around, which would lead to multiple instance of the same listener in case the content.js script would make repeated calls to background.js! See here for details.

The sandbox.js script is able to receive messages from background.js using the following code:

// Listen to messages sent by background.js
window.addEventListener('message', function (event) {
  console.log('sandboxjs')
  event.source.postMessage({ hello: 'from sandboxjs' }, event.origin)
})

The sandbox posts a message back to background.js, which in turn posts the message back to content.js (via the sendResponse() call).

In order to check whether everything is working as advertised you have to look in two different places when developing a browser extension:

  • The console() output of content.js is sent to the developer console of the current web page.
  • The console() output of background.js and sandbox.js is sent to another developer console namely the one of the extension itself (because extensions are autonomous browser instances). Go to chrome://extensions/ in your browser, toggle "Developer mode" to "on", and click the "background.html" link inside your extension's details card. This will open the console dedicated to your extension.

If you load the extension using the "Load unpacked" feature, open a web page, and then open its developer console you'll see this:

contentjs
{hello: "from sandboxjs"}

Likewise, if you open the extension's developer console you'll see this:

sandboxjs
backgroundjs

This "proves" that sandbox.js is able to sent data all the way from its iframe back to your web page.

The complete code discussed in this post can be found here: https://github.com/evaneykelen/chrome-extension-scaffolding

Comments

Check out my products Releasewise, a project inbox for product owners and Sprouted, easy and encrypted user management.


2019-12-24 15:02 — By Erik van Eykelen

Recreating GitHub's Punch Card With SVG and Ruby

Recently I searched for GitHub's punch card feature which used to be part of the Graphs aka Insights tab. It seems it's no longer available from the user interface. Luckily the GitHub API still offers an endpoint which spits out the data used by the punch card.

This is how my punch card looks like:

The data can be obtained by calling the API:

curl --request GET \
    --url https://api.github.com/repos/{username}/{reponame}/stats/punch_card \
    --header 'authorization: Bearer {personal-access-token}'

Set username to your GitHub user name, reponame to the repository name, and create a Personal Access Token.

The result of curl will be something like this:

[
  [
    0,
    0,
    0
  ],
  [
    0,
    1,
    0
  ],
  [
    0,
    2,
    0
  ],
  [
    0,
    3,
    0
  ],
  ...

The punch card statistics are delivered as an array of tuples. Each tuple looks like like [weekday, hour, nr_commits].

In order to visualize the data I looked for a way to generate SVG programmatically. The Victor Ruby gem is exactly what I needed.

A link to the full source code can be found below. It's cool though to see how easy it is to render an SVG using Ruby and Victor:

Create a new SVG:

svg = Victor::SVG.new(width: '1120', height: '370', viewBox: "0 0 1120 370")

Render the weekdays:

xoff = 130
yoff = 80
g(font_size: 18, font_family: 'Helvetica', fill: '#555') do
  %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday].each_with_index do |day, idx|
    text(day, x: xoff, y: yoff + (idx * 40), 'text-anchor': 'end')
  end
end

Render a circle:

circle(cx: 10, cy: 20, r: 10, fill: '#ccc')

The least trivial thing was to display tooltips containing the number of commits when hovering over a cell. It turns out you need to create a rect containing a title to do this. Since each circle has a different radius I decided to draw a transparent rect on top of each cell making the hit box for each tooltip as big as possible.

Full source code

Check out my products Releasewise, a project inbox for product owners and Sprouted, easy and encrypted user management.


2019-12-15 13:28 — By Erik van Eykelen

Changelog JSON Specification

This document attempts to describe how a changelog can be stored in JSON format. The reason for posting this specification is that I needed a public format for https://releasewise.com/, and I was unable to find anything on the web.

But first, what is a changelog? Keep A Changelog defines it as follows:

A changelog is a file which contains a curated, chronologically ordered list of notable changes for each version of a project.

I proposes the following structure:

{
  "releases": [
    {
      "version": "",
      "codename": "",
      "released": "",
      "changelog": [
        {
          "title": "",
          "released": "",
          "label": "",
          "module": "",
          "language": "",
          "author": "",
          "description_group": [
            {
              "media_type": "",
              "body": ""
            }
          ],
          "urgency": "",
          "url_group": [
            {
              "url": "",
              "title": "",
              "url_type": "",
              "media_type": ""
            }
          ]
        }
      ]
    }
  ]
}

Example of a typical changelog:

{
  "releases": [
    {
      "version": "1.2.10",
      "codename": "Big Electric Cat",
      "released": "2019/03/30",
      "changelog": [
        {
          "title": "Tasks can now be dragged between projects",
          "released": "2019/03/28",
          "label": "Added",
          "module": "ui",
          "language": "en",
          "author": "David",
          "description_group": [
            {
              "media_type": "text/plain",
              "body": "Previously you had to cut a task and paste it into a project column in order to move tasks. With this change you can now drag and drop tasks between project columns."
            }
          ],
          "urgency": "low",
          "url_group": [
            {
              "url": "https://example.com/assets/screenshot.jpg",
              "title": "Dragging and dropping a task",
              "url_type": "image",
              "media_type": "image/jpeg"
            }
          ]
        }
      ]
    }
  ]
}
  • releases[]: array of { "version", "codename", "released", "changelog" } elements
  • version: version number, preferably based on Semantic Versioning (required)
  • codename: code name
  • released: release date of version formatted as ISO 8601 date or Unreleased
  • changelog: array of entries describing new features, bug fixes, etc
    • title: short description of the feature, preferably less than 50 characters (required)
    • released: release date of change formatted as ISO 8601 date or Unreleased
    • label: see below (required)
    • module: module name, useful for mono repos
    • language: ISO 639-1 language code
    • author: name of author
    • description_group: array of { "media_type", "body" } elements
    • media_type: allowed values are text/plain, text/html, text/markdown
    • body: description of the change
    • urgency: allowed values are low, medium, high, critical indicating importance of this change
    • url_group: array of { "url", "title", "url_type", "media_type" } elements
    • url: link to web page, image, document, or other URL-addressable asset (required)
    • title: description of resource URL points to
    • url_type: allowed values are web, image, video, audio, document
    • media_type: media/MIME type

Notes on label

The Keep A Changelog site recommends:

  • Added for new features
  • Changed for changes in existing functionality
  • Deprecated for soon-to-be removed features
  • Removed for now removed features
  • Fixed for any bug fixes
  • Security in case of vulnerabilities

Other often used labels are:

  • Unreleased
  • Improved
  • Refactored
  • Performance
  • Enhancement
  • Feature
  • Other

This specification doesn't specify a required set of label values. Although this would be preferable it seems there are too many variations out in the wild.

Check out my products Releasewise, a project inbox for product owners and Sprouted, easy and encrypted user management.