taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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:
Mdonau-verificator/README.md | 5++---
Mdonau-verificator/src/main/java/net/taler/donauverificator/MainActivity.java | 9+++++----
Mdonau-verificator/src/main/java/net/taler/donauverificator/Results.java | 212++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mdonau-verificator/src/main/res/values/colors.xml | 9+++++++--
Mdonau-verificator/src/main/res/values/dimens.xml | 4++--
Mdonau-verificator/src/main/res/values/strings.xml | 4++--
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>