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
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.
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:
- Whitepapers —
TimeBasedOneTimePasswords/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
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.
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.
- Android 12 (API 31) or newer
- A camera, only for scanning and validating QR-Codes
Copyright 2024-2026 GraphDefined GmbH
Licensed under the Apache License, Version 2.0