ldapjs-type-parsers
Version:
A collection of functions to parse raw buffers from LDAPJS
399 lines (376 loc) • 16.7 kB
JavaScript
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,
}