Overview

This post is to help explain and guide you in creating a secure plugin for the Apache Cordova Electron platform following Cordova’s pre-existing plugin pattern.

Since the release of Cordova Electron v3.0.0, plugins loaded in an isolated context from the app’s WebView. The front-end code can execute native code that has been exported and loaded in the WebView. With isolation context, plugin developers have greater control over what to expose to the front end. One of the purposes of this feature is to ensure that no arbitrary code from an external source can have full access access to the system.

Instead of calling node modules directly within the app, which is insecure, app developers will now call the plugin’s exported methods, which are made available through Cordova’s bridging implementation. Think of the exported methods as doorways to the native side, and plugin developers are the gatekeepers deciding what should be exposed.

Are you the gatekeeper?

This concept is known as Context Isolation and is considered the best practice for safety and security in providing node module access to Electron.

Previously, Cordova-Electron 2.x and lower allowed the front-end app to access the node modules by setting the nodeIntegration flag. But, this flag was not a permanent solution and was not secure. nodeIntegration should be disabled and removed if set.

In short, if the app loads any external third-party libraries that are trusted but a malicious user tampered with the remote code. Now that malicious user has access to node modules and gives them full access to the user’s file system. This risk is a concern to app users' security and privacy.

Read here for more on the flag’s security risks.

Lets dive into the guide to start making a secure plugin!

Cordova Plugin Directory & File Structure Overview

./
├── package.json
├── plugin.xml
├── src
│   └── electron
│       ├── index.js
│       └── package.json
└── www
    └── sample.js

The above directory & file structure focuses on the bare minimum for creating only a Cordova Electron plugin.

The structure is not strict so some directories and file names can be changed to your preference, but in this guide, we will follow the above project structure.

/package.json File

This npm package file, in the root directory, contains properties such as the plugin name, supported Cordova platforms, npm package dependencies etc.

This file contains the exact plugin name that end developers will use when installing the plugin with the cordova plugin add command. This is not the package that is installed into the Electron app and should not contain the Electron-related dependencies. The Electron app plugin will be dfined in another file.

This is the Cordova plugin package which will be released to the npmjs registry.

/plugin.xml File

This pluging.xml file is a counterpart to the package.json file. Since Cordova predates the npmjs registry, it had its own registry infrastructure. This file is what was used to contain all the plugin information. Over time as Cordova migrated to the npmjs registry, some of the information was moved over to package.json. But as there is still some old code in the Cordova tooling, it is currently safer to redefine some of these properties.

Eventually, some of these properties will become deprecated but the entirety of the file will not be going away. It will continue to contain platform-specific implementation, manipulating configuration files (AndroidManifest.xml, Info.plist) etc.

/src/electron Directory

Think of this directory and its contents as a sub-node module that will be installed in the Electron App.

In this example, everything starting from the root directory (/) is being bundled and deployed to the npmjs registry, including this sub-node module (/src/electron). During plugin installation, the sub-node module will be installed into the Electron application and bundled during the build process.

This sub-package does not need to be deployed to the npmjs registry as it will be installed by the local path.

/src/electron/package.json File

This package.json file is for the Electron App plugin.

/src/electron/index.js File

This is the main file that will contain the native-side logic. This file is loaded in contextIsolation and can have access to node modules.

The directory name and filename can be anything you prefer, but references should also be updated.

Additionally, if this file becomes large, it could be split into multiple files and imported with require blocks. In this example, I will keep it all in one file.

/www/sample.js File

This file contains the front-end code that is accessible to the WebView and which makes the call to the native layer.

Building Our Plugin Project

We will break this section down into three parts.

The first part will cover creating the base of the Cordova Plugin that contains all platform native code, configurations, and front-end web-accessible logic. Note that this example will only focus on Electron.

The second part will cover creating the Electron app’s portion of the plugin, which focuses on the native layer code.

The third part will be the front-end logic that the app developer uses within their app code to access the native layer’s code that was written in the second part.

Part 1: Creating the Cordova Plugin

Step 1: Create Project Directory

mkdir cordova-plugin-sample
cd cordova-plugin-sample

Step 2: Initalize Git

git init

Step 2: Initalize NPM Package

npm init

For the following questions:

  • package name: (cordova-plugin-sample)

    Generally, I leave this as default. It will use the directory name as the default, which I usually label as the plugin name that I want. If the plugin name is already taken in the npmjs registry, a different package name is required. You can also scope your package name.

    Please check the npmjs registry to confirm if your plugin name is available. The plugin name can be changed at a later time.

    For the default, press the enter key without typing anything in.

  • version: (1.0.0)

    This should be left as default, but you may change the starting version if you prefer. Some developers prefer starting with 0.0.1 as initial development and releases might be breaking and unstable.

    For default, press the enter key without typing anything in.

  • description:

    It is recommended to provide a short but detailed description of the plugin so end users can understand its purpose and searchability.

    In this guide, I will use the following

    Sample Cordova Plugin

  • entry point: (index.js)

    Cordova plugins do not use entry points. For now, use the default and remove it directly from the package.json file after initialization.

  • test command:

    In this guide, we will not cover testing. In this case, we can accept the default value and press enter to continue.

  • git repository:

    A git repository address can be added here if created. For this example, we will leave it empty and continue.

  • keywords:

    Keywords are used as search terms on the npmjs registry.

    For this guide, I will use the following keywords:

    ecosystem:cordova cordova cordova-electron sample

  • author:

    Author of the plugin. It can be either your name or a company’s name.

    For this guide, I will use:

    Erisu

  • license: (ISC)

    There are various license to choose from, but you will need to research each one to decide which license fits you best.

    In this guide, I will enter the following:

    Apache-2.0

After filling out the form:

After filling out the above questions for creating the npm package, you will see an output of what the package.json file content will look like. It should look something like this:

{
  "name": "cordova-plugin-sample",
  "version": "1.0.0",
  "description": "Sample Cordova Plugin",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "ecosystem:cordova",
    "cordova",
    "cordova-electron",
    "sample"
  ],
  "author": "Erisu",
  "license": "Apache-2.0"
}

Go ahead and press the enter key, if you have not already, so we can complete the npm package creation.

With your favorite text editor, let’s update the file with additional information that will be used later.

{
  "name": "cordova-plugin-sample",
  "version": "1.0.0",
  "description": "Sample Cordova Plugin",
  "keywords": [
    "ecosystem:cordova",
    "cordova",
    "cordova-electron",
    "sample"
  ],
  "author": "Erisu",
  "license": "Apache-2.0",
  "cordova": {
    "id": "cordova-plugin-sample",
    "platforms": [
      "electron"
    ]
  },
  "engines": {
    "cordovaDependencies": {
      "1.0.0": {
        "cordova": ">100",
        "cordova-electron": ">=3.0.0"
      }
    }
  }
}

In the above changes, we removed the main and scripts properties since we do not use them in this example.

We added the cordova and engines properties.

The cordova property contains the plugin id, which usually matches the name property, and the list of supported platforms.

The engines property contains a list of cordovaDependencies. These are basic requirements for the plugin to work and install successfully. In this case, we require cordova-electron to be version 3.0.0 or greater.

Step 3: Creating plugin.xml

As previously mentioned, this file is more or less a counterpart to the package.json. It contains similar data but with some additional properties.

For this part, it is better to copy and paste the following sample file content:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="cordova-plugin-sample"
    version="1.0.0">
    <name>Sample</name>
    <description>Sample Cordova Plugin</description>
    <license>Apache 2.0</license>
    <keywords>cordova,sample</keywords>

    <!-- platform requirements -->
    <engines>
        <engine name="cordova-electron" version=">=3.0.0" />
    </engines>

    <!-- Application's front-end code that  communicates with the native layer. -->
    <js-module src="www/sample.js" name="sample">
        <clobbers target="sample" />
    </js-module>

    <!-- electron platform configs -->
    <platform name="electron">
        <framework src="src/electron" />
    </platform>
