commit 1e558f482d0459aa452f121edbca2e8d749b39e0
parent c3c23bfc73a91b6269faacf5e36d3e2c78ad996b
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Tue, 7 Oct 2025 01:58:27 +0200
[donau-verificator] update to support lsd0013 donau link
Diffstat:
6 files changed, 196 insertions(+), 47 deletions(-)
diff --git a/donau-verificator/README.md b/donau-verificator/README.md
@@ -6,7 +6,7 @@ The app verifies the donation statement made by a Donau.
2. For test purposes, a string of a valid donation statement is already hard coded.
3. With the defined URI scheme following command can be used:
```bash
-adb shell am start -a android.intent.action.VIEW -d "donau://2024/EUR:15/7560001010000/1234/SAAM5BA1F9H4VT6T78CFC3X63HAMY2TXB597XBVZ0EMXEZ90QPJ3000BXDBJ3ECHGB8AEX9FFQ5BAXVSF6X6NXM98PY353F2R99PP1R/E24CDJHGSPZG20ZSSTMTBREGCCP495WKETQYCYA9C93EPMZN4FEG"
+adb shell am start -a android.intent.action.VIEW -d "donau://example.com/megacharity/1234/2024/7560001010000/1234?total=EUR:15&sig=ED25519:SAAM5BA1F9H4VT6T78CFC3X63HAMY2TXB597XBVZ0EMXEZ90QPJ3000BXDBJ3ECHGB8AEX9FFQ5BAXVSF6X6NXM98PY353F2R99PP1R&pub=E24CDJHGSPZG20ZSSTMTBREGCCP495WKETQYCYA9C93EPMZN4FEG"
```
## Future Work
The public key should be requested directly from the Donau over HTTPS,
@@ -22,4 +22,4 @@ Mac OS, Linux:
- ./gradlew
Windows:
-- gradlew.bat
-\ No newline at end of file
+- gradlew.bat
diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java b/donau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java
@@ -39,6 +39,8 @@ import androidx.core.content.ContextCompat;
import net.taler.donauverificator.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
+ private static final String DEBUG_DONATION_STATEMENT =
+ "donau://example.com/megacharity/1234/2024/7560001010000/1234?total=EUR:15&sig=ED25519:SAAM5BA1F9H4VT6T78CFC3X63HAMY2TXB597XBVZ0EMXEZ90QPJ3000BXDBJ3ECHGB8AEX9FFQ5BAXVSF6X6NXM98PY353F2R99PP1R&pub=E24CDJHGSPZG20ZSSTMTBREGCCP495WKETQYCYA9C93EPMZN4FEG";
private int PERMISSIONS_REQUEST_CAMERA = 0;
private ActivityMainBinding binding;
private CodeScanner mCodeScanner;
@@ -69,8 +71,8 @@ public class MainActivity extends AppCompatActivity {
});
}
});
- //temporary for debugging
- sendRequestDialog("donau://2024/EUR:15/7560001010000/1234/SAAM5BA1F9H4VT6T78CFC3X63HAMY2TXB597XBVZ0EMXEZ90QPJ3000BXDBJ3ECHGB8AEX9FFQ5BAXVSF6X6NXM98PY353F2R99PP1R/E24CDJHGSPZG20ZSSTMTBREGCCP495WKETQYCYA9C93EPMZN4FEG");
+ //temporary for debugging (valid donation statement from sample QR code)
+ sendRequestDialog(DEBUG_DONATION_STATEMENT);
}
@@ -155,4 +157,4 @@ public class MainActivity extends AppCompatActivity {
}
}
-}
-\ No newline at end of file
+}
diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/Results.java b/donau-verificator/src/main/java/net/taler/donauverificator/Results.java
@@ -27,17 +27,17 @@ import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.List;
+import java.util.Locale;
public class Results extends AppCompatActivity {
static {
System.loadLibrary("verification");
}
- // QR-string : YEAR/TOTALAMOUNT/TAXID/TAXIDSALT/ED25519SIGNATURE/PUBKEY
+ // lsd0013 format: donau://host/year/taxid/salt?total=...&sig=ED25519:...
// CrockfordBase32 encoded: SIGNATURE, PUBLICKEY
// TODO: Salt and taxId should maybe also be encoded
- private final int NUMBER_OF_ARGUMENTS = 6;
private String year;
private String totalAmount;
private String taxId;
@@ -71,41 +71,22 @@ public class Results extends AppCompatActivity {
tableLayout.setVisibility(View.INVISIBLE);
Intent intent = getIntent();
- String scheme[];
- // handle URI scheme
- if (null != intent.getData()) {
- scheme = new String[2];
- Uri uri = intent.getData();
- List<String> pathSegments = uri.getPathSegments();
- StringBuilder fullPath = new StringBuilder();
- fullPath.append(uri.getHost());
- for (String segment : pathSegments) {
- fullPath.append("/").append(segment);
- }
- scheme[1] = fullPath.toString();
- // handle self scanned QR code
- } else {
- scheme = intent.getStringExtra("QR-String").split("//");
- if (scheme == null || scheme.length != 2 || !scheme[0].equals("donau:")) {
- statusHandling(SignatureStatus.INVALID_SCHEME);
- return;
- }
+ Uri uri = resolveUri(intent);
+ if (uri == null) {
+ statusHandling(SignatureStatus.INVALID_SCHEME);
+ return;
}
- String[] parts = scheme[1].split("/");
- if (parts == null || parts.length != NUMBER_OF_ARGUMENTS) {
- statusHandling(SignatureStatus.INVALID_NUMBER_OF_ARGUMENTS);
+
+ String scheme = uri.getScheme();
+ if (!isSupportedScheme(scheme)) {
+ statusHandling(SignatureStatus.INVALID_SCHEME);
return;
}
- try {
- year = parts[0];
- totalAmount = parts[1];
- taxId = parts[2];
- salt = parts[3];
- eddsaSignature = parts[4];
- publicKey = parts[5];
- } catch (Exception e) {
- statusHandling(SignatureStatus.MALFORMED_ARGUMENT);
+ resetParsedFields();
+ SignatureStatus parseStatus = parseDonauUri(uri);
+ if (parseStatus != null) {
+ statusHandling(parseStatus);
return;
}
@@ -162,5 +143,168 @@ public class Results extends AppCompatActivity {
public native int ed25519_verify(String year, String totalAmount,
String taxId, String salt,
String eddsaSignature, String publicKey);
-}
+ private Uri resolveUri(Intent intent) {
+ Uri data = intent.getData();
+ if (data != null) {
+ return data;
+ }
+ String raw = intent.getStringExtra("QR-String");
+ if (raw == null) {
+ return null;
+ }
+ return Uri.parse(raw);
+ }
+
+ private boolean isSupportedScheme(String scheme) {
+ if (scheme == null) {
+ return false;
+ }
+ String lowered = scheme.toLowerCase(Locale.ROOT);
+ return "donau".equals(lowered) || "donau+http".equals(lowered);
+ }
+
+ private void resetParsedFields() {
+ year = null;
+ totalAmount = null;
+ taxId = null;
+ salt = null;
+ eddsaSignature = null;
+ publicKey = null;
+ }
+
+ private SignatureStatus parseDonauUri(Uri uri) {
+ String host = uri.getHost();
+ if (isEmpty(host)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+
+ List<String> segments = uri.getPathSegments();
+ if (segments == null) {
+ return SignatureStatus.INVALID_NUMBER_OF_ARGUMENTS;
+ }
+
+ if (segments.size() < 3) {
+ return SignatureStatus.INVALID_NUMBER_OF_ARGUMENTS;
+ }
+
+ int lastIndex = segments.size() - 1;
+ String saltCandidate = segments.get(lastIndex);
+ String taxIdCandidate = segments.get(lastIndex - 1);
+ String yearCandidate = segments.get(lastIndex - 2);
+
+ if (yearCandidate != null) {
+ yearCandidate = yearCandidate.trim();
+ }
+ if (!isFourDigitYear(yearCandidate)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+
+ year = yearCandidate;
+
+ if (taxIdCandidate != null) {
+ taxIdCandidate = taxIdCandidate.trim();
+ }
+ if (!isValidTaxId(taxIdCandidate)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ taxId = taxIdCandidate;
+
+ if (saltCandidate != null) {
+ saltCandidate = saltCandidate.trim();
+ }
+ if (!isDigitsOnly(saltCandidate)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ salt = saltCandidate;
+
+ String totalParam = uri.getQueryParameter("total");
+ if (isEmpty(totalParam)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ totalAmount = totalParam.trim();
+
+ String sigParam = uri.getQueryParameter("sig");
+ if (isEmpty(sigParam)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ eddsaSignature = extractEd25519Signature(sigParam);
+ if (isEmpty(eddsaSignature)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+
+ //TODO: Remove to follow the lsd0013
+ // we can do it, when we have a donau instance in open web
+ String publicKeyParam = uri.getQueryParameter("pub");
+ if (isEmpty(publicKeyParam)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ publicKey = publicKeyParam.trim();
+
+ return null;
+ }
+
+ private boolean isFourDigitYear(String value) {
+ if (value == null || value.length() != 4) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ if (!Character.isDigit(value.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isEmpty(String value) {
+ return value == null || value.trim().isEmpty();
+ }
+
+ private String extractEd25519Signature(String raw) {
+ if (raw == null) {
+ return null;
+ }
+ String trimmed = raw.trim();
+ int separatorIndex = trimmed.indexOf(':');
+ if (separatorIndex < 0) {
+ separatorIndex = trimmed.indexOf('=');
+ }
+ if (separatorIndex <= 0 || separatorIndex >= trimmed.length() - 1) {
+ return null;
+ }
+ String algorithm = trimmed.substring(0, separatorIndex).trim();
+ if (!"ED25519".equalsIgnoreCase(algorithm)) {
+ return null;
+ }
+ String signature = trimmed.substring(separatorIndex + 1).trim();
+ if (signature.isEmpty()) {
+ return null;
+ }
+ return signature;
+ }
+
+ private boolean isDigitsOnly(String value) {
+ if (isEmpty(value)) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ if (!Character.isDigit(value.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isValidTaxId(String value) {
+ if (isEmpty(value)) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char ch = value.charAt(i);
+ if (!(Character.isLetterOrDigit(ch) || ch == '-' || ch == '.')) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/donau-verificator/src/main/res/values/colors.xml b/donau-verificator/src/main/res/values/colors.xml
@@ -9,4 +9,10 @@
<color name="white">#FFFFFFFF</color>
<color name="red">#870C0C</color>
<color name="green">#045F06</color>
-</resources>
-\ No newline at end of file
+ <color name="validation_surface_error">#FFF3E0</color>
+ <color name="validation_surface_success">#E8F5E9</color>
+ <color name="validation_surface_info">#E3F2FD</color>
+ <color name="validation_surface_neutral">#F5F5F5</color>
+ <color name="text_primary">#212121</color>
+ <color name="text_secondary">#616161</color>
+</resources>
diff --git a/donau-verificator/src/main/res/values/dimens.xml b/donau-verificator/src/main/res/values/dimens.xml
@@ -2,4 +2,5 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
-</resources>
-\ No newline at end of file
+ <dimen name="validation_item_spacing">12dp</dimen>
+</resources>
diff --git a/donau-verificator/src/main/res/values/strings.xml b/donau-verificator/src/main/res/values/strings.xml
@@ -7,4 +7,5 @@
<string name="invalid_signature">Donation statement signature is invalid!</string>
<string name="valid_signature">Donation statement signature is valid!</string>
<string name="invalid_scheme">Invalid scheme!</string>
-</resources>
-\ No newline at end of file
+ <string name="content_description_validation_icon">Validation status indicator</string>
+</resources>