The link on the left discusses the details of this problem and unsigned arithmetic.
Many attributes in Active Directory have a data type (syntax) called Integer8. These 64-bit numbers (8 bytes)
often represent time in 100-nanosecond intervals. If the Integer8 attribute is a date, the value represents
the number of 100-nanosecond intervals since 12:00 AM January 1, 1601. Any
leap seconds are ignored.
In .NET Framework (and PowerShell) these 100-nanosecond intervals are called ticks, equal to one ten-millionth
of a second. There are 10,000 ticks per millisecond. In addition, .NET Framework
and PowerShell DateTime values represent dates as the number of ticks since
12:00 AM January 1, 0001.
ADSI automatically employs the IADsLargeInteger interface to deal with these 64-bit numbers. This interface has
two property methods, HighPart and
LowPart, which break the number up into two 32-bit numbers. The
HighPart and
LowPart
property methods return values between -2^31 and
2^31 - 1. The standard method of handling these attributes is
demonstrated by this VBScript program to retrieve the domain lockoutDuration value in minutes.
Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
' Retrieve lockoutDuration with IADsLargeInteger interface.
Set objDuration = objDomain.lockoutDuration
' Calculate number of 100-nanosecond intervals.
lngDuration = (objDuration.HighPart * (2^32)) + objDuration.Lowpart
' Convert to minutes. The value retrieved is negative, so make positive.
lngDuration = -lngDuration / (60 * 10000000)
Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration
However, whenever the LowPart method returns a negative value, the calculation above is wrong by 7 minutes, 9.5
seconds. The work-around is to increase the value returned by the
HighPart method by one whenever the value
returned by the LowPart method is negative. The revised code below gives correct results in all cases.
Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
' Retrieve lockoutDuration with IADsLargeInteger interface.
Set objDuration = objDomain.lockoutDuration
lngHigh = objDuration.HighPart
lngLow = objDuration.LowPart
' Adjust for error in IADsLargeInteger interface.
If (lngLow < 0) then
lngHigh = lngHigh + 1
End If
' Calculate number of 100-nanosecond intervals.
lngDuration = (lngHigh * (2^32)) + lngLow
' Convert to minutes.
lngDuration = -lngDuration / (60 * 10000000)
Wscript.Echo "Domain policy lockout duration in minutes: " & lngDuration
The error introduced if this inaccuracy is not accounted for is not large. The error is always 2^32 100- nanosecond intervals, which is 7 minutes, 9.5 seconds. All the programs on this site that deal with Integer8 attributes have been revised as shown on this page to give accurate results.
The link on the left discusses the details of this problem and unsigned arithmetic.
The function linked below accounts for this problem and can be used to convert any Integer8 attribute
value into a date in the local time zone:
Integer8Date.txt <<-- Click here to view or download the program
For completeness, here is a VBScript program that converts a date and time
in the local time zone into the corresponding Integer8 value:
DateToInteger8.txt <<-- Click here to view or download the program
An alternative method to convert Integer8 values into dates uses the Windows time
service tool w32tm.exe. This is included with Windows XP and Windows Server 2003 default
installations (and newer operating systems). This tool can be used to convert 64-bit values to dates in the local time
zone. The program must still use the IADsLargeInteger property methods to convert the
Integer8 value to a 64-bit number. We must also account for the inaccuracy described above
when the LowPart method returns a negative value. However, w32tm.exe takes the local time
zone bias into account and converts a 64-bit value into a date and time in the local time
zone. The program linked below also uses the Exec method of the wshShell object, so it
requires WSH 5.6 as well as w32tm.exe. The example program demonstrating a function using
w32tm.exe is linked here:
Integer8Date2.txt <<-- Click here to view or download the program
In PowerShell (and .NET Framework) DateTime values are represented internally
as the number of Ticks since 12:00 AM January 1, 0001. Ticks due to leap
seconds are ignored (as are the days lost when the switch was made from the
Julian to the Gregorian calendar in 1582). A PowerShell script to convert an
Integer8 value into the corresponding date in both the local time zone and
UTC (Coordinated Universal Time) is linked here:
PSInteger8ToDate.txt <<-- Click here to view or download the program
And a PowerShell script to convert a DateTime value in the local time zone
into the corresponding Integer8 value is linked here:
PSDateToInteger8.txt <<-- Click here to view or download the program
If you use ADO in a VBScript program to retrieve Integer8 attribute values, the following code will not invoke
the IADsLargeInteger interface and will raise an error:
Do Until adoRecordset.EOF
' This does not invoke the IADsLargeInteger interface.
Set objDate = adoRecordset.Fields("pwdLastSet")
' This statement raises an error.
lngHigh = objDate.HighPart
' Likewise, the Intger8Date function, documented above,
' raises an error.
dtmDate = Integer8Date(objDate, lngTZBias)
adoRecordset.MoveNext
Loop
You must either specify the Value property of the Field object and use the Set keyword:
Do Until adoRecordset.EOF
' Specify the Value property of the Field object.
Set objDate = adoRecordset.Fields("pwdLastSet").Value
' Invoke methods of the IADsLargeInteger interface directly.
lngHigh = objDate.HighPart
' Or use the Integer8Date function documented in the link above.
dtmDate = Integer8Date(objDate, lngTZBias)
adoRecordset.MoveNext
Loop
Or, you must assign the value to a variant, and then use the Set keyword to invoke the IADsLargeInteger interface:
Do Until adoRecordset.EOF
' Assign the value to a variant.
lngDate = adoRecordset.Fields("pwdLastSet")
' Use the Set keyword to invoke the IADsLargeInteger interface.
Set objDate = lngDate
' Invoke methods of the IADsLargeInteger interface directly.
lngHigh = objDate.HighPart
' Or use the Integer8Date function documented in the link above.
dtmDate = Integer8Date(objDate, lngTZBias)
adoRecordset.MoveNext
Loop
A complication arises if the Integer8 attribute does not have a value. If you attempt to retrieve the value directly from the Active Directory object, the IADs interface returns data type "Empty", instead of "Object". If you use ADO to retrieve the attribute value, the data type is "Null" when the Integer8 attribute has no value. In both cases, the Set statement used to invoke the IADsLargeInteger interface raises an error. One way to handle this is to trap the possible error. For example:
Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")
On Error Resume Next
Set objDate = objUser.lockoutTime
If (Err.Number <> 0) Then
On Error GoTo 0
dtmDate = "Never"
Else
On Error GoTo 0
' Use the Integer8Date function documented in the link above.
dtmDate = Integer8Date(objDate, lngTZBias)
End If
An alternative way to handle the possibility that an Integer8 attribute does not have a value is to use the VBscript TypeName or VarType function. For example:
Do Until adoRecordset.EOF
strType = TypeName(adoRecordset.Fields("lockoutTime").Value)
If (strType = "Object") Then
Set objDate = adoRecordset.Fields("lockoutTime").Value
' Use the Integer8Date function documented in the link above.
dtmDate = Integer8Date(objDate, lngTZBias)
Else
dtmDate = "Never"
End If
adoRecordset.MoveNext
Loop
Another complication arises if the Integer8 value corresponds to a date so far in the future that an error is raised when the 64-bit value is converted into a date. The accountExpires attribute is the only one where this has been seen. If a user object has never had an expiration date, Active Directory assigns the value 2^63 - 1 to the accountExpires attribute. This is the largest number that can be saved as a 64-bit value. It really means "never". If you attempt to convert the value to a date using the CDate function, an error is raised. The Integer8Date and Integer8Date2 functions linked above account for this and trap the error. The following table documents the possible values that have been observed for several Integer8 attributes that represent dates.
attribute | No Value | 0 | 2^63 - 1 | Date |
lastLogon | Yes | Yes | Yes | |
lastLogonTimeStamp | Yes | Yes | Yes | |
pwdLastSet | Yes | Yes | ||
accountExpires | Yes | Yes | Yes | |
lockoutTime | Yes | Yes | Yes | |
badPasswordTime | Yes | Yes | Yes |
Finally, it should be noted that a few Integer8 attributes can be modified with the IADsLargeInteger interface. So far the only Integer8 attributes found that can be modified in code (and assigned values other than 0 and -1) are maxStorage, accountExpires, maxPwdAge, minPwdAge, lockoutDuration, and lockoutObservationWindow. For example, the following VBScript program assigns the account expiration date of November 21, 2009, 4:02:18 PM UTC:
Set objUser = GetObject("LDAP://cn=Jim Smith,ou=West,dc=MyDomain,dc=com")
Set objDate = objUser.Get("accountExpires")
objDate.HighPart = 30042820
objDate.LowPart = 1500000
objUser.Put "accountExpires", objDate
objUser.SetInfo
Because the accountExpires attribute seems to always have a value (see the
table above), we do not expect an error to be raised by the
"Set objDate" statement. If an error could be
raised, the solution would be to first assign a value that does not require
the IADsLargeInteger interface, such as 0 (zero), so that we can then
retrieve the object reference required to assign values with the
HighPart
and LowPart methods.
And
the following VBScript program assigns the value -9,000,000,000
(corresponding to 15 minutes) to the domain minPwdAge attribute:
Set objDomain = GetObject("LDAP://dc=MyDomain,dc=com")
Set objDate = objDomain.Get("minPwdAge")
objDomain.HighPart = -3
objDomain.LowPart = -410065408
objDomain.Put "minPwdAge", objDate
objDomain.SetInfo