Skip to content

Instantly share code, notes, and snippets.

@ernstki
Last active November 6, 2025 16:40
Show Gist options
  • Save ernstki/475186db21bad40d523454121faa2cb1 to your computer and use it in GitHub Desktop.
Save ernstki/475186db21bad40d523454121faa2cb1 to your computer and use it in GitHub Desktop.
Convert LDAP timestamps (100-nanosecond intervals since 1 Jan, 1601) to human-readable times in your local timezone
#!/usr/bin/env python3
##
## Convert LDAP’s “number of 100-nanosecond intervals since January 1, 1601
## (UTC)” timestamps to human-readable dates, in the local timezone. See
## reference [1] for more details.
##
## Does not use f-strings, so should work in any old Python 3.
##
## Author: Kevin Ernst <ernstki -at- mail.uc.edu>
## Date: 13 Oct 2025
## License: WTFPL or ISC, at your option
## Homepage: https://gist.github.com/ernstki/475186db21bad40d523454121faa2cb1/edit
##
## References:
## [1] https://learn.microsoft.com/en-us/windows/win32/adschema/a-badpasswordtime
## [2] https://www.loc.gov/item/today-in-history/november-18
import sys
from datetime import (datetime as dt, timedelta as td, timezone as tz)
HOMEPAGE = 'https://gist.github.com/ernstki/475186db21bad40d523454121faa2cb1/edit'
def hectonanosecs_to_datetime(hectonanosecs):
# for example: 134048391507850787
epoch = dt(year=1601, month=1, day=1, tzinfo=tz.utc)
seconds_since_epoch = int(hectonanosecs) * 100 / 1e9 # secs since 1/1/1601
delta = td(seconds=seconds_since_epoch)
return epoch + delta
def hectonanosecs_to_human_date(hectonanosecs, raises=False):
dt = hectonanosecs_to_datetime(hectonanosecs)
try:
timestamp = dt.astimezone()
return str(timestamp)
except ValueError as e:
# timedelta doesn't have information do timezone-aware math for dates
# before 1883, because without this you get this error:
#
# ValueError: offset must be a timedelta representing a whole
# number of minutes, not datetime.timedelta(-1, 68638)
#
# If you're curious _why_, reference [2], a "Today in History"
# article from the Library of Congress, has your answer:
#
# > On November 18, 1883, precisely at noon, North American railroads
# > switched to a new standard time system for rail operations, which
# > they called Standard Railway Time (SRT). Almost immediately after
# > being implemented, many American cities enacted ordinances,
# > thus resulting in the creation of time “zones.” The four
# > standard time zones adopted were Eastern, Central, Mountain,
# > and Pacific. Though tailored to the railroad companies’ train
# > schedules, the new system was quickly adopted nationwide,
# > forestalling federal intervention in civil time for more than
# > thirty years, until 1918, when daylight saving time was introduced.
if raises:
raise RuntimeError(
"Invalid timestamp. Mininum supported value is"
"89268227999999995 (1883-11-18 12:00:00-05:00)") from e
else:
return '<invalid timestamp>'
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='Converts timestamps in hectonanoseconds to normal '
'datestamps.',
epilog="Hint: Can also be used as a filter, reading from the "
"stdout of another program like 'ldapsearch'. When used "
"this way, any sufficiently large number is deemed to be "
"a timestamp, converted in-place, and highlighted in ANSI "
"color. Problems? Leave a comment at {}".format(HOMEPAGE))
parser.add_argument('timestamps', nargs='*', metavar='TIMESTAMP',
help='a timestamp in hectonanoseconds')
parser.add_argument(
'-C', dest='color', action='store_const', const='always',
help="force color when used as a filter, even if stdout isn't a "
"terminal (same as '--color=always')")
parser.add_argument('--raise', dest='raises', action='store_true',
help='raise detailed exceptions for bad timestamps instead of '
'just printing "<invalid timestamp>"')
parser.add_argument('-d', '--debug', action='store_true',
help='enable debug logging')
parser.add_argument('--color', dest='color', metavar='WHEN',
help="control ANSI color highlighting; 'always' = use color even "
"when stdout isn't a tty, 'never' = suppress color, even if "
"stdout is a tty")
args = parser.parse_args()
if sys.stdin.isatty():
if not len(args.timestamps):
parser.error('Expected one or more timestamps.')
for timestamp in args.timestamps:
timestamp = hectonanosecs_to_human_date(timestamp, args.raises)
print(timestamp)
sys.exit(1)
import re, logging
ansi_color = False
boldblue = "\x1b[34;1m"; reset ="\x1b[0m"
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
if (sys.stdout.isatty() and not args.color in \
['0', 'no', 'off', 'none', 'never', 'false']) \
or args.color == 'always':
ansi_color = True
for line in sys.stdin.readlines():
line = line.strip()
# if a timestamp is < 17 digits and it's before time zones existed, so
# don't even bother; in the high 18 digits it's like 4,500 CE, and
# after 19 digits Python's datetime library wraps back to the 1900s
matches = re.findall(r'\b\d{17,18}\b', line)
if not matches:
logging.debug("No match for line '%s'" % line)
logging.debug("matches: %s", matches)
print(line)
continue
for match in matches:
timestamp = hectonanosecs_to_human_date(match, args.raises)
if ansi_color: #and not match == line:
timestamp = r"%s%s%s" % (boldblue, timestamp, reset)
line = line.replace(match, timestamp)
print(line)
@ernstki
Copy link
Author

ernstki commented Oct 14, 2025

Before:

$ ldapsearch -H ldap://ldap.yourdomain.tld [opts...] '(uid=login)' | grep -E 'bad|pwd'
badPwdCount: 2
badPasswordTime: 134048391507850787
pwdLastSet: 134042376042553412

After:

$ ldapsearch -H ldap://ldap.yourdomain.tld [opts...] '(uid=login)' | grep -E 'bad|pwd' | ./dehectonanoseconds.py
badPwdCount: 2
badPasswordTime: 2025-10-13 10:25:50.785080-00:00
pwdLastSet: 2025-10-06 11:20:04.255341-00:00

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment