From aeea37d8000d28245aee042092b4582f70bae7f4 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 8 Aug 2024 14:21:26 +0200 Subject: [PATCH] feat: create async version of API --- index.d.ts | 2 + index.js | 104 ++++++++++++++++++++++++++++++++++++++----------- src/binding.cc | 83 +++++++++++++++++++++++++++++++++++++-- test.js | 84 +++++++++++++++++++++++---------------- 4 files changed, 215 insertions(+), 58 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1cb1088..f3d4c2c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -32,8 +32,10 @@ declare function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult declare namespace exportCertificateAndPrivateKey { function exportCertificateAndPrivateKey(input: LookupOptions): PfxResult; + function exportCertificateAndPrivateKeyAsync(input: LookupOptions): Promise; function exportSystemCertificates(input: StoreOptions): string[]; + function exportSystemCertificatesAsync(input: StoreOptions): Promise; } export = exportCertificateAndPrivateKey; \ No newline at end of file diff --git a/index.js b/index.js index 3e5c487..1bee531 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,14 @@ 'use strict'; const { exportCertificateAndKey, + exportCertificateAndKeyAsync, exportAllCertificates, + exportAllCertificatesAsync, storeTypes } = require('bindings')('win_export_cert'); const { randomBytes, X509Certificate } = require('crypto'); const util = require('util'); +const { promisify } = util; const DEFAULT_STORE_TYPE_LIST = ['CERT_SYSTEM_STORE_LOCAL_MACHINE', 'CERT_SYSTEM_STORE_CURRENT_USER']; @@ -19,24 +22,56 @@ function validateStoreTypeList(storeTypeList) { return storeTypeList.map(st => typeof st === 'number' ? st : storeTypes[st]); } -function exportSystemCertificates(opts = {}) { - let { - store, - storeTypeList - } = opts; +function addExportedCertificatesToSet(set, list) { + for (const cert of list) { + // X509Certificate was added in Node.js 15 and accepts DER as input, but .toString() returns PEM + set.add(new X509Certificate(cert).toString()); + } +} + +function exportSystemCertificates({ + store, + storeTypeList +} = {}) { storeTypeList = validateStoreTypeList(storeTypeList); const result = new Set(); for (const storeType of storeTypeList) { - for (const cert of exportAllCertificates(store || 'ROOT', storeType)) { - // X509Certificate was added in Node.js 15 and accepts DER as input, but .toString() returns PEM - result.add(new X509Certificate(cert).toString()); - } + addExportedCertificatesToSet(result, exportAllCertificates(store || 'ROOT', storeType)); + } + + return [...result]; +} + +async function exportSystemCertificatesAsync({ + store, + storeTypeList +} = {}) { + storeTypeList = validateStoreTypeList(storeTypeList); + + const result = new Set(); + for (const storeType of storeTypeList) { + addExportedCertificatesToSet(result, await promisify(exportAllCertificatesAsync)(store || 'ROOT', storeType)); } return [...result]; } +function validateSubjectAndThumbprint(subject, thumbprint) { + if (!subject && !thumbprint) { + throw new Error('Need to specify either `subject` or `thumbprint`'); + } + if (subject && thumbprint) { + throw new Error('Cannot specify both `subject` and `thumbprint`'); + } + if (subject && typeof subject !== 'string') { + throw new Error('`subject` needs to be a string'); + } + if (thumbprint && !util.types.isUint8Array(thumbprint)) { + throw new Error('`thumbprint` needs to be a Uint8Array'); + } +} + function exportCertificateAndPrivateKey(opts = {}) { let { subject, @@ -59,18 +94,7 @@ function exportCertificateAndPrivateKey(opts = {}) { throw err; } - if (!subject && !thumbprint) { - throw new Error('Need to specify either `subject` or `thumbprint`'); - } - if (subject && thumbprint) { - throw new Error('Cannot specify both `subject` and `thumbprint`'); - } - if (subject && typeof subject !== 'string') { - throw new Error('`subject` needs to be a string'); - } - if (thumbprint && !util.types.isUint8Array(thumbprint)) { - throw new Error('`thumbprint` needs to be a Uint8Array'); - } + validateSubjectAndThumbprint(subject, thumbprint); requirePrivKey = requirePrivKey !== false; const passphrase = randomBytes(12).toString('hex'); const pfx = exportCertificateAndKey( @@ -82,6 +106,42 @@ function exportCertificateAndPrivateKey(opts = {}) { return { passphrase, pfx }; } +async function exportCertificateAndPrivateKeyAsync(opts = {}) { + let { + subject, + thumbprint, + store, + storeTypeList, + requirePrivKey + } = opts; + storeTypeList = validateStoreTypeList(storeTypeList); + + if (storeTypeList.length !== 1) { + let err; + for (const storeType of storeTypeList) { + try { + return await exportCertificateAndPrivateKeyAsync({ ...opts, storeTypeList: [storeType] }); + } catch(err_) { + err = err_; + } + } + throw err; + } + + validateSubjectAndThumbprint(subject, thumbprint); + requirePrivKey = requirePrivKey !== false; + const passphrase = (await promisify(randomBytes)(12)).toString('hex'); + const pfx = await promisify(exportCertificateAndKeyAsync)( + passphrase, + store || 'MY', + storeTypeList[0], + subject ? { subject } : { thumbprint }, + requirePrivKey); + return { passphrase, pfx }; +} + module.exports = exportCertificateAndPrivateKey; module.exports.exportCertificateAndPrivateKey = exportCertificateAndPrivateKey; -module.exports.exportSystemCertificates = exportSystemCertificates; \ No newline at end of file +module.exports.exportCertificateAndPrivateKeyAsync = exportCertificateAndPrivateKeyAsync; +module.exports.exportSystemCertificates = exportSystemCertificates; +module.exports.exportSystemCertificatesAsync = exportSystemCertificatesAsync; \ No newline at end of file diff --git a/src/binding.cc b/src/binding.cc index ec621f8..1d560c5 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -1,6 +1,8 @@ #include "certs.h" #include +#define PACKAGE "win-export-certificate-and-key" + namespace { using namespace Napi; using namespace WinExportCertificateAndKey; @@ -36,9 +38,7 @@ Value ExportAllCertificatesSync(const CallbackInfo& args) { } } -// Export a given certificate from a system certificate store, -// identified either by its thumbprint or its subject line. -Value ExportCertificateAndKeySync(const CallbackInfo& args) { +ExportCertificateAndKeyArgs GatherExportCertificateAndKeyArgs(const CallbackInfo& args) { ExportCertificateAndKeyArgs exp_args; exp_args.password_buf = MultiByteToWideChar(args[0].ToString()); exp_args.sys_store_name = MultiByteToWideChar(args[1].ToString()); @@ -56,6 +56,13 @@ Value ExportCertificateAndKeySync(const CallbackInfo& args) { } else { throw Error::New(args.Env(), "Need to specify either `thumbprint` or `subject`"); } + return exp_args; +} + +// Export a given certificate from a system certificate store, +// identified either by its thumbprint or its subject line. +Value ExportCertificateAndKeySync(const CallbackInfo& args) { + ExportCertificateAndKeyArgs exp_args = GatherExportCertificateAndKeyArgs(args); try { auto result = ExportCertificateAndKey(exp_args); return Buffer::Copy(args.Env(), result.data(), result.size()); @@ -64,11 +71,81 @@ Value ExportCertificateAndKeySync(const CallbackInfo& args) { } } +Value ExportAllCertificatesAsync(const CallbackInfo& args) { + class Worker final : public AsyncWorker { + public: + Worker(Function callback, std::wstring&& sys_store_name, DWORD store_type) + : AsyncWorker(callback, PACKAGE ":ExportAllCertificates"), + sys_store_name(std::move(sys_store_name)), store_type(store_type) {} + ~Worker() {} + + void Execute() override { + results = ExportAllCertificates(sys_store_name, store_type); + } + + void OnOK() override { + try { + Callback().Call({Env().Null(), BufferListToArray(Env(), results)}); + } catch (const std::exception& e) { + throw Error::New(Env(), e.what()); + } + } + + private: + std::vector> results; + std::wstring sys_store_name; + DWORD store_type; + }; + + Worker* worker = new Worker( + args[2].As(), + MultiByteToWideChar(args[0].ToString()), + args[1].ToNumber().Uint32Value()); + worker->Queue(); + return args.Env().Undefined(); +} + +Value ExportCertificateAndKeyAsync(const CallbackInfo& args) { + ExportCertificateAndKeyArgs exp_args = GatherExportCertificateAndKeyArgs(args); + + class Worker final : public AsyncWorker { + public: + Worker(Function callback, ExportCertificateAndKeyArgs&& exp_args) + : AsyncWorker(callback, PACKAGE ":ExportCertificateAndKey"), + exp_args(std::move(exp_args)) {} + ~Worker() {} + + void Execute() override { + result = ExportCertificateAndKey(exp_args); + } + + void OnOK() override { + try { + Callback().Call({Env().Null(), Buffer::Copy(Env(), result.data(), result.size())}); + } catch (const std::exception& e) { + throw Error::New(Env(), e.what()); + } + } + + private: + std::vector result; + ExportCertificateAndKeyArgs exp_args; + }; + + Worker* worker = new Worker( + args[5].As(), + std::move(exp_args)); + worker->Queue(); + return args.Env().Undefined(); +} + } static Object InitWinExportCertAndKey(Env env, Object exports) { exports["exportCertificateAndKey"] = Function::New(env, ExportCertificateAndKeySync); exports["exportAllCertificates"] = Function::New(env, ExportAllCertificatesSync); + exports["exportCertificateAndKeyAsync"] = Function::New(env, ExportCertificateAndKeyAsync); + exports["exportAllCertificatesAsync"] = Function::New(env, ExportAllCertificatesAsync); Object storeTypes = Object::New(env); storeTypes["CERT_SYSTEM_STORE_CURRENT_SERVICE"] = Number::New(env, CERT_SYSTEM_STORE_CURRENT_SERVICE); storeTypes["CERT_SYSTEM_STORE_CURRENT_USER"] = Number::New(env, CERT_SYSTEM_STORE_CURRENT_USER); diff --git a/test.js b/test.js index 28e720f..adf030d 100644 --- a/test.js +++ b/test.js @@ -4,7 +4,9 @@ const fs = require('fs'); const assert = require('assert'); const { exportCertificateAndPrivateKey, - exportSystemCertificates + exportCertificateAndPrivateKeyAsync, + exportSystemCertificates, + exportSystemCertificatesAsync, } = require('./'); describe('exportCertificateAndPrivateKey', () => { @@ -39,43 +41,59 @@ describe('exportCertificateAndPrivateKey', () => { tlsServer.close(); }); - it('throws when no cert can be found', () => { - assert.throws(() => { - exportCertificateAndPrivateKey({ subject: 'Banana Corp '}); - }, /CertFindCertificateInStore\(\) failed with: Cannot find object or property. \(0x80092004\)/); - }); + for (const method of ['sync', 'async']) { + const fn = { + sync: exportCertificateAndPrivateKey, + async: exportCertificateAndPrivateKeyAsync + }[method]; + context(method, () => { + it('throws when no cert can be found', async() => { + await assert.rejects(async() => { + await fn({ subject: 'Banana Corp '}); + }, /CertFindCertificateInStore\(\) failed with: Cannot find object or property. \(0x80092004\)/); + }); - it('loads a certificate based on its thumbprint', async() => { - const { passphrase, pfx } = exportCertificateAndPrivateKey({ - thumbprint: Buffer.from('d755afda2bbad2509d39eca5968553b9103305af', 'hex') - }); - tls.connect({ ...tlsServerConnectOptions, passphrase, pfx }); - assert.strictEqual(await authorized, true); - }); + it('loads a certificate based on its thumbprint', async() => { + const { passphrase, pfx } = await fn({ + thumbprint: Buffer.from('d755afda2bbad2509d39eca5968553b9103305af', 'hex') + }); + tls.connect({ ...tlsServerConnectOptions, passphrase, pfx }); + assert.strictEqual(await authorized, true); + }); - it('loads a certificate based on its subject', async() => { - const { passphrase, pfx } = exportCertificateAndPrivateKey({ - subject: 'Internet Widgits Pty Ltd' + it('loads a certificate based on its subject', async() => { + const { passphrase, pfx } = await fn({ + subject: 'Internet Widgits Pty Ltd' + }); + tls.connect({ ...tlsServerConnectOptions, passphrase, pfx }); + assert.strictEqual(await authorized, true); + }); }); - tls.connect({ ...tlsServerConnectOptions, passphrase, pfx }); - assert.strictEqual(await authorized, true); - }); + } }); describe('exportSystemCertificates', () => { - it('exports certificates from the ROOT store as .pem', () => { - const certs = exportSystemCertificates({ store: 'ROOT' }); - assert.notStrictEqual(certs.length, 0); - for (const cert of certs) { - assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/); - } - }); + for (const method of ['sync', 'async']) { + const fn = { + sync: exportSystemCertificates, + async: exportSystemCertificatesAsync + }[method]; + context(method, () => { + it('exports certificates from the ROOT store as .pem', async() => { + const certs = await fn({ store: 'ROOT' }); + assert.notStrictEqual(certs.length, 0); + for (const cert of certs) { + assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/); + } + }); - it('exports certificates from the CA store as .pem', () => { - const certs = exportSystemCertificates({ store: 'CA' }); - assert.notStrictEqual(certs.length, 0); - for (const cert of certs) { - assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/); - } - }); + it('exports certificates from the CA store as .pem', async() => { + const certs = await fn({ store: 'CA' }); + assert.notStrictEqual(certs.length, 0); + for (const cert of certs) { + assert.match(cert.trim(), /^-----BEGIN CERTIFICATE-----[\s\S]+-----END CERTIFICATE-----$/); + } + }); + }); + } });