Search code examples
pythondatetimetimestampdnp3

Convert 48-bits (6 octets) from DNP3 time to timestamp in python


I'm trying to convert 48-bits (8 octets) to a timestamp using python for a little security project. I'm working with some network packets from the DNP3 protocol and I'm trying to decode timestamp values foreach DNP3 class object.

According to the DNP3 standard, "DNP3 time (in the form of an UINT48): Absolute time value expressed as the number of milliseconds since the start of January 1, 1970".

I have the following octets which need to be converted into a datetime:

# List of DNP3 timestamps
DNP3_ts = []

# Feb 20, 2016 00:27:07.628000000 UTC
DNP3_ts.append('\xec\x58\xed\xf9\x52\x01')

# Feb 20, 2016 00:34:08.107000000 UTC
DNP3_ts.append('\x6b\xc3\xf3\xf9\x52\x01')

# Feb 20, 2016 00:42:40.460000000 UTC
DNP3_ts.append('\xcc\x94\xfb\xf9\x52\x01')

# Feb 20, 2016 00:56:47.642000000 UTC
DNP3_ts.append('\x1a\x82\x08\xfa\x52\x01')

# Feb 20, 2016 00:56:48.295000000 UTC
DNP3_ts.append('\xa7\x84\x08\xfa\x52\x01')

# Feb 20, 2016 00:58:21.036000000 UTC
DNP3_ts.append('\xec\xee\x09\xfa\x52\x01')

# Feb 20, 2016 01:17:09.147000000 UTC
DNP3_ts.append('\x9b\x25\x1b\xfa\x52\x01')

# Feb 20, 2016 01:49:05.895000000 UTC
DNP3_ts.append('\xe7\x64\x38\xfa\x52\x01')

# Feb 20, 2016 01:58:30.648000000 UTC
DNP3_ts.append('\xf8\x02\x41\xfa\x52\x01')

for ts in DNP3_ts:
    print [ts]

So I need figure out the following steps:

# 1. Converting the octets into a 48bit Integer (which can't be done in python)

# 2. Using datetime to calculate time from 01/01/1970

# 3. Convert current time to 48bits (6 octets)

If anyone can help me out with these steps it would be very much appreciated!


Solution

  • You can trivially combine the bytes to create a 48-bit integer with some bitwise operations. You can convert each octet to a uint8 with ord() and left shift them by a different multiple of 8 so they all occupy a different location in the 48-bit number.

    DNP3 encodes the bytes in a reverse order. To visualise this, let your octets from left to right called A-F and the bits of A called aaaaaaaa, etc. So from your octets to the 48-bit number you want to achieve this order.

           A        B        C        D        E        F
    ffffffff eeeeeeee dddddddd cccccccc bbbbbbbb aaaaaaaa
    

    Once you have the milliseconds, divide them by 1000 to get a float number in seconds and pass that to datetime.datetime.utcfromtimestamp(). You can further format this datetime object with the strftime() method. The code to achieve all this is

    from datetime import datetime
    
    def dnp3_to_datetime(octets):
        milliseconds = 0
        for i, value in enumerate(octets):
            milliseconds = milliseconds | (ord(value) << (i*8))
    
        date = datetime.utcfromtimestamp(milliseconds/1000.)
        return date.strftime('%b %d, %Y %H:%M:%S.%f UTC')
    

    By calling this function for each of your DNP3 times, you get the following results.

    Feb 19, 2016 14:27:07.628000 UTC
    Feb 19, 2016 14:34:08.107000 UTC
    Feb 19, 2016 14:42:40.460000 UTC
    Feb 19, 2016 14:56:47.642000 UTC
    Feb 19, 2016 14:56:48.295000 UTC
    Feb 19, 2016 14:58:21.036000 UTC
    Feb 19, 2016 15:17:09.147000 UTC
    Feb 19, 2016 15:49:05.895000 UTC
    Feb 19, 2016 15:58:30.648000 UTC
    

    You'll notice that these results lag by 8 hours exactly. I can't figure out this discrepancy, but I don't think my approach is wrong.

    In order to go from a datetime to a DNP3 time, start by converting the time to a timestamp of milliseconds. Then, by right shifting and masking 8 bits at a time you can construct the DNP3 octets.

    def datetime_to_dnp3(date=None):
        if date is None:
            date = datetime.utcnow()
        seconds = (date - datetime(1970, 1, 1)).total_seconds()
        milliseconds = int(seconds * 1000)
        return ''.join(chr((milliseconds >> (i*8)) & 0xff) for i in xrange(6))
    

    If you call it without arguments, it'll give you the current time, but you have the option to specify any specific datetime. For example, datetime(1970, 1, 1) will return \x00\x00\x00\x00\x00\x00 and datetime(1970, 1, 1, 0, 0, 0, 1000) (one millisecond after the 1970 epoch) will return \x01\x00\x00\x00\x00\x00.

    Note, depending on the bytes in the DNP3 time, you may get weird symbols if you try to print them. Don't worry though, the bytes are still there, it's just that Python trying to encode them to characters. If you want to see the individual bytes with interfering with each other, simply print list(DNP3_ts[i]). You may notice that it prints '\x52' as R (similar to many ASCII printable characters), but they are equivalent.