UNPKG

ldapjs-type-parsers

Version:

A collection of functions to parse raw buffers from LDAPJS

399 lines (376 loc) 16.7 kB
const attributeTransforms = { objectGUID: parseObjectGUID, objectSid: parseObjectSID, userAccountControl: parseUserAccountControl, sAMAccountType: parseSAMAccountType, certificate: bufferToPEMCertString, userCertificate: bufferToPEMCertString, lastLogonTimestamp: parseIntegerDate, lastLogon: parseIntegerDate, lastLogoff: parseIntegerDate, badPasswordTime: parseIntegerDate, lockoutTime: parseIntegerDate, accountExpires: parseIntegerDate, pwdLastSet: parseIntegerDate, badPwdTime: parseIntegerDate, msExchMailboxGuid: parseObjectGUID, whenChanged: parseGeneralizedDate, whenCreated: parseGeneralizedDate, "mS-DS-ConsistencyGuid": parseObjectGUID, "msRTCSIP-UserRoutingGroupId": parseObjectGUID, badPwdCount: parseInteger8, logonCount: parseInteger8, } const attributeSyntaxTransforms = { //////// Active Directory attribute Syntax OIDs //"2.5.5.12": parseUnicodeString, //Unicode String "2.5.5.11": parseGeneralizedDate, //GeneralizedDate "2.5.5.10": parseOctetString, //Octet String (raw buffer) "2.5.5.17": parseObjectSID, //ObjectSID "2.5.5.16": parseLargeInteger, //LargeInteger "2.5.5.9": parseInteger8, //Integer8 "2.5.5.8": parseBoolean, //Active Directory Boolean //////// OpenLDAP attribute Syntax OIDs //"1.3.6.1.4.1.1466.115.121.1.15": parseUnicodeString, //Directory String "1.3.6.1.4.1.1466.115.121.1.40": parseOctetString, //Octet String (raw buffer) "1.3.6.1.4.1.1466.115.121.1.24": parseGeneralizedDate, //Generalized Time "1.3.6.1.4.1.1466.115.121.1.7": parseBoolean, //Boolean "1.3.6.1.4.1.1466.115.121.1.27": parseInteger8, //Integer "1.3.6.1.4.1.1466.115.121.1.53": parseIntegerDate, //UTC Time } /** * Coerce an ldapjs entry into an object with keys (using entry.object as a base) * (This uses module.attributeTransforms to map attribute names to type parsers. Many of the names present are Active-Directory-LDAP specific and I don't honestly know how well it works on other instances) * @param {Object} entry - An entry object retuend from ldapjs * @return {Object} An object in the form { sAMAccountName: 'PickleRick', objectSid: 'S-1-5-21-3231561394-915883793-4011936924-6446' } */ function coerceEntry(entry){ if(!entry || !entry.attributes || !Array.isArray(entry.attributes)){ throw new TypeError('entry.attributes must be an array') } return entry.attributes.reduce((obj, attr) => { if(attributeTransforms[attr.type]){ const ar = attr.buffers.map(attributeTransforms[attr.type]) obj[attr.type] = ar.length === 1 ? ar[0] : ar } return obj }, {...entry.object}) } /** * Parse an LDAP Interger8 buffer into a Number * @param {Buffer} buffer - A buffer representing an attribute of type Integer8. * @return {Number} A Number */ function parseInteger8(buffer){ return Number(buffer.toString('utf8')) } /** * Parse an LDAP OctetString buffer into a base 64 encoded certificate string with line breaks every 64 characters. * (this should match the content in a .pem certificate file) * @param {Buffer} buffer - A buffer representing an LDAP attribute of type OctetString * @return {String} A Base64 string */ function bufferToPEMCertString(buffer){ //convert the buffer to a base 64 string and insert newlines every 64 characters const base64String = Array.from(buffer.toString('base64')) .map((v, i) => { return (i + 1) % 64 === 0 ? v + "\n" : v }) .join('') return `-----BEGIN CERTIFICATE-----\n${base64String}\n-----END CERTIFICATE-----\n` } /** * Parse a base 64 encoded certificate string into a buffer representing an LDAP OctetString. * @param {String} str - A Base64 string * @return {Buffer} A buffer representing an LDAP attribute of type OctetString. */ function PEMCertStringToBuffer(str){ const lines = str .replace(/\r/g, '\n') .split('\n') .map(line => line.trim()) .filter(line => line.length) //if findIndex return -1, adding one will give us 0 (perfect) const startIndex = lines.findIndex(line => line.toUpperCase().startsWith('-----BEGIN CERTIFICATE-----')) + 1 let endIndex = lines.findIndex(line => line.toUpperCase().startsWith('-----END CERTIFICATE-----')) //if endIndex === -1, slice will return all the rest of the indices return Buffer.from(lines.slice(startIndex, endIndex).join(''), 'base64') } /** * Parse an LDAP GeneralizedDate buffer into a javascript Date object * @param {Buffer} buffer - A buffer representing an LDAP GeneralizedDate attribute. * @return {Date} A Date object */ function parseGeneralizedDate(buffer){ const str = buffer.toString('utf8') //the Date constructor accepts an ISO date string, so we can just add in some delimiters to the RFC4517 format return new Date( `${str.substring(0, 4)}-${str.substring(4, 6)}-${str.substring(6, 8)}` + `T${str.substring(8, 10)}:${str.substring(10, 12)}:${str.substring(12)}` ) } /** * Convert a javascript Date object into an LDAP GeneralizedDate string * (Can be converted to a buffer with Buffer.from(str, 'utf8) but the most common use-case is in ldapjs filters, which take a string) * @param {Date} date - A Date object * @return {String} - A String in the format "YYYYMMDDHHMMSS.mmmZ" eg."20211225070000.000Z" */ function toGeneralizedDateString(date) { if(isNaN(date)){ throw new TypeError('toGeneralizedDateString cannot convert an invalid date') } return date.getUTCFullYear().toString().padStart(4, '0') + (date.getUTCMonth() + 1).toString().padStart(2, '0') + date.getUTCDate().toString().padStart(2, '0') + date.getUTCHours().toString().padStart(2, '0') + date.getUTCMinutes().toString().padStart(2, '0') + date.getUTCSeconds().toString().padStart(2, '0') + `.${date.getUTCMilliseconds()}Z` } /** * Parse an LDAP IntegerDate buffer into a javascript Date object * @param {Buffer} buffer - A buffer representing an LDAP IntegerDate attribute. * @return {Date} A Date object */ function parseIntegerDate(buffer){ const str = buffer.toString('utf8') if(str === '' || str === '0'){ return null } return new Date(Number(str) / 1e4 - 1.16444736e13) } /** * Convert a javascript Date object into an LDAP IntegerDate string * (Can be converted to a buffer with Buffer.from(str, 'utf8) but the most common use-case is in ldapjs filters, which take a string) * @param {Date} date - A Date object * @return {String} - A string representation of an integer */ function toIntegerDateString(date){ if(isNaN(date)){ return '' } return ((date.getTime() + 1.16444736e13) * 1e4).toString() } /** * Parse an LDAP IntegerDate buffer into a javascript Date object * @param {Buffer} buffer - A buffer representing an LDAP IntegerDate attribute. * @param {Function} NumberType - A constructor for the Type of Number you want returned (defaults to String). You can supply "BigInt" here, if your environment supports it. * @return {Date} A Date object */ function parseLargeInteger(buffer, NumberType = String){ const str = buffer.toString('utf8') return str === '' ? null : NumberType(str) } /** * Parse an LDAP ObjectSID buffer into a string * @param {Buffer} buffer - A buffer representing an LDAP ObjectSID attribute. * @return {String} An ObjectSID string eg S-1-5-21-3231561394-915883793-4011936924-6446 */ function parseObjectSID(buffer) { const str = [ 'S', buffer.readUInt8(0), //version buffer.readUIntBE(2, 6), //type ...Array //append the actual sid parts //create an empty array with a length equal to number of fields in the sid (found at byte 1) .from({ length: buffer.readUInt8(1) }) //for each index, read a 4 byte unsigned integer (starting at offset 8) .map((v, i) => buffer.readUInt32LE((i * 4) + 8)) ].join('-') return str } /** * Convert an objectSID string into an LDAP ObjectSID buffer * @param {String} str - An ObjectSID string eg. S-1-5-21-3231561394-915883793-4011936924-6446 * @return {Buffer} A buffer representing an LDAP ObjectSID */ function toObjectSIDBuffer(str) { const buffer = Buffer.alloc(28) const parts = str.split('-') const sidParts = parts.slice(3) buffer.writeUInt8(Number(parts[1]), 0) //version buffer.writeUInt8(sidParts.length, 1) //number of sid fields buffer.writeUIntBE(Number(parts[2]), 2, 6) //type sidParts.forEach((sidPart, i) => buffer.writeUInt32LE(Number(sidPart), (i * 4) + 8)) return buffer } /** * Parse an LDAP ObjectGUID buffer into a string * @param {Buffer} buffer - A buffer representing an LDAP ObjectGUID attribute. * @return {String} An ObjectSID string eg S-1-5-21-3231561394-915883793-4011936924-6446 */ //adapted from https://github.com/ldapjs/node-ldapjs/issues/481#issuecomment-782540019 function parseObjectGUID(buffer){ //an array of byte offsets for each part of the guid return [ [3, 2, 1, 0], [5, 4], [7, 6], [8, 9], [10, 11, 12, 13, 14, 15] ] .map(byteOffsets => byteOffsets.map(byteOffset => //convert the byte to a hexidecimal string and pad the start if it only has one character buffer[byteOffset].toString(16).padStart(2, '0').toUpperCase() ).join('') ).join('-') } /** * Convert an ObjectGUID string into an LDAP ObjectGUID buffer * @param {String} str - An ObjectGUID string eg. 1E633E26-741B-4D0E-94C7-64D8DED0791F * @return {Buffer} A buffer representing an LDAP ObjectGUID attribute */ function toObjectGUIDBuffer(str){ const parts = String(str).split('-') if(parts.length !== 5){ return null } //an array of "coordinates" for where to find each byte of the buffer //(each sub-array contains which "part" of the string and the offset within that part to find the 2 character byte) return Buffer.from([ [0, 3], [0, 2], [0, 1], [0, 0], [1, 1], [1, 0], [2, 1], [2, 0], [3, 0], [3, 1], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4], [4, 5], //parse each 2 character hexidecial string into an integer (eg af => 25) ].map(([partIndex, stringIndex]) => parseInt(parts[partIndex].substr(stringIndex * 2, 2), 16))) } //Just a utility function to pass the buffer through unchanged function parseOctetString(buffer){ return buffer } /** * Parse an LDAP Boolean buffer into a javascript Boolean * @param {Buffer} buffer - A buffer representing an LDAP Boolean attribute. * @return {Boolean} A javascript Boolean */ function parseBoolean(buffer){ return buffer.toString('utf8').toUpperCase() === 'TRUE' } /** * Convert a javascript Boolean into a string representation of an LDAP Boolean * @param {Boolean} buffer - A buffer representing an LDAP Boolean attribute * @return {String} An upper case javascript string. Either "TRUE" or "FALSE" */ function toBooleanString(bool){ return bool ? 'TRUE' : 'FALSE' } /** * Parse an LDAP UnicodeString buffer into a javascript String * @param {Buffer} buffer - A buffer representing an LDAP UnicodeString attribute * @return {String} A javascript String */ function parseUnicodeString(buffer){ return buffer.toString('utf8') } /** * Parse an LDAP UserAccountControl buffer into a javascript Object * @param {Buffer} buffer - A buffer representing an LDAP UserAccountControl attribute * @return {Object} A javascript Object with Boolean properties representing each possible flag in a UserAccountControl bitmask eg: { accountDisabled: true, lockedOut: false, ...} */ function parseUserAccountControl(buffer){ const value = Number(buffer.toString('utf8')) //AD attributes defined below //https://docs.microsoft.com/en-us/windows/win32/api/iads/ne-iads-ads_user_flag_enum return Object.entries({ //ADS_UF_SCRIPT The logon script is executed. logonScriptExecuted: 0x00000001, //ADS_UF_ACCOUNTDISABLE The user account is disabled. accountDisabled: 0x00000002, //ADS_UF_HOMEDIR_REQUIRED - The home directory is required. homeDirRequired: 0x00000008, //ADS_UF_LOCKOUT - The account is currently locked out. lockedOut: 0x00000010, //ADS_UF_PASSWD_NOTREQD - No password is required. passwordNotRequired: 0x00000020, //ADS_UF_PASSWD_CANT_CHANGE - The user cannot change the password. passwordCantChange: 0x00000040, //ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED - The user can send an encrypted password. encryptedPasswordAllowed: 0x00000080, //ADS_UF_TEMP_DUPLICATE_ACCOUNT //This is an account for users whose primary account is in another domain. //This account provides user access to this domain, but not to any domain that trusts this domain. //Also known as a local user account. duplicateAccount: 0x00000100, //ADS_UF_NORMAL_ACCOUNT - This is a default account type that represents a typical user. normalAccount: 0x00000200, //ADS_UF_INTERDOMAIN_TRUST_ACCOUNT - This is a permit to trust account for a system domain that trusts other domains. interdomainTrustAccount: 0x00000800, //ADS_UF_WORKSTATION_TRUST_ACCOUNT - This is a computer account for a computer that is a member of this domain. workstationTrustAccount: 0x00001000, //ADS_UF_SERVER_TRUST_ACCOUNT - This is a computer account for a system backup domain controller that is a member of this domain. serverTrustAccount: 0x00002000, //ADS_UF_DONT_EXPIRE_PASSWD - The password for this account will never expire. passwordNeverExpires: 0x00010000, //ADS_UF_MNS_LOGON_ACCOUNT - This is an MNS logon account. msnLogonAccount: 0x00020000, //ADS_UF_SMARTCARD_REQUIRED - The user must log on using a smart card. smartcardRequired: 0x00040000, //ADS_UF_TRUSTED_FOR_DELEGATION //The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. //Any such service can impersonate a client requesting the service. trustedForDelegation: 0x00080000, //ADS_UF_NOT_DELEGATED - The security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation. notDelagated: 0x00100000, //ADS_UF_USE_DES_KEY_ONLY - Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. useDESKeysOnly: 0x00200000, //ADS_UF_DONT_REQUIRE_PREAUTH - This account does not require Kerberos pre-authentication for logon. dontRequirePreauth: 0x00400000, //ADS_UF_PASSWORD_EXPIRED - The user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy. accountExpired: 0x00800000, //ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION - The account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network. trustedToAuthenticateForDelegation: 0x01000000, }).reduce((obj, [propertyName, bitMask]) => { obj[propertyName] = (value & bitMask) === bitMask return obj }, {}) } /** * Parse an LDAP SAMAccountType buffer into a javascript Object * @param {Buffer} buffer - A buffer representing an LDAP SAMAccountType attribute * @return {Object} A javascript Object with Boolean properties representing each possible flag in a SAMAccountType bitmask eg: { userObject: true, normalUserAccount: true, ...} */ function parseSAMAccountType(buffer){ const value = Number(buffer.toString('utf8')) return Object.entries({ domainObject: 0x0, //SAM_DOMAIN_OBJECT groupObject: 0x10000000, //SAM_GROUP_OBJECT nonSecurityGroupObject: 0x10000001, //SAM_NON_SECURITY_GROUP_OBJECT aliasObject: 0x20000000, //SAM_ALIAS_OBJECT nonSecurityAliasObject: 0x20000001, //SAM_NON_SECURITY_ALIAS_OBJECT userObject: 0x30000000, //SAM_USER_OBJECT normalUserAccount: 0x30000000, //SAM_NORMAL_USER_ACCOUNT machineAccount: 0x30000001, //SAM_MACHINE_ACCOUNT trustAccount: 0x30000002, //SAM_TRUST_ACCOUNT appBasicGroup: 0x40000000, //SAM_APP_BASIC_GROUP appQueryGroup: 0x40000001, //SAM_APP_QUERY_GROUP accountTypeMax: 0x7fffffff, //SAM_ACCOUNT_TYPE_MAX }).reduce((obj, [propertyName, bitMask]) => { obj[propertyName] = (value & bitMask) === bitMask return obj }, {}) } module.exports = { attributeTransforms, attributeSyntaxTransforms, coerceEntry, bufferToPEMCertString, PEMCertStringToBuffer, parseGeneralizedDate, toGeneralizedDateString, parseIntegerDate, toIntegerDateString, parseInteger8, parseLargeInteger, parseObjectSID, toObjectSIDBuffer, parseObjectGUID, toObjectGUIDBuffer, parseBoolean, toBooleanString, parseUserAccountControl, parseSAMAccountType, parseUnicodeString, }