Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

frereit/pydhl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pyDHL

pyDHL is a Python implementation of the protocols used by the DHL Lean Packstation (LPS) lockers and the older FLAC variant used in Germany to pick up parcels. LPS lockers are those without a display, and without internet, while FLAC lockers have a display and are usually used with a QR-code from your phone.

LPS lockers make an interesting target for security analysis, because all messages between the LPS device and the LPS API have to be relayed by the (untrustworthy) user device, because the LPS device itself has no internet connection.

This project was started in the Reverse Engineering Lab lecture by SEEMO @ TU Darmstadt.

Installation

You can install this package using uv (or any other Python package manager):

uv pip install git+https://github.com/frereit/pydhl.git

Usage

This implementation uses async in most places. Therefore, to use this package, you should run it inside an asyncio executor, like so:

import asyncio

async def main() -> int:
    # Interact with the pyDHL package in here
    ...

    return 0

if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))

Authentication

Most interactions with the Packstation and the Web APIs require a JWT token for your DHL account.

We have implemented a small helper function that spawns Google Chrome using Selenium and lets you log in to your account using OpenID Connect.

It should be used with a context manager so that the underlying API connection is closed correctly:

from pydhl.api.oidc import login

async with await login() as sess:
    print("Succesfully logged in to account with postnumber", await sess.get_postnumber())

Device Registration

If you want to pick up a parcel from either a FLAC or Lean Packstation, you will need to register a new device with your DHL account or obtain an existing device seed. This is required to decrypt data from the DHL Web API.

At the moment, the only device registration method implemented in pyDHL is "Peer to Peer" registration, where you use an already registered device to authenticate your new device. Therefore, you will need access to an already registered device for the registration process.

Assuming we already have an authenticated API session in sess, we must first request a registration challenge, then obtain an authentication token using another registered device, and finally use both to register our new device:

# This is metadata about the device we're adding
device_info = DeviceInfo(
    manufacturer_model="Foo",
    manufacturer_name="Bar",
    manufacturer_operating_system="Baz",
    name="TROOPERS25",
)
# We generate a new cryptographically secure random key for the device
device_key = DeviceKey.generate()

# We'll sign this challenge with the new device key
# but pyDHL handles this for us
challenge = await sess.begin_registration()

# Get the token. This is encoded as a QR code under "Geräteaktivierung" > "Gerät hinzufügen".
# For this example, we're not dealing with the QR code, but an example that scan the QR code
# from an emulator / screenshot is in example/demo_register_new_device.py
token = input("Please get an authentication token from a registered device: ")

# Perform the registration
device = await sess.register_by_peer_device(challenge, token, device_key, device_info)

# `device` is now a fully registered device.
# As an example, we use it to obtain a new authentication token
token = await sess.get_auth_token(device)

print("Registered a device and got a new authentication token:", token)

Parcel pickup

To pick up a parcel, you first need to get a list of parcels in your DHL account:

parcels = await sess.get_parcels(device)

The DeliverMachineData tells us where each parcel is located and what type of machine it is in. If it is a FLAC machine, we can easily generate a pickup code using pyDHL by passing the parcel to the get_pickup_code function:

from pydhl.flac import get_pickup_code

for p in parcels:
    if p.delivery_machine_data.type != "LPS":
        code = get_pickup_code(p)
        # This code is something like "A 012 345 678" and can be entered at a FLAC Packstation.
        # It is valid only for a short period of time, after which a new code must be generated.
        print("Parcel", parcel.parcel_data.identcode, "can be picked up with code", code)

Parcels of type "LPS" are a bit more involved, as you must first discover the Bluetooth device, connect to it, and finally call the LPS pickup API:

# This starts a Bluetooth scan for the LPS
lps = LpsBleScanner.discover(p.deliver_machine_data.uuid)

# Open the session. Note that currently only LPS that use the "Vintage"
# communication protocol are supported. Attempting to connect to a LPS
# with the "Neoteric" communication protocol will fail.
async with await LpsClient.open_session(
    p.deliver_machine_data.uuid,
    ble_communicator=LpsBleVintageCommunicator(lps),
    postnumber=sess.get_postnumber(),
    signer=device,
) as c:

    # List the parcels actually available at this LPS.
    # Note: This is a different API than the "account-wide" parcel listing.
    shipments = await c.api_session.get_parcels()
    for shipment in shipments:
        if shipment.parcel_id == p.parcel_data.parcel_id:
            # Call the pickup API to start the pickup process.
            # This will return once the compartment is closed again.
            await c.pickup_parcel(shipment)
            break

Limitations

  • pyDHL only implements parcel pickup. Sending parcels using the Packstation is not possible with this package.
  • pyDHL only supports the VintageCommunicationStrategy, which is only one of the two possible "low level" protocols built on BLE used by the app. The Packstation that we had access to when doing this research uses this protocol. There are vague plans to implement this at some point but no current progress.

Pull requests are very welcome. If you plan on implementing any of this, feel free to reach out to discuss.

Technical details

Cryptography

Every "registered device" owns a keypair, the public key of which is known to DHL and associated to your account.

Internally, this keypair is represented as a 32 byte random seed, which is then used to derive keys for encryption and signing.

Encryption and Signing is implemented using libsodium, with sealed boxes and public-key signatures respectively. The keys for these operations are derived from the same 32 byte seed using crypto_box_seed_keypair and crypto_sign_seed_keypair.

There is some additional cryptography happening between the LPS and the Web API that the user's device cannot see because it only serves as a relay. Our best guess is that every LPS knowns some public key assosciated with DHL servers and uses RSA encryption to encrypt a symmetric session key under this public key, which is then relayed to the DHL servers, which can then use the symmetric session key for all further communication. This is based on the fact that the inital message comes from the LPS and is always 4096 bit long, which might be a RSA-4096 ciphertext. Additionally, based on 25 sample ciphertexts, they are not uniformly distributed between 0 and 2^4096 (p < 5%). Poking at it (flipping bits etc) did not reveal anything, as the server rejects the ciphertext immediately. This is no surprise, as any RSA implemention will use some kind of padding like OAEP, which bit flips will invalidate.

Protocols

I won't detail the protocols here. Please, check the code for the exact details of how to interact with the API. However, here are some diagrams that showcase the general flow of information between the DHL servers, your device, and the Packstation.

Device Registration

Sequence diagram of device registration

LPS Pickup

Sequence diagram of lean pickup

Bluetooth communication

The Bluetooth messages are encoded as a Tag-Length-Value (TLV) message, which contains a command, some encrypted payload, an initialisation vector for said payload, metadata, and a checksum:

 0                   1                   2                   3  
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    Command    |                Remaining Length                
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 (cont.)        |                 Payload Length                 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 (cont.)        |                   Payload...                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           IV Length                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             IV...                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Metadata Length                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Metadata...                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Checksum           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Note that the "Remaining Length" indicates the length of the entire message excluding the command byte and the length field itself.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages