Overview

This guide is to help create 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, the plugin’s code can now be load in a separate contest from the WebView’s context. What this means is that the plugin’s native code is isolated from the WebApp’s front-code.

But, App developers can still call native code with the bridging methods that you, as a plugin developer, will provide. These methods are only the doorways to the native side, or in other words, the gatekeeper.

Are you the gatekeeper?

This concept, Context Isolation, is a safe and secure way of developing an Electron plugin.

Previously, the Electron app had access to node modules with the nodeIntegration flag. This flag was a temporary workaround solution for accessing node modules. Now that Cordova Electron has implemented the proper plugin environment, the nodeIntegration should be disabled and removed.

This flag has security risks. For example, if the app is loads an external third-party library that is trusted but a malicious user tampered with the remote code. Now that malicious user has access to node modules that give them access to the user’s file system. This risk is a concern to app user’s security and privacy.

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

Cordova Plugin Directory & File Structure

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

The above directory & file structure focuses on the bare minimum. Having these files and directories will be the key factors for creating a successful plugin.

The directory names and some of the file names can be changed to your own personal preference, but this guide will be using the above structure. The guide will cover how these files and directories are used so you can understand what you would need to change if you choose to change any of the folder and file names.

package.json

This file contains the the plugin defintions such as the name, what platforms are supported, what npm dependencies to install, etc.

This file is not used in the Electron app and should not contain the Electron related dependencies. This is the Cordova plugin pacakge which you will release to the npmjs registry.

Sample File Content:

{
  "name": "cordova-plugin-sample",
  "version": "1.0.0-dev",
  "description": "Cordova Sample Plugin",
  "cordova": {
    "id": "cordova-plugin-sample",
    "platforms": [
      "electron"
    ]
  },
  "repository": "github:apache/cordova-plugin-sample",
  "bugs": "https://github.com/erisu/cordova-plugin-sample/issues",
  "keywords": [
    "ecosystem:cordova",
    "cordova",
    "cordova-electron",
    "sample"
  ],
  "author": "Apache Software Foundation",
  "license": "Apache-2.0",
  "engines": {
    "cordovaDependencies": {
      "1.0.0": {
        "cordova": ">100",
        "cordova-electron": ">=3.0.0"
      }
    }
  }
}

plugin.xml

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-dev">
    <name>Sample</name>
    <description>Cordova Sample Plugin</description>
    <license>Apache 2.0</license>
    <keywords>cordova,sample</keywords>
    <repo>github:apache/cordova-plugin-sample</repo>
    <issue>https://github.com/erisu/cordova-plugin-sample/issues</issue>

    <!-- 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>

src/electron/index.js

Native Side Code…

Sample File Content:

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)
    }
  }
};

src/electron/package.json

Electon App plugin package…

Sample File Content:

{
  "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"
  }
}

ww/sample.js

Sample File Content:

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();

Building Our Plugin Project

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)

    This should be left as default, but if the plugin is already taken in the npmjs registry, then you will need to pick a different package name.

    For default, prese enter 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 may start with 0.0.1 as the inital development phase where development releases can be release, but expected that the first release may not be compatiable with the development phase releases.

    For default, prese enter without typing anything in.

  • description:

    It is usefuly to provide a description about the plugin so end users can understand what is the purpose of this npm package. In this guide I will use the following

    Sample Cordova Plugin

  • entry point: (index.js)

    Cordova plugins do not use entry point. For now, we can use the default and later remove it directly from the package.json file which will be created after this process.

  • 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:

    If you have a git repository already create for this plugin, you can add the information here, but for this example we will leave it empty and continue.

  • keywords:

    Keywords is used as search terms and is useful for end-users to easily discover your package on npmjs register. In this guide I will use the following keywords:

    ecosystem:cordova cordova cordova-electron sample

  • author:

    Author will be who owns the plugin. It will be either your name or a company’s name if the company is the owner of the work. For this guide, I will enter:

    Erisu

  • license: (ISC)

    There is various different types of license that you can choose from but you will need to research and read up on each one to decide which license best fits your needs.

    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 enter, if you have not already, so we can complete the npm package creation.

Step 3: …

Binding