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< 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) => ` ${application} ${issuer} http://schemas.xmlsoap.org/ws/2005/02/trust/Validate `; 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, };