Reading and Writing to the Windows Registry in-process from Node.js

Anthony Turner

A single npm module that enables Node developers to update the Windows Registry, create file associations, and much more!

As cross-platform application developers, we often need the ability to update the Windows Registry when installing our applications on Windows. Common features requested by app developers include reading and writing keys to Windows registry, creating file associations to make Windows assign default programs to a file extension, elevating processes to run as an administrator, and last but not least doing all this within the same process as our application. This code story describes the development of a single npm module that enables Node developers to perform these commonly-requested operations when installing Node.js applications on Windows.

The Problem

Electron is a framework that enables developers to build cross platform desktop apps with web technologies. Electron is behind Microsoft’s Visual Studio Code, Slack’s Apps, GitHub’s Atom, Facebook’s Nucleide, Docker’s Kinematic, and a few others. Prior to our hackathon with GitHub and Visual Studio Code, developers had limited number of resources to work with in order to interact with the Windows Registry. More importantly, there was no library that allowed developers to update the Windows Registry without running a separate shell script. In order to provide a great app install experience for users on Windows, it was obvious that we needed to create an NPM module that can address all these issues. Since this is an NPM module, all Node.js applications can take advantage of this library.

npm module for windows registry

Overview of the Solution

We partnered with GitHub, the maintainer of Electron, and Visual Studio Code, to enable registry operations and file associations for Node application on Windows. As a result of our hackathon, there is now a NPM module for Windows Registry you can include in your application to do just that.

Interact with Windows APIs

This library interacts with native Windows APIs. We leveraged the following node modules to enable Node.js application to communicate with Windows interfaces:

  • node-ffi – A Node.js addon for loading and calling dynamic libraries using pure Javascript. It can be used to create bindings to native libraries without writing any C++ code. We used this module to load a list of Windows DLLs to call native Windows APIs.
  • ref – A NPM module that turns buffer instances into pointers. We used this module to define our own data types to map to the Windows data types. It also conveniently allowed us to reference and dereference buffers.
  • ref-struct – This module allows us to define and implement structures. In order to communicate with Windows APIs, we needed to implement structs that mirror the inputs and outputs of Windows interfaces.

We leveraged the following Windows DLLs to communicate with Windows APIs.

  • Advapi32.dll – A Windows library that supports numerous APIs including many security and registry calls. We used this library to perform CRUD operations in Windows Registry.
  • Shell32.dll – A Windows library that contains Windows Shell APIs, which are used to open processes on Windows. We used this library to elevate privilege of a process, similar to the experience of running an application as an administrator.

Let’s dive into the code!

Creating Node Wrappers for Windows APIs

We created a Node wrapper adv_api.js for Advapi32.dll to support basic registry manipulation. Using the ffi node module, we can load Advapi32.dll. Then for each Windows API we wanted to call, we created a Javascript API with the expected inputs and outputs.

In C++, the follow example illustrates the use of RegOpenCurrentUser, which is used to retrieve a handle to the HKEY_CURRENT_USER key for the user that the current thread is impersonating.

HKEY keyCurrentUser;
lResult = RegOpenCurrentUser(KEY_READ, &keyCurrentUser);

In our Node.js wrapper, we used the ffi node module to load Advapi32.dll. Then we defined the RegOpenCurrentUser API and other Registry related APIs with their expected inputs and outputs.

var ffi = require('ffi');
var advApi = ffi.Library('Advapi32', {
    RegOpenCurrentUser: ['uint64', [types.REGSAM, types.PHKEY]],
    RegQueryValueExA: ['uint64', [types.HKEY, 'string', 'pointer', types.LPDWORD, types.LPBYTE, types.LPDWORD]],
    ...

});

Similarly, we created a Node wrapper shell32.js for Shell32.dll, which is used to launch a process on Windows.

In C++, the following example illustrates the use of ShellExecuteEx to launch an application:

BOOL result = ShellExecuteExA(&ShExecInfo);

In our Node.js wrapper, we used the ffi node module to load Shell32.dll, then we defined the ShellExecuteExA API with its expected input and output.

var ffi = require('ffi');
var shell32 = ffi.Library('Shell32', {

    ShellExecuteExA: ['bool',  [SHELLEXECUTEINFOPtr]]
});

Create Windows Data Types in JavaScript

As you can see, in order to replicate the exact Windows API call in JavaScript, we needed to create the same function with the exact inputs and outputs of the same data type as the Windows native data types.

For example, let’s look at how we defined the data types for the ShellExecuteExA function.

In C++, here is the definition of the ShellExecuteEx function:

BOOL ShellExecuteExA(
  _Inout_ SHELLEXECUTEINFO *pExecInfo
);

With the above definition, ShellExecuteEx expects a parameter of the type SHELLEXECUTEINFO*, which is a pointer to a SHELLEXECUTEINFO structure that contains and receives information about the application being executed. It also has a return value of type BOOL, which represents the result of executing this call.

In C++, this is the definition of the SHELLEXECUTEINFO structure.

typedef struct _SHELLEXECUTEINFO {
  DWORD     cbSize;
  ULONG     fMask;
  HWND      hwnd;
  LPCTSTR   lpVerb;
  LPCTSTR   lpFile;
  LPCTSTR   lpParameters;
  LPCTSTR   lpDirectory;
  int       nShow;
  HINSTANCE hInstApp;
  LPVOID    lpIDList;
  LPCTSTR   lpClass;
  HKEY      hkeyClass;
  DWORD     dwHotKey;
  union {
    HANDLE hIcon;
    HANDLE hMonitor;
  } DUMMYUNIONNAME;
  HANDLE    hProcess;
} SHELLEXECUTEINFO, *LPSHELLEXECUTEINFO;

In our Node.js wrapper windef.js, we needed to create the same SHELLEXECUTEINFO struct. Luckily, we had ref, ref struct, and ref union node modules to help us create the same struct in Javascript.

SHELLEXECUTEINFO: struct({
        cbSize: types.DWORD,
        fMask: types.ULONG,
        hwnd: types.HWND,
        lpVerb:  types.STRING,
        lpFile:  types.STRING,
        lpParameters: types.STRING,
        lpDirectory: types.STRING,
        nShow: types.INT,
        hInstApp: types.HINSTANCE,
        lpIDList: types.LPVOID,
        lpClass: types.STRING,
        hkeyClass: types.HKEY,
        dwHotKey: types.DWORD,
        DUMMYUNIONNAME: DUMMYUNIONNAME,
        hProcess: types.HANDLE
    })

For each struct member, we looked up its data type and its size from the Windows Data Types reference, then created the same data type with the same properties in types.js. We leveraged the ref node module to create types.

var ref = require('ref');

var types = {
    REGSAM: ref.types.uint64,
    DWORD: ref.types.uint32,
    ULONG: ref.types.uint32,
 ...

};

Now that we have the SHELLEXECUTEINFO struct in Javascript, we can create an instance of it in utils.js with the runas ShellExecuteEx verb for the lpVerb member and a file path to the process we want to launch for the lpFile member. Let’s call it shellexecuteinfoval. This instance can then be used as a parameter of ShellExecuteExA so that we can launch a process with elevated privilege.

var shellexecuteinfoval = new windef.SHELLEXECUTEINFO({
            cbSize: windef.SHELLEXECUTEINFO.size,
            fMask: 0x00000000,
            hwnd: null,
            lpVerb: lpVerb,
            lpFile: filepath,
            lpParameters: parameters,
            lpDirectory: null,
            nShow: SW_SHOWNORMAL,
            hInstApp: hInstApp,
            lpIDList: null,
            lpCLass: null,
            hkeyClass: null,
            dwHotKey: null,
            DUMMYUNIONNAME: {
                hIcon: null,
                hMonitor: null
            },
            hProcess: ref.alloc(types.HANDLE)
        });

Let’s call ShellExecuteExA with a reference of our new parameter. We implemented this as an asynchronous call so that when the UAC (User Account Control) prompt is launched, the current process can continue to run without waiting on the user to respond.

shell32.ShellExecuteExA.async(shellexecuteinfoval.ref(), callback);

To wrap everything together, we created an API elevate in our module that takes in a filepath of the process you want to launch, its parameters if any, and a callback to get the user’s response to the UAC prompt.

module.exports = {
    elevate: function (filepath, parameters, callback) {
    ...
    shell32.ShellExecuteExA.async(shellexecuteinfoval.ref(), callback);
}};