</plugin>

The main difference between package.json and this file is the js-module and platform configurations. Other properties can be defined in this file but are not necessary for this plugin example. To learn more about the additional properties, refer to the official Apache Cordova docs.

<js-module>

The js-module is takes in the front-end source file and clobbers it to the WebView’s window object. App has access to the plugin front-end code once the app has triggered deviceready.

In the above example is basically saying that sample.js will be added to window.sample.

<platform>

In the above example, we are defining the Electron specific platform configurations. Other platform nodes can be created for other platforms, for example iOS and Android. It is important to have only one platform node per platform that you support.

Part 2: Creating the Electron App’s Plugin

Step 1: Creating Directory src/electron

Step 2: Initalize NPM Package for Electron App Plugin

{
  "name": "cordova-plugin-sample-electron",
  "version": "1.0.0",
  "description": "Electron Native Support for Cordova Sample Plugin",
  "main": "index.js",
  "keywords": [
    "cordova",
    "electron",
    "support",
    "native"
  ],
  "author": "Apache Software Foundation",
  "license": "Apache-2.0",
  "dependencies": {
    "systeminformation": "^5.8.0"
  },
  "cordova": {
    "serviceName": "Support"
  }
}

Step 3: Creating File src/electron/index.js

const { system, osInfo } = require('systeminformation');

module.exports = {
  getDeviceInfo: async () => {
    try {
      const { manufacturer, model, uuid } = await system();
      const { platform, distro, codename, build } = await osInfo();

      return {
        manufacturer,
        model,
        platform: platform === 'darwin' ? codename : distro,
        version: build,
        uuid,
        isVirtual: false
      }
    } catch (e) {
      console.log(e)
    }
  }
};

Part 3: Writting the Front-end Logic

Step 1: Creating File src/www/index.js

var argscheck = require('cordova/argscheck');
var channel = require('cordova/channel');
var exec = require('cordova/exec');
var cordova = require('cordova');

channel.createSticky('onCordovaInfoReady');
// Tell cordova channel to wait on the CordovaInfoReady event
channel.waitForInitialization('onCordovaInfoReady');

/**
 * This represents the mobile device, and provides properties for inspecting the model, version, UUID of the
 * phone, etc.
 * @constructor
 */
function Sample () {
    this.available = false;
    this.platform = null;
    this.version = null;
    this.uuid = null;
    this.cordova = null;
    this.model = null;
    this.manufacturer = null;
    this.isVirtual = null;
    this.serial = null;

    var me = this;

    channel.onCordovaReady.subscribe(function () {
        me.getInfo(
            function (info) {
                // ignoring info.cordova returning from native, we should use value from cordova.version defined in cordova.js
                // TODO: CB-5105 native implementations should not return info.cordova
                var buildLabel = cordova.version;
                me.available = true;
                me.platform = info.platform;
                me.version = info.version;
                me.uuid = info.uuid;
                me.cordova = buildLabel;
                me.model = info.model;
                me.isVirtual = info.isVirtual;
                me.manufacturer = info.manufacturer || 'unknown';
                me.serial = info.serial || 'unknown';
                channel.onCordovaInfoReady.fire();
            },
            function (e) {
                me.available = false;
                console.error('[ERROR] Error initializing cordova-plugin-device: ' + e);
            }
        );
    });
}

/**
 * Get device info
 *
 * @param {Function} successCallback The function to call when the heading data is available
 * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL)
 */
Sample.prototype.getInfo = function (successCallback, errorCallback) {
    argscheck.checkArgs('fF', 'Sample.getInfo', arguments);
    exec(successCallback, errorCallback, 'Sample', 'getDeviceInfo', []);
};

module.exports = new Sample();

Binding