This package has been deprecated

Author message:

This package has been deprecated in favour of the passport-desktop package

node-ms-passport
TypeScript icon, indicating that this package has built-in type declarations

1.2.9 • Public • Published

node-ms-passport

Microsoft Passport and Credential storage for Node.js. Only works on Windows. Obviously. Uses C# and C++ to store credentials and sign data. Typescript definitions are available.

This addon is only intended to be used with client-only applications, e.g. electron.

Another important note: As of version 1.2.0, when installed and imported on a non-windows machine, that process will no longer throw errors. Instead, if any of the modules methods/constructors are called an Error will be thrown.

Installation

npm install node-ms-passport

The native library is built using cmake-js, so you should set the runtime to electron in your package.json if you are planning on using this addon with electron:

{
  "cmake-js": {
    "runtime": "electron",
    "runtimeVersion": "electron-runtime-version-here",
    "arch": "whatever-setting-is-appropriate-for-your-application's-windows-build"
  }
}

Build requirements

Visual Studio 2017 or 2019 with installed packages:

  • .NET SDK v4.7.2 or later
  • C++ build tools
  • CMake
  • Windows 10 SDK

or with installed workloads:

  • .NET desktop development
  • Desktop development with C++

Usage

Check if the module is available

If you want to check if the module is available on your type of system, call PassportModule.available(): boolean. If this returns true every method from the module is available and can be used as described below. If this method returns false, any call to any method will throw an Error because the native module could not be found as the current system type is not supported.

Importing the module on an unsupported system will not throw an error, only calling methods will. But importing the module will throw an error if the native module could not be found and the type of system is actually supported.

import {PassportModule} from "node-ms-passport";

if (PassportModule.available()) {
    // The module is available and can be used
} else {
    // The module is not avalable and should not be called
}

Electron asar packaging fix

There is still a bug with electron-packager when the application gets packaged into an asar package: Although the module gets unpacked and stored next to the app.asar file in a directory called app.asar.unpacked, the packaged app (for some reason) chooses to load the module from within the app.asar file, which causes any Passport operation to fail as the C# DLL cannot be loaded from within an archive (no DLL can be loaded from within an archive, that's why app.asar.unpacked exists). To fix this, PassportModule.electronAsarFix() was created, which is basically an alias for:

const {PassportModule} = require('node-ms-passport');

if (PassportModule.available() &&
    PassportModule.csModuleLocation.includes("\\app.asar\\")) {
    PassportModule.csModuleLocation = PassportModule.csModuleLocation
        .replace("\\app.asar\\", "\\app.asar.unpacked\\");
}

Use it like this:

const {PassportModule} = require('node-ms-passport');

PassportModule.electronAsarFix();

Passport

To use it, simply define

const {Passport} = require('node-ms-passport');

static passport.passportAvailable(): boolean

Check if Microsoft passport is available on this system:

if (Passport.passportAvailable()) {
    // MS Passport is available
} else {
    // MS Passport is not available
}

static passportAccountExists(accountId: string): boolean

Check if a passport account exists:

if (Passport.passportAccountExists("SOME_ID")) {
    // The passport account with the id exists
} else {
    // The account does not exist
}

static verifySignatureHex(challenge: string, signature: string, publicKey: string): Promise<boolean>

Verify a signature (all arguments as hex-encoded strings):

const matches = await Passport.verifySignatureHex(CHALLENGE, SIGNATURE, PUBLICKEY);

static verifySignature(challenge: Buffer, signature: Buffer, publicKey: Buffer): Promise<boolean>

Verify a signature:

const matches = await Passport.verifySignature(CHALLENGE, SIGNATURE, PUBLICKEY);

static requestVerification(message: string): Promise<VerificationResult>

Request the user to verify themselves. Returns a VerificationResult, which is defined as

enum VerificationResult {
    Verified = 0,
    DeviceNotPresent = 1,
    NotConfiguredForUser = 2,
    DisabledByPolicy = 3,
    DeviceBusy = 4,
    RetriesExhausted = 5,
    Canceled = 6
}

Additionally, a message to be shown to the user must be passed.

const result = await Passport.requestVerification("Verify your identity");
if (result == VerificationResult.Verified) {
    // Successfully verified
} else {
    // Verification failed
}

new passport(accountId: string)

Create a new instance of the passport class

const pass = new Passport("SOME_ID");

If the account already exists, passport.accountExists will be set to true.

passport.createPassportKey(): Promise<void>

Create a passport account and generate a public key:

await pass.createPassportKey();

passportSignHex(challenge: string): Promise<string>

Sign a challenge with the account's private key. Returns the signature as a hex string.

const signature = await pass.passportSignHex("SOME_CHALLENGE");

passportSign(challenge: Buffer): Promise<Buffer>

Sign a challenge with the account's private key. Returns the signature bytes in a Buffer.

const signature = await pass.passportSign(Buffer.from("SOME_CHALLENGE"));

getPublicKeyHex(encoding?: PublicKeyEncoding | string | null): Promise<string>

Get the account's public key as a hex string. If you want to get the public key in a specific encoding, you can pass a PublicKeyEncoding as the encoding parameter. Pass null or nothing to get the public key in the default encoding. PublicKeyEncoding is defined as:

enum PublicKeyEncoding {
    X509SubjectPublicKeyInfo= "X509SubjectPublicKeyInfo",
    Pkcs1RsaPublicKey = "Pkcs1RsaPublicKey",
    BCryptPublicKey = "BCryptPublicKey",
    Capi1PublicKey = "Capi1PublicKey",
    BCryptEccFullPublicKey = "BCryptEccFullPublicKey"
}

Get the public key:

const pubkey = await pass.getPublicKeyHex();

getPublicKey(encoding?: PublicKeyEncoding | string | null): Promise<Buffer>

Get the account's public key bytes in a Buffer:

const pubkey = await pass.getPublicKey();

getPublicKeyHashHex(): Promise<string>

Get the SHA256 Hash of the public key as a hex string:

const hash = await pass.getPublicKeyHashHex();

getPublicKeyHash(): Promise<string>

Get the SHA256 Hash bytes of the public key in a Buffer:

const hash = await pass.getPublicKeyHash();

deletePassportAccount(): Promise<void>

Delete the passport account:

await pass.deletePassportAccount();

Exceptions

Almost every passport operation can throw an instance of PassportError. Every instance of PassportError stores an error code, which is one of PassportErrorCode:

enum PassportErrorCode {
    // An exception was thrown by the native
    // addon of which the error code is unknown
    ERR_ANY = -1,
    // An unknown error occurred
    ERR_UNKNOWN = 1,
    // The user needs to create a pin
    ERR_MISSING_PIN = 2,
    // The user cancelled the operation
    ERR_USER_CANCELLED = 3,
    // The user prefers a password
    ERR_USER_PREFERS_PASSWORD = 4,
    // The passport account was not found
    ERR_ACCOUNT_NOT_FOUND = 5,
    // The sign operation failed
    ERR_SIGN_OP_FAILED = 6,
    // The key is already deleted
    ERR_KEY_ALREADY_DELETED = 7,
    // The access was denied
    ERR_ACCESS_DENIED = 8
}

Credential vault

It also supports the windows credential vault. Passwords will be encrypted by default. To use it, simply define

const {CredentialStore} = require('node-ms-passport');

new CredentialStore(accountId: string, encryptPasswords: boolean = false)

Create a new CredentialStore instance. Takes an account id and a boolean which controls whether to encrypt the stored password. The second argument determines whether the password should be encrypted before storing it. Note: The key to encrypt and decrypt the password is destroyed when the user logs off, thus the password can't be read after that, so only set this to true if you know what you are doing.

const store = new CredentialStore("some/id");

get accountId(): string

Get the stored account id

get encryptPasswords(): boolean

Check if the passwords will be encrypted

write(user: string, password: string): Promise<void>

Store a username and password. Throws on error. If the credentials already exist, they will be overwritten.

let ok = await store.write("username", "pa$$word");

read(): Promise<Credential>

Read a username and password from the credential vault. Returns a Credential on success storing the username and password. Throws an error if the operation failed.

let cred = await store.read();

remove(): Promise<void>

Remove a username and password from the credential vault. Throws on error. This will cause all subsequent interactions with the credential vault (refreshData() and setEncrypted()) from the Credential class to fail.

await store.remove();

isEncrypted(): Promise<boolean>

Check if the password in the credential vault is encrypted:

let encrypted = store.isEncrypted();

exists(): Promise<boolean>

Check if the account exists:

if (await store.exists()) {
    // Do something like
    let cred = await store.read();
}

static enumerateAccounts(target?: string | null): Promise<Credential[]>

Get all password vault accounts. The target may be a name of a account to search for or end with an asterisk to match all targets with a name same to the name given, so 'user*' will return all accounts starting with 'user'. If the argument is omitted or set to null, all accounts will be retrieved.

On retrieval, all passwords will be checked if they are encrypted and if they are encoded in utf-8 or utf-16. Once that is done, the passwords are converted to utf-16 and encrypted to keep them in the system's memory. On write, the passwords will be written as utf-16 string to the password vault.

// Print all accounts
let accounts: Credential[] = await CredentialVault.enumerateAccounts();
accounts.forEach(e => console.log(e.accountId));

// Print all accounts beginning with 'Windows'
accounts = await CredentialVault.enumerateAccounts("Windows*");
accounts.forEach(e => console.log(e.accountId));

Credential class

Credentials are stored using the Credential class. The password is stored in an encrypted form until loadPassword() is called.

Note: Almost all password operations are synchronized, so calling a async operation and a sync operation, the synchroized operation will possibly wait on the async operation to finish so make sure to not call a operation which may take some time like loadPassword() and a synchronized operation like get password() at once since this may cause your application to freeze.

TL;DR: Always await async operations before calling sync operations on these types of objects.

Get a Credential instance
// Create a new credential store instance
const store = new CredentialStore("some/id", true);

// Read the data. Throws if the data does not exist.
const cred: Credential = await store.read();
get accountId(): string

Get the account id

get username(): string

Get the username

get password(): string | null

Get the password. Returns null if the password is not loaded. In this case you should call loadPassword() to load the password. After that, the plain text password will be returned.

get encrypted(): boolean

Check if the password will be stored in an encrypted form. Does not represent if the password is actually stored in an encrypted form.

get passwordBuffer(): Buffer | null

Get the password in a char16_t buffer. Returns null if the password is not loaded.

get valid(): boolean

Check if this credential instance is valid. If it is, it will work as expected, but if it isn't, any password read-related operations will throw (update, refreshData and isEncrypted will work though). This can only be false if this Credential instance is a result of CredentialStore.enumerateAccounts() as CredentialStore.read() will throw an error if the credential could not be read. The credential counts as invalid if the data is protected (CredIsProtected returned true) but could not be decrypted using CredUnprotect.

loadPassword(): Promise<void>

Load the password to retrieve it later on. If the password is already loaded, this is a no-op.

// Get the password
let password = cred.password;
if (password == null) {
    // The password is not loaded, load it
    await cred.loadPassword();
    // The password is now retrieved
    password = cred.password;
} else {
    // The password is already loaded
}
unloadPassword(): Promise<void>

Unloads the password. The password get operation will return null after this is called. If the password is not loaded, this is a no-op.

refreshData(): Promise<void>

Refresh the cached data with the data actually stored in the password vault.

update(username: string, password: string): Promise<void>

Update the stored username and password. After this call, the password will not be loaded, you must call loadPassword to retrieve it.

await store.update("newUser", "newPassword");
setEncrypted(encrypt: boolean): Promise<void>

Set whether the password should be stored in encrypted form. Does not alter the form in which the password is stored in the memory (RAM). After this call, the password will not be loaded, you must call loadPassword to retrieve it. If the supplied value matches the current value, this is a no-op.

Note: This does not change the encryptPasswords value in the CredentialStore class that was used to create this Credential instance. And again, the key to encrypt and decrypt the password is destroyed when the user logs off so only set this to true if you know what you are doing.

ioEncrypted(): Promise<boolean>

Check if the password is actually encrypted in the credential store.

await store.setEncrypted(true);

let encrypted = await store.isEncrypted();
// encrypted is now true

Encrypt data

Note: The key to encrypt and decrypt the password is destroyed when the user logs off so the any data stored using this will be unreadable after that. Encrypting data using CredProtectW and CredUnprotectW is also supported. To use it, simply define

const {passwords} = require('node-ms-passport');

passwords.encryptHex(data: string): Promise<string>

Encrypt a password. Returns the encrypted password as a hex string.

const encrypted = await passwords.encryptHex("pa$$word");

passwords.encrypt(data: string): Promise<Buffer>

Encrypt a password. Returns the encrypted password bytes in a Buffer.

const encrypted = await passwords.encrypt("pa$$word");

passwords.decryptHex(data: string): Promise<string>

Decrypt a hex-encoded password. Returns the decrypted password string.

const password = await passwords.decryptHex(encrypted);

passwords.decrypt(data: Buffer): Promise<string>

Decrypt password bytes in a Buffer. Returns the decrypted password string.

const password = await passwords.decrypt(encrypted);

passwords.isEncryptedHex(data: string): Promise<boolean>

Check if a hex-encoded password is encrypted:

const is_encrypted = await passwords.isEncryptedHex(encrypted);

passwords.isEncrypted(data: Buffer): Promise<boolean>

Check if password bytes in a Buffer are encrypted:

const is_encrypted = await passwords.isEncrypted(encrypted);

Examples

Passport

import {Passport} from "node-ms-passport";
import * as crypto from "crypto";

// Check if this system supports ms passport
if (!Passport.passportAvailable()) {
    // MS Passport is not available
    return;
}

// Create a new passport instance
const pass = new Passport("SOME_ID");

// If the account does not exist,
// create it
if (!pass.accountExists) {
    await pass.createPassportKey();
}

// Get the public key
let pubkey: Buffer = await pass.getPublicKey();

// Generate a challenge
let challenge: Buffer = crypto.randomBytes(25);

// Sign the challenge
let signature: Buffer = await pass.passportSign(challenge);

// Check if the signature matches
let signature_matches = await Passport.verifySignature(
                                                challenge,
                                                signature,
                                                pubkey);

if (!signature_matches) {
    // The signature does not match
    return;
}

// Delete the passport key
await pass.deletePassportAccount();

Credential vault

import {CredentialStore, Credential} from "node-ms-passport";

// Create a new CredentialStore instance
// with encrypting password
const store = new CedentialStore("test/test", true);

// Check if the credentials are already stored
if (await store.exists()) {
    // Do something with that
}

// Create a new credentialStore instance
// without encryping passwords
const plain_store = new CredentialStore("test/plain", false);

// Write the password. Throws on error.
await store.write("test", "pa$$word");

// Check if the stored password is encrypted
if (!await store.isEncrypted()) {
    // It should be encrypted. This shouldn't happen.
    // If it still does, please open a new issue.
    return;
}

// Read the password. Throws on error.
let cred: Credential = await store.read();

// Get the username
let user: string = res.username;

// The password is not yet loaded,
// call loadPassword() to load it
await cred.loadPassword();

// Get the password
let pass: string = cred.password;

// Do something with the data...

// We are done with the password, unload it.
// Note: As strings are copied, this can also
// be called directly after the password has
// been retrieved, it will not affect any
// already retrieved passwords
await cred.unloadPassword();

// Maybe update the data...
await cred.update("newUser", "newPass");

// Maybe we don't want the credentials
// to be encrypted anymore...
await cred.setEncrypted(false);

// Delete the credential.
await store.remove();

Password encryption

Encrypting and decrypting a password:

import {passwords} from "node-ms-passport";

// Encrypt a password
let data: Buffer;
try {
    data = await passwords.encrypt("TestPassword");
} catch (e) {
    return; // Throws on failure
}

// Check if the data is encrypted
let encrypted: boolean;
try {
    encrypted = await passwords.isEncrypted(data);
} catch (e) {
    return; // Throws error on failure
}

if (!encrypted) { // Should not be called
    console.error("Data should be encrypted, but it is not");
    return;
}

// Trying to call any function which requires a hex string with
// invalid data will throw an error
try {
    await passwords.isEncrypted("data"); // 't' is no valid hex character
} catch (e) {
    console.error(e);
    return;
}

// Decrypt the data
try {
    data = passwords.decrypt(data);
} catch (e) {
    return; // Throws on failure
}

C++ Api

A c++ api is shipped with the addon to be used with custom node.js modules. To get the include path call: node -p "require('node-ms-passport').passport_lib.include_dir", for the library to link to call: node -p "require('node-ms-passport').passport_lib.library".

Your should probably set the location of the C# dll in order for the program to work properly:

nodeMsPassport::passport::setCSharpDllLocation("CSHARP_DLL_LOCATION");

Passport example

#include <NodeMsPassport.hpp>

int main() {
    using namespace nodeMsPassport::passport;

    // Check if passport is available
    if (!passportAvailable()) {
        return 1;    
    }

    // Create passport key, this call will block and wait for user input
    OperationResult result = createPassportKey("test");
    if (!result.ok()) {
        return 1;
    }

    // The public key will be stored in OperationResult::data
    secure_vector<unsigned char> publicKey = result.data;

    std::string challenge_str = "Challenge";
    secure_vector<unsigned char> challenge(challenge_str.begin(),
                                             challenge_str.end());

    // Sign the challenge, this call will block and wait for user input, too
    result = passportSign("test", challenge);
    if (!result.ok()) {
            return 1;
    }

    // Check if the signature matches (Probably on a different machine)
    bool matches = verifySignature(challenge, result.data, publicKey);
    if (!matches) {
        return 2;    
    }

    // Delete the passport account
    int status = deletePassportAccount("test");
    if (status != 0) { // Returns 0 on success, like everything else
        return 1;
    }

    return 0;
}

Credential manager example

Since the c++ api has all the functionality of the node.js api, the credential manager is also available.

#include <NodeMsPassport.hpp>
#include <iostream>

int main() {
    using namespace nodeMsPassport::credentials;

    try {
        // Write a password and encrypt it. Throws on error.
        write(L"test/test", L"testUser", L"testPassword", true);

        // Check if the password is encrypted
        if (!isEncrypted(L"test/test")) {
            // Should never be called,
            // if the encryption fails, write returns false.
            return 2;
        }

        // Read the password, it is encrypted, so decrypt it.
        // If you don't know if the password is encrypted,
        // use isEncrypted() to find out.
        std::wstring user;
        secure_wstring password;
        read(L"test/test", user, password, true);

        // Remove the password from the database
        remove(L"test/test");
    } catch (const std::exception &e) {
        std::cerr << "An error occurred: " << e.what() << std::endl;
        return 1;
    }

    return 0;
}

Data encryption

Data encryption also works as with node.js and uses CredProtectW and CredUnprotectW to protect data.

#include <NodeMsPassport.hpp>
#include <iostream>

int main() {
    using namespace nodeMsPassport::passwords;
    
    secure_wstring password = L"Password";

    try {
        // Encrypt the data
        encrypt(password);

        // Check if the data is encrypted
        if (!isEncrypted(password)) {
            // Should never be called, if the encryption fails,
            // encrypt returns false.
            return 2;
        }

        // Decrypt the data
        decrypt(password);
    } catch (const std::exception &e) {
        std::cerr << "An error occurred: " << e.what() << std::endl;
        return 1;
    }

    // Result should match
    if (password == L"Password") {
        return 0;
    } else {
        return -1;
    }
}

Package Sidebar

Install

npm i node-ms-passport

Weekly Downloads

24

Version

1.2.9

License

MIT

Unpacked Size

188 kB

Total Files

28

Last publish

Collaborators

  • markusjx