The process you want to launch with admin access will only be launched after the callback is called and only if the user clicks Yes in the UAC prompt. Otherwise, the process will not be launched. If the user is already running as an admin, the UAC prompt will not be triggered and the process you provided will be launched as an administrator automatically.

Here is an example of this running:

utils.elevate('C:\Program Files\nodejs\node.exe', 'index.js', function (err, result){console.log('callback');});

Elevate

Enable Your Application

To add the NPM module for Windows Registry to your Node application, install the package:

npm install windows-registry

Installation

To install node modules that require compilation on Windows, make sure you have installed the necessary build tools. Specifically, you need npm install -g node-gyp, a cross-platform cli written in Node.js for native addon modules for Node.js.

To install node-gyp, you need to have the following prerequisites installed and configured in your development environment.

  • Install python v2.7.3, and add it to your PATH, npm config set python python2.7
  • Install VC++ Build Tools Technical Preview. You do not need to install the full Visual Studio, only the build tools are required.
  • Launch cmd, npm config set msvs_version 2015 --global (this is instead of npm install [package name] --msvs_version=2015 every time.)

Once the prerequisites are installed, you should be able to do npm install -g node-gyp.

Reading and Writing to the Windows Registry

This library implements only a few of the basic registry commands, which allow you to do basic CRUD operations for keys in the registry.

Opening a Registry Key

Registry keys can be opened by either opening a predefined registry key defined in the windef module:

var Key = require('windows-registry').Key;
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);

Or you can open a sub key from an already opened key:

var Key = require('windows-registry').Key;
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '', windef.KEY_ACCESS.KEY_ALL_ACCESS);
var key2 = key.openSubKey('.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);

And don’t forget to close your key when you’re done. Otherwise, you will leak native resources:

key.close();

Creating a Key

Creating a key just requires that you have a Key object by either using the predefined keys within the windef.HKEY or opening a subkey from an existing key.

var Key = require('windows-registry').Key;
// predefined key
var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '', windef.KEY_ACCESS.KEY_ALL_ACCESS);
var createdKey = key.createSubKey('test_key_name', windef.KEY_ACCESS.KEY_ALL_ACCESS);

Deleting a Key

To delete a key just call the Key.deleteKey() function.

createdKey.deleteKey();

Writing a Value to a Key

To write a value, you will again need a Key object and just need to call the Key.setValue function:

var Key = require('windows-registry').Key,
    windef = require('windows-registry').windef;

var key = new Key(windef.HKEY.HKEY_CLASSES_ROOT, '.txt', windef.KEY_ACCESS.KEY_ALL_ACCESS);
key.setValue('test_value_name', windef.REG_VALUE_TYPE.REG_SZ, 'test_value');

Getting a Value From a Key

To get a value from a key, just call Key.getValue:

var value = key.getValue('test_value_name');

The return value depends on the type of the key (REG_SZ for example will give you a string).

Creating File Associations

To create a file association, you can call the fileAssociation.associateExeForFile api, which will make windows assign a default program for an arbitrary file extension:

var utils = require('windows-registry').utils;
utils.associateExeForFile('myTestHandler', 'A test handler for unit tests', 'C:\path\to\icon', 'C:\Program Files\nodejs\node.exe %1', '.zzz');

After running the code above, you will see files with the extension of .zzz will be automatically associated with the Node program and their file icon will be changed to the Node file icon.

File Association

Launching Process as an Admin

To launch a process as an Administrator, you can call the utils.elevate api, which will launch a process as an Administrator causing the UAC (User Account Control) elevation prompt to appear if required. This is similar to the Windows Explorer command “Run as administrator”. Pass in FILEPATH to the process you want to elevate. Pass in any PARAMETERS to run with the process. Since this is an asynchronous call, pass in a callback to handle user’s selection.

var utils = require('windows-registry').utils;
utils.elevate('C:\Program Files\nodejs\node.exe', 'index.js', function (err, result){console.log(result);});

Opportunities for Reuse

With NPM module for Windows Registry, developers can now read and write keys to Windows registry, create file associations to make Windows assign default programs to a file extension, and elevate processes to run as an administrator for any Node application running on Windows. With the source code of this NPM module in GitHub, our solution also serves as an example for how to interact with native Windows APIs in Node.js.

0 comments

Discussion is closed.

Feedback usabilla icon