205 lines
12 KiB
JavaScript
205 lines
12 KiB
JavaScript
const process = require('node:process');
|
|
const child_process = require('node:child_process');
|
|
const path = require('node:path');
|
|
const koffi = require('koffi');
|
|
|
|
const DEFAULT_USER_AGENT = 'Borland SOAP 1.2';
|
|
const DEFAULT_ISSUER = 'https://ssoi.sts.va.gov/Issuer/smtoken/SAML2';
|
|
|
|
const BOOL = koffi.alias('BOOL', koffi.types.int);
|
|
const BYTE = koffi.alias('BYTE', koffi.types.uchar);
|
|
const LPBYTE = koffi.pointer('LPBYTE', BYTE);
|
|
const DWORD = koffi.alias('DWORD', koffi.types.uint32);
|
|
const LPDWORD = koffi.pointer('LPDWORD', DWORD);
|
|
const LONG = koffi.alias('LONG', koffi.types.long);
|
|
const LPVOID = koffi.pointer('LPVOID', koffi.types.void);
|
|
const LPCVOID = koffi.pointer('LPCVOID', koffi.types.void); // const void *
|
|
const LPWSTR = koffi.pointer('LPWSTR', koffi.types.wchar);
|
|
const LPCWSTR = koffi.pointer('LPCWSTR', koffi.types.wchar); // const wchar_t *
|
|
const LPCSTR = koffi.pointer('LPCSTR', koffi.types.char); // const char *
|
|
const LPFILETIME = koffi.pointer('LPFILETIME', koffi.opaque());
|
|
const HWND = koffi.pointer('HWND', koffi.opaque());
|
|
const HKEY = koffi.pointer('HKEY', koffi.opaque());
|
|
const HCERTSTORE = koffi.pointer('HCERTSTORE', koffi.opaque());
|
|
const PCERT_INFO = koffi.pointer('PCERT_INFO', koffi.opaque());
|
|
const HCRYPTPROV_LEGACY = koffi.pointer('HCRYPTPROV_LEGACY', koffi.opaque());
|
|
const COMPUTER_NAME_FORMAT = koffi.alias('COMPUTER_NAME_FORMAT', koffi.types.int32);
|
|
|
|
const CERT_CONTEXT = koffi.struct('CERT_CONTEXT', {
|
|
dwCertEncodingType: DWORD,
|
|
pbCertEncoded: LPBYTE,
|
|
cbCertEncoded: DWORD,
|
|
pCertInfo: PCERT_INFO,
|
|
hCertStore: HCERTSTORE,
|
|
});
|
|
const PCCERT_CONTEXT = koffi.pointer('PCCERT_CONTEXT', CERT_CONTEXT); // const CERT_CONTEXT *
|
|
const PPCCERT_CONTEXT = koffi.pointer('PPCCERT_CONTEXT', PCCERT_CONTEXT);
|
|
|
|
const CERT_STORE_PROV_MEMORY = 'Memory';
|
|
const X509_ASN_ENCODING = 0x00000001;
|
|
const PKCS_7_ASN_ENCODING = 0x00010000;
|
|
const CERT_COMPARE_ANY = 0;
|
|
const CERT_COMPARE_SHIFT = 16;
|
|
const CERT_FIND_ANY = CERT_COMPARE_ANY<<CERT_COMPARE_SHIFT;
|
|
const CERT_NAME_FRIENDLY_DISPLAY_TYPE = 5;
|
|
const CERT_DIGITAL_SIGNATURE_KEY_USAGE = 0x80;
|
|
const CERT_STORE_ADD_ALWAYS = 4;
|
|
const CERT_HASH_PROP_ID = CERT_SHA1_HASH_PROP_ID = 3;
|
|
const HKEY_LOCAL_MACHINE = 0x80000002;
|
|
const RRF_RT_REG_SZ = 0x00000002;
|
|
const COMPUTER_NAME_DNS_FULLY_QUALIFIED = 3;
|
|
|
|
const crypt32 = koffi.load('crypt32');
|
|
const CertOpenStore = crypt32.func('__stdcall', 'CertOpenStore', HCERTSTORE, [LPCSTR, DWORD, HCRYPTPROV_LEGACY, DWORD, LPCVOID]);
|
|
const CertOpenSystemStoreW = crypt32.func('__stdcall', 'CertOpenSystemStoreW', HCERTSTORE, [HCRYPTPROV_LEGACY, LPCWSTR]);
|
|
const CertCloseStore = crypt32.func('__stdcall', 'CertCloseStore', BOOL, [HCERTSTORE, DWORD]);
|
|
const CertEnumCertificatesInStore = crypt32.func('__stdcall', 'CertEnumCertificatesInStore', PCCERT_CONTEXT, [HCERTSTORE, PCCERT_CONTEXT]);
|
|
const CertFindCertificateInStore = crypt32.func('__stdcall', 'CertFindCertificateInStore', PCCERT_CONTEXT, [HCERTSTORE, DWORD, DWORD, DWORD, LPCVOID, PCCERT_CONTEXT]);
|
|
const CertAddCertificateContextToStore = crypt32.func('__stdcall', 'CertAddCertificateContextToStore', BOOL, [HCERTSTORE, PCCERT_CONTEXT, DWORD, koffi.out(PPCCERT_CONTEXT)]);
|
|
const CertGetNameStringW = crypt32.func('__stdcall', 'CertGetNameStringW', DWORD, [PCCERT_CONTEXT, DWORD, DWORD, LPVOID, koffi.out(LPWSTR), DWORD]);
|
|
const CertGetIntendedKeyUsage = crypt32.func('__stdcall', 'CertGetIntendedKeyUsage', BOOL, [DWORD, PCERT_INFO, koffi.out(LPBYTE), DWORD]);
|
|
const CertGetCertificateContextProperty = crypt32.func('__stdcall', 'CertGetCertificateContextProperty', BOOL, [PCCERT_CONTEXT, DWORD, koffi.out(LPVOID), koffi.inout(LPDWORD)]);
|
|
const CertVerifyTimeValidity = crypt32.func('__stdcall', 'CertVerifyTimeValidity', LONG, [LPFILETIME, PCERT_INFO]);
|
|
|
|
const cryptui = koffi.load('cryptui');
|
|
const CryptUIDlgSelectCertificateFromStore = cryptui.func('__stdcall', 'CryptUIDlgSelectCertificateFromStore', PCCERT_CONTEXT, [HCERTSTORE, HWND, LPCWSTR, LPCWSTR, DWORD, DWORD, LPCVOID]);
|
|
|
|
const advapi32 = koffi.load('advapi32');
|
|
const RegGetValueW = advapi32.func('__stdcall', 'RegGetValueW', LONG, [HKEY, LPCWSTR, LPCWSTR, DWORD, koffi.out(LPDWORD), koffi.out(LPVOID), koffi.inout(LPDWORD)]);
|
|
|
|
const kernel32 = koffi.load('kernel32');
|
|
const GetConsoleWindow = kernel32.func('__stdcall', 'GetConsoleWindow', HWND, []);
|
|
const GetComputerNameExW = kernel32.func('__stdcall', 'GetComputerNameExW', BOOL, [COMPUTER_NAME_FORMAT, koffi.out(LPWSTR), koffi.inout(LPDWORD)]);
|
|
|
|
/**
|
|
* WS-Trust STS endpoints from VDL documentation
|
|
* https://www.va.gov/vdl/documents/Infrastructure/KAAJEE/kaajee_ssowap_8_791_depg_r.pdf
|
|
* https://www.va.gov/vdl/documents/Financial_Admin/Bed_Management_Solution_(BMS)/bms_4_0_tm.pdf
|
|
* https://www.va.gov/vdl/documents/VistA_GUI_Hybrids/National_Utilization_Management_Integration_Archive/numi_server_setup_guide_v15_9.pdf
|
|
*/
|
|
const get_registry_iam = () => get_registry_value(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\IAM') || 'https://services.eauth.va.gov:9301/STS/RequestSecurityToken';
|
|
const get_registry_iam_ad = () => get_registry_value(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\IAM_AD') || 'https://services.eauth.va.gov:9201/STS/RequestSecurityToken';
|
|
const get_registry_rioserver = () => get_registry_value(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\RIOSERVER') || 'SecurityTokenService';
|
|
const get_registry_rioport = () => get_registry_value(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Vista\\Common\\RIOPORT') || 'RequestSecurityToken';
|
|
function get_registry_value(hkey, subkey, value=null) {
|
|
const ref_bufsz = [0];
|
|
RegGetValueW(hkey, subkey, value, RRF_RT_REG_SZ, null, null, ref_bufsz);
|
|
const buffer = Buffer.alloc(koffi.sizeof('wchar_t')*ref_bufsz[0]);
|
|
RegGetValueW(hkey, subkey, value, RRF_RT_REG_SZ, null, buffer, ref_bufsz);
|
|
return koffi.decode(buffer, 'wchar_t', ref_bufsz[0]);
|
|
}
|
|
|
|
const get_iam_request = (application, issuer) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
|
|
<soapenv:Header/>
|
|
<soapenv:Body>
|
|
<ns:RequestSecurityToken>
|
|
<ns:Base>
|
|
<wss:TLS xmlns:wss="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>
|
|
</ns:Base>
|
|
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
|
|
<wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">
|
|
<wsa:Address>${application}</wsa:Address>
|
|
</wsa:EndpointReference>
|
|
</wsp:AppliesTo>
|
|
<ns:Issuer>
|
|
<wsa:Address xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">${issuer}</wsa:Address>
|
|
</ns:Issuer>
|
|
<ns:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Validate</ns:RequestType>
|
|
</ns:RequestSecurityToken>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>`;
|
|
|
|
function get_local_computer_name() {
|
|
const ref_bufsz = [0];
|
|
GetComputerNameExW(COMPUTER_NAME_DNS_FULLY_QUALIFIED, null, ref_bufsz);
|
|
const buffer = Buffer.alloc(koffi.sizeof('wchar_t')*ref_bufsz[0]);
|
|
GetComputerNameExW(COMPUTER_NAME_DNS_FULLY_QUALIFIED, buffer, ref_bufsz);
|
|
return koffi.decode(buffer, 'wchar_t', ref_bufsz[0]);
|
|
}
|
|
|
|
function get_app_name() {
|
|
const argv0 = path.basename(process.argv[0]);
|
|
return (process.argv.length <= 1) || (argv0.substring(0, 4) != 'node') ? argv0 : path.basename(process.argv[1]);
|
|
}
|
|
|
|
function get_vista_certificate(show_cert_dialog=true, hwnd=null) {
|
|
store_system = CertOpenSystemStoreW(null, 'MY');
|
|
try {
|
|
store_memory = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, null, 0, null);
|
|
try {
|
|
let cert_selection, cert_iter, cert_valid;
|
|
while(cert_iter = CertEnumCertificatesInStore(store_system, cert_iter)) if(cert_valid = CertFindCertificateInStore(store_system, X509_ASN_ENCODING|PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, null, null)) {
|
|
const name_bufsz = CertGetNameStringW(cert_iter, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, null, null, 0);
|
|
name_buffer = Buffer.alloc(koffi.sizeof('wchar_t')*name_bufsz);
|
|
CertGetNameStringW(cert_iter, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, null, name_buffer, name_bufsz);
|
|
name_string = koffi.decode(name_buffer, 'wchar_t', name_bufsz);
|
|
const ref_key_usage_bits = [0];
|
|
const pCertInfo = koffi.decode(cert_iter, 'CERT_CONTEXT').pCertInfo;
|
|
CertGetIntendedKeyUsage(X509_ASN_ENCODING|PKCS_7_ASN_ENCODING, pCertInfo, ref_key_usage_bits, koffi.sizeof(BYTE));
|
|
const valid_date = CertVerifyTimeValidity(null, pCertInfo);
|
|
if(((ref_key_usage_bits[0]&CERT_DIGITAL_SIGNATURE_KEY_USAGE) == CERT_DIGITAL_SIGNATURE_KEY_USAGE) && (valid_date == 0) && (name_string.indexOf('Card Authentication') < 0) && (name_string.indexOf('0,') < 0) && (name_string.indexOf('Signature') < 0)) {
|
|
CertAddCertificateContextToStore(store_memory, cert_iter, CERT_STORE_ADD_ALWAYS, [cert_valid]);
|
|
cert_selection = cert_valid;
|
|
}
|
|
}
|
|
return show_cert_dialog ? CryptUIDlgSelectCertificateFromStore(store_memory, hwnd || GetConsoleWindow(), 'VistA Logon - Certificate Selection', 'Select a certificate for VistA authentication', 0, 0, null) : cert_selection;
|
|
} finally {
|
|
CertCloseStore(store_memory, 0);
|
|
}
|
|
} finally {
|
|
CertCloseStore(store_system, 0);
|
|
}
|
|
}
|
|
|
|
function get_certificate_thumbprint(certificate) {
|
|
if(certificate) {
|
|
const ref_bufsz = [0];
|
|
CertGetCertificateContextProperty(certificate, CERT_HASH_PROP_ID, null, ref_bufsz);
|
|
const buffer = Buffer.alloc(ref_bufsz[0]);
|
|
CertGetCertificateContextProperty(certificate, CERT_HASH_PROP_ID, buffer, ref_bufsz);
|
|
return buffer.subarray(0, ref_bufsz[0]).toString('hex');
|
|
} else throw new TypeError('Cannot access null certificate');
|
|
}
|
|
|
|
function get_certificate_friendly_display_name(certificate) {
|
|
if(certificate) {
|
|
const bufsz = CertGetNameStringW(certificate, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, null, null, 0);
|
|
const buffer = Buffer.alloc(koffi.sizeof('wchar_t')*bufsz);
|
|
CertGetNameStringW(certificate, CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, null, buffer, bufsz);
|
|
return koffi.decode(buffer, 'wchar_t', bufsz);
|
|
} else throw new TypeError('Cannot access null certificate');
|
|
}
|
|
|
|
function get_sso_token({ iam, ua, certificate, issuer, hostname, application }={}) {
|
|
let ptr_cert;
|
|
if((certificate) || ((ptr_cert = get_vista_certificate()) && (certificate = get_certificate_thumbprint(ptr_cert)))) {
|
|
const res = child_process.spawnSync('curl', ['-fsSL', '-X', 'POST', iam || get_registry_iam(), '--ca-native', '--cert', 'CurrentUser\\MY\\' + certificate, '-A', ua || DEFAULT_USER_AGENT, '-H', 'Content-Type: application/xml', '-H', 'Accept: application/xml', '-d', get_iam_request(`https://${hostname || get_local_computer_name()}/Delphi_RPC_Broker/${application || get_app_name()}`, issuer || DEFAULT_ISSUER)]);
|
|
if(res.stderr.length > 0) process.stderr.write(res.stderr);
|
|
if(res.status === 0) return res.stdout.toString('utf-8');
|
|
}
|
|
}
|
|
|
|
function get_sso_token_async({ iam, ua, certificate, issuer, hostname, application }={}) {
|
|
let ptr_cert;
|
|
if((certificate) || ((ptr_cert = get_vista_certificate()) && (certificate = get_certificate_thumbprint(ptr_cert)))) {
|
|
const child = child_process.spawn('curl', ['-fsSL', '-X', 'POST', iam || get_registry_iam(), '--ca-native', '--cert', 'CurrentUser\\MY\\' + certificate, '-A', ua || DEFAULT_USER_AGENT, '-H', 'Content-Type: application/xml', '-H', 'Accept: application/xml', '-d', get_iam_request(`https://${hostname || get_local_computer_name()}/Delphi_RPC_Broker/${application || get_app_name()}`, issuer || DEFAULT_ISSUER)]), res = [];
|
|
child.stdout.on('data', function(chunk) { if(chunk) res.push(chunk); });
|
|
child.stderr.on('data', function(chunk) { if(chunk) process.stderr.write(res.stderr); });
|
|
return new Promise(function(resolve, reject) {
|
|
child.on('close', function(code) {
|
|
if(code === 0) resolve(res.join(''));
|
|
else reject(code);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
get_vista_certificate,
|
|
get_certificate_thumbprint,
|
|
get_certificate_friendly_display_name,
|
|
get_sso_token,
|
|
get_sso_token_async,
|
|
};
|