Skip to content

Repository files navigation

Secure Dynamic QR-Codes for Android

CI Nightly Vector drift

An Android app for generating the Secure Dynamic QR-Code URLs used for "Ad hoc payment at charging stations", as defined by OCPP v2.1 (Edition 2), use case C25: Ad hoc payment via a QR code.

A static QR-Code sticker on a charging station can be replaced by a fraudulent one pointing at a fake payment site. A secure dynamic QR-Code prevents this: the URL carries a Time-Based One-Time Password (TOTP) derived from a shared secret known only to the Charging Station and the CSMS, so a scanned URL can be proven authentic and expires within seconds. Additionally the TOTP protects the CSMS from fraudulent requests and Distributed Denial-of-Service (DDoS) attacks, as it acts as a proof-of-presence of the EV driver sending this request to really be currently in front of the specified charging station.

This app covers both ends of that exchange interactively: it builds the URL the way a Charging Station does, and it checks a scanned one the way a CSMS does. That makes it useful for testing a CSMS, for demonstrating the mechanism, and for checking a charging station's configuration against a reference implementation.

You can download it now from the Google PlayStore

Features

As a Charging Station

  • Generate a secure dynamic QR-Code URL from a configurable URL template, EVSE ID, shared secret, validity time, TOTP length and alphabet
  • A running and syncing a clock against legal time servers, to inspect the URL of any given moment
  • Live display of the remaining validity of the current time slot

As a CSMS

  • Receive a request including a secure dynamic QR-Code and read its placeholders back out using the URL template
  • Verify the token against the previous, the current and the next time slot, and report which one matched - a token that keeps landing in a neighbour points at a drifting clock
  • Report a URL that does not follow the template at all, which is the sticker-over-sticker fraud C25 exists to prevent
  • Achieve some proof-of-presence, that the EV driver is really in front of the given charging station

Validation uses the timestamp from the input field rather than the system clock, so that it agrees with the URL shown above it. Freezing the timestamp therefore makes a scan reproducible, which is what testdata/qrcodes builds on: three codes, one per outcome, for checking the scanner by hand.

The line below the scan button names the build that is running - version, git revision and the moment it was installed - because the version alone cannot tell a store release from a local build. A long press copies it.

The TOTP algorithm

TOTP.kt implements TOTP algorithm, version 1 of OCPP 2.1 C25: HMAC-SHA256 over the big-endian byte representation of the current time interval, mapped to an alphabet starting at an offset derived from the last byte of the hash.

The implementation is deliberately free of Android dependencies so that it can be verified against the conformance vectors that every implementation of this algorithm shares:

  • WhitepapersTimeBasedOneTimePasswords/test-vectors/, the normative annex the vectors belong to
  • TOTP.ts — the TypeScript reference implementation, which vendors a copy of the same files
  • DynamicQRCodes — the algorithm and its documentation

TOTPTest.kt runs those vectors, covering the default parameters, alternative alphabets and token lengths, slot boundaries, the epoch wrap-around, and the inputs that must be rejected. Tokens generated by this app are byte-identical to those of the reference implementations.

It runs a subset: 29 of the 38 generation vectors and 13 of the 18 rejection cases. The rest select SHA-384 or SHA-512, require TLS channel binding, or feed in values a UInt parameter cannot express — none of which apply to an implementation of version 1 of the algorithm. app/src/test/resources/conformance-vector-scope.txt lists every published vector with its verdict, and two things keep it honest: a unit test that fails when it disagrees with the vectors pinned in TOTPTest.kt, and a nightly workflow that fails when the specification publishes an id it does not mention.

Some tests have to run on a device rather than locally. Android matches regular expressions with ICU while local unit tests use the JVM's own engine, and the two disagree about what is a valid pattern, so URLTemplateInstrumentedTest exercises the URL template where the app actually runs:

gradlew.bat connectedAndroidTest

Building

Requires JDK 17 or newer. CI builds on JDK 21 and the nightly repeats the whole build on JDK 26, so both ends of that range are verified continuously. The Gradle wrapper brings its own Gradle.

Run the unit tests:

gradlew.bat test

Build a release APK, or an App Bundle for Google Play:

gradlew.bat assembleRelease
gradlew.bat bundleRelease

The Gradle build picks the JDK up from JAVA_HOME. To pin a specific JDK without touching the repository, set org.gradle.java.home in your personal ~/.gradle/gradle.properties.

Continuous integration

Three workflows, each answering a different question. Every one of them can also be started by hand from the Actions tab.

CI gates every push and pull request: unit tests, lint, a debug build, and a release build — the last of these because it is the only one that runs R8 and the resource shrinker, so a keep rule that stops covering something used reflectively fails here rather than on a device. A second job runs the instrumented tests on an API 31 emulator, for the class of bug that local unit tests structurally cannot catch.

Nightly changes one thing about the environment at a time: the build on the newest JDK there is, the instrumented tests across API 31, 34 and 36, and a report of what has a newer version. That last one is equally useful locally and needs nothing but curl:

bash ci/dependency-updates.sh

Vector drift is the only one that does not test what this repository contains. It fetches the conformance vectors as currently published and fails when the specification has moved: a vector id nobody here has classified, or an expected value that no longer matches what TOTP.kt computes. A gate against pinned copies cannot see either.

Requirements

  • Android 12 (API 31) or newer
  • A camera, only for scanning and validating QR-Codes

License

Copyright 2024-2026 GraphDefined GmbH

Licensed under the Apache License, Version 2.0

About

OCPP v2.1 Secure Dynamic QR-Codes Testing Application for Android

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages