XWBSSOi-js/xwbssoi.js

205 lines
12 KiB
JavaScript
Raw Normal View History

2025-02-04 22:18:58 -05:00
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,
};