commit 2dcc36867561729c7b8bad2ead694ab0931d5cfe
parent d41c9b30baa545b352a8e2ff68fdf00a01421e6d
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Wed, 8 Oct 2025 00:48:16 +0200
[donau-verificator] small fix v2
Diffstat:
5 files changed, 434 insertions(+), 123 deletions(-)
diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/Results.java b/donau-verificator/src/main/java/net/taler/donauverificator/Results.java
@@ -24,7 +24,6 @@ import android.util.Log;
import android.view.View;
import android.widget.TextView;
-
import androidx.annotation.ColorRes;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
@@ -35,22 +34,21 @@ import com.google.android.material.button.MaterialButton;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import org.json.JSONArray;
+import net.taler.donauverificator.network.CrockfordBase32;
+import net.taler.donauverificator.network.DonauNetworkClient;
+import net.taler.donauverificator.network.DonauNetworkClient.DonationStatement;
+import net.taler.donauverificator.network.DonauNetworkClient.HttpStatusException;
+
import org.json.JSONException;
-import org.json.JSONObject;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
-import java.util.zip.GZIPInputStream;
-import java.util.zip.InflaterInputStream;
public class Results extends AppCompatActivity {
static {
@@ -61,7 +59,6 @@ public class Results extends AppCompatActivity {
// lsd0013 format: donau://host/year/taxid/salt?total=...&sig=ED25519:...
// CrockfordBase32 encoded: SIGNATURE, PUBLICKEY
- // TODO: Salt and taxId should maybe also be encoded
private String uriScheme;
private String host;
@@ -74,6 +71,7 @@ public class Results extends AppCompatActivity {
private String salt;
private String eddsaSignature;
private String publicKey;
+ private DonauNetworkClient networkClient;
TextView sigStatusView;
View summaryContainer;
TextView hostLabelView;
@@ -95,6 +93,9 @@ public class Results extends AppCompatActivity {
INSECURE_HTTP_DISABLED,
KEY_DOWNLOAD_FAILED,
KEY_NOT_FOUND,
+ DONATION_STATEMENT_DOWNLOAD_FAILED,
+ DONATION_STATEMENT_NOT_FOUND,
+ DONATION_STATEMENT_INVALID,
SIGNATURE_INVALID,
SIGNATURE_VALID;
}
@@ -164,13 +165,17 @@ public class Results extends AppCompatActivity {
private void startVerificationAsync() {
new Thread(() -> {
- SignatureStatus status = ensurePublicKeyAvailable();
+ SignatureStatus statusResult = ensurePublicKeyAvailable();
+ if (statusResult == null) {
+ statusResult = ensureDonationStatementAvailable();
+ }
+ SignatureStatus finalStatus = statusResult;
runOnUiThread(() -> {
if (isFinishing() || isDestroyed()) {
return;
}
- if (status != null) {
- statusHandling(status);
+ if (finalStatus != null) {
+ statusHandling(finalStatus);
return;
}
try {
@@ -206,6 +211,15 @@ public class Results extends AppCompatActivity {
case KEY_NOT_FOUND:
updateStatusCard(R.string.status_key_not_found, R.color.validation_surface_info, R.color.colorSecondary, false);
break;
+ case DONATION_STATEMENT_DOWNLOAD_FAILED:
+ updateStatusCard(R.string.status_donation_statement_download_failed, R.color.validation_surface_error, R.color.red, false);
+ break;
+ case DONATION_STATEMENT_NOT_FOUND:
+ updateStatusCard(R.string.status_donation_statement_not_found, R.color.validation_surface_info, R.color.colorSecondary, false);
+ break;
+ case DONATION_STATEMENT_INVALID:
+ updateStatusCard(R.string.status_donation_statement_invalid, R.color.validation_surface_error, R.color.red, false);
+ break;
case SIGNATURE_INVALID:
updateStatusCard(R.string.invalid_signature, R.color.validation_surface_error, R.color.red, false);
break;
@@ -297,13 +311,15 @@ public class Results extends AppCompatActivity {
}
private void showSignatureDialog() {
+ String lineBreak = "\n";
+ String message = getString(R.string.signature_info_salt, valueOrUnknown(salt))
+ + lineBreak
+ + getString(R.string.signature_info_signature, valueOrUnknown(eddsaSignature))
+ + lineBreak
+ + getString(R.string.signature_info_public_key, valueOrUnknown(publicKey));
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.signature_info_title)
- .setMessage(getString(
- R.string.signature_info_message,
- valueOrUnknown(salt),
- valueOrUnknown(eddsaSignature),
- valueOrUnknown(publicKey)))
+ .setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();
}
@@ -345,6 +361,7 @@ public class Results extends AppCompatActivity {
salt = null;
eddsaSignature = null;
publicKey = null;
+ networkClient = null;
}
private SignatureStatus parseDonauUri(Uri uri) {
@@ -407,23 +424,31 @@ public class Results extends AppCompatActivity {
salt = saltCandidate;
String totalParam = uri.getQueryParameter("total");
- if (isEmpty(totalParam)) {
- return SignatureStatus.MALFORMED_ARGUMENT;
+ if (totalParam != null) {
+ String trimmedTotal = totalParam.trim();
+ if (trimmedTotal.isEmpty()) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ totalAmount = trimmedTotal;
+ } else {
+ totalAmount = null;
}
- 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;
+ if (sigParam != null) {
+ eddsaSignature = extractEd25519Signature(sigParam);
+ if (isEmpty(eddsaSignature)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
+ }
+ } else {
+ eddsaSignature = null;
}
String publicKeyParam = uri.getQueryParameter("pub");
publicKey = isEmpty(publicKeyParam) ? null : publicKeyParam.trim();
+ networkClient = new DonauNetworkClient(isInsecureScheme(), host, port, authorityPathSegments);
+
return null;
}
@@ -447,116 +472,126 @@ public class Results extends AppCompatActivity {
}
try {
- String fetchedKey = fetchSigningKey(insecureScheme);
+ DonauNetworkClient client = getNetworkClient();
+ int targetYear = parseYearOrDefault(year);
+ String fetchedKey = client.fetchSigningKey(targetYear);
if (isEmpty(fetchedKey)) {
return SignatureStatus.KEY_NOT_FOUND;
}
publicKey = fetchedKey;
return null;
+ } catch (HttpStatusException e) {
+ Log.e(TAG, "Failed to download Donau signing keys, HTTP " + e.getStatusCode(), e);
+ return SignatureStatus.KEY_DOWNLOAD_FAILED;
} catch (IOException | JSONException e) {
Log.e(TAG, "Failed to download Donau signing keys", e);
return SignatureStatus.KEY_DOWNLOAD_FAILED;
}
}
- private String buildHostDisplay() {
- if (isEmpty(host)) {
+ private SignatureStatus ensureDonationStatementAvailable() {
+ boolean needsTotal = isEmpty(totalAmount);
+ boolean needsSignature = isEmpty(eddsaSignature);
+ if (!needsTotal && !needsSignature) {
return null;
}
- StringBuilder builder = new StringBuilder(host);
- if (port != -1) {
- builder.append(":").append(port);
- }
- for (String segment : authorityPathSegments) {
- if (!isEmpty(segment)) {
- builder.append("/").append(segment);
- }
- }
- return builder.toString();
- }
-
- private String fetchSigningKey(boolean insecure) throws IOException, JSONException {
- URL keysUrl = buildKeysUrl(insecure);
- if (keysUrl == null) {
- return null;
+ if (isEmpty(taxId) || isEmpty(salt) || isEmpty(year)) {
+ return SignatureStatus.MALFORMED_ARGUMENT;
}
- HttpURLConnection connection = (HttpURLConnection) keysUrl.openConnection();
- connection.setRequestMethod("GET");
- connection.setConnectTimeout(5000);
- connection.setReadTimeout(5000);
- connection.setInstanceFollowRedirects(false);
- connection.setRequestProperty("Accept", "application/json");
- connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
try {
- int status = connection.getResponseCode();
- if (status != HttpURLConnection.HTTP_OK) {
- throw new IOException("HTTP " + status);
+ String donorHash = computeDonorHash(taxId, salt);
+ DonauNetworkClient client = getNetworkClient();
+ int donationYear = parseYearOrDefault(year);
+ DonationStatement statement = client.fetchDonationStatement(donationYear, donorHash);
+ String statementTotal = statement.getTotal();
+ String statementSignature = statement.getSignature();
+ String statementPublicKey = statement.getPublicKey();
+ if (isEmpty(statementTotal) || isEmpty(statementSignature) || isEmpty(statementPublicKey)) {
+ Log.e(TAG, "Donation statement response missing required fields");
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
}
- InputStream input = connection.getInputStream();
- String encoding = connection.getHeaderField("Content-Encoding");
- if (encoding != null) {
- if ("gzip".equalsIgnoreCase(encoding)) {
- input = new GZIPInputStream(input);
- } else if ("deflate".equalsIgnoreCase(encoding)) {
- input = new InflaterInputStream(input);
- }
+ if (!needsTotal && totalAmount != null && !totalAmount.equals(statementTotal)) {
+ Log.e(TAG, "Donation statement total mismatch");
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
}
- String body;
- try {
- body = readStream(input);
- } finally {
- input.close();
+ if (!isEmpty(publicKey) && !publicKey.equals(statementPublicKey)) {
+ Log.e(TAG, "Donation statement public key mismatch");
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
}
- JSONObject json = new JSONObject(body);
- JSONArray signkeys = json.optJSONArray("signkeys");
- if (signkeys == null) {
- return null;
- }
- for (int i = 0; i < signkeys.length(); i++) {
- JSONObject entry = signkeys.optJSONObject(i);
- if (entry != null) {
- String keyCandidate = entry.optString("key", null);
- if (isEmpty(keyCandidate) && entry.has("key")) {
- JSONObject keyObj = entry.optJSONObject("key");
- if (keyObj != null) {
- keyCandidate = keyObj.optString("eddsa_pub", null);
- }
- }
- if (!isEmpty(keyCandidate)) {
- return keyCandidate.trim();
- }
- } else {
- String keyCandidate = signkeys.optString(i, null);
- if (!isEmpty(keyCandidate)) {
- return keyCandidate.trim();
- }
+ String extractedSignature = extractEd25519Signature(statementSignature);
+ String normalizedSignature;
+ if (extractedSignature != null) {
+ normalizedSignature = extractedSignature;
+ } else {
+ if (statementSignature.contains(":") || statementSignature.contains("=")) {
+ Log.e(TAG, "Donation statement signature format invalid");
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
}
+ normalizedSignature = statementSignature.trim();
}
+ if (!needsSignature && eddsaSignature != null && !eddsaSignature.equals(normalizedSignature)) {
+ Log.e(TAG, "Donation statement signature mismatch");
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
+ }
+ totalAmount = statementTotal;
+ eddsaSignature = normalizedSignature;
+ publicKey = statementPublicKey;
return null;
- } finally {
- connection.disconnect();
+ } catch (HttpStatusException e) {
+ if (e.getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) {
+ return SignatureStatus.DONATION_STATEMENT_NOT_FOUND;
+ }
+ Log.e(TAG, "Donation statement download failed, HTTP " + e.getStatusCode(), e);
+ return SignatureStatus.DONATION_STATEMENT_DOWNLOAD_FAILED;
+ } catch (IOException | JSONException e) {
+ Log.e(TAG, "Donation statement download failed", e);
+ return SignatureStatus.DONATION_STATEMENT_DOWNLOAD_FAILED;
+ } catch (NoSuchAlgorithmException e) {
+ Log.e(TAG, "Unable to hash donor identifier", e);
+ return SignatureStatus.DONATION_STATEMENT_INVALID;
+ }
+ }
+
+ private DonauNetworkClient getNetworkClient() {
+ if (networkClient == null) {
+ networkClient = new DonauNetworkClient(isInsecureScheme(), host, port, authorityPathSegments);
}
+ return networkClient;
+ }
+
+ private int parseYearOrDefault(String value) {
+ if (isEmpty(value)) {
+ return Integer.MIN_VALUE;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return Integer.MIN_VALUE;
+ }
+ }
+
+ private String computeDonorHash(String taxIdValue, String saltValue) throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ digest.update(taxIdValue.getBytes(StandardCharsets.UTF_8));
+ digest.update(saltValue.getBytes(StandardCharsets.UTF_8));
+ byte[] hash = digest.digest();
+ return CrockfordBase32.encode(hash);
}
- private URL buildKeysUrl(boolean insecure) {
+ private String buildHostDisplay() {
if (isEmpty(host)) {
return null;
}
- try {
- Uri.Builder builder = new Uri.Builder()
- .scheme(insecure ? "http" : "https")
- .encodedAuthority(port != -1 ? host + ":" + port : host);
- for (String segment : authorityPathSegments) {
- if (!isEmpty(segment)) {
- builder.appendPath(segment.trim());
- }
+ StringBuilder builder = new StringBuilder(host);
+ if (port != -1) {
+ builder.append(":").append(port);
+ }
+ for (String segment : authorityPathSegments) {
+ if (!isEmpty(segment)) {
+ builder.append("/").append(segment);
}
- builder.appendPath("keys");
- return new URL(builder.build().toString());
- } catch (MalformedURLException | IllegalArgumentException e) {
- Log.e(TAG, "Invalid /keys URL", e);
- return null;
}
+ return builder.toString();
}
private boolean isInsecureScheme() {
@@ -576,16 +611,6 @@ public class Results extends AppCompatActivity {
return host != null && host.equalsIgnoreCase("example.com");
}
- private String readStream(InputStream input) throws IOException {
- BufferedReader reader = new BufferedReader(new InputStreamReader(input, java.nio.charset.StandardCharsets.UTF_8));
- StringBuilder builder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- builder.append(line);
- }
- return builder.toString();
- }
-
private boolean isFourDigitYear(String value) {
if (value == null || value.length() != 4) {
return false;
diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/network/CrockfordBase32.java b/donau-verificator/src/main/java/net/taler/donauverificator/network/CrockfordBase32.java
@@ -0,0 +1,36 @@
+package net.taler.donauverificator.network;
+
+/**
+ * Minimal encoder for the Crockford Base32 alphabet as specified for Donau.
+ * Padding is omitted because the Donau specification does not require it.
+ */
+public final class CrockfordBase32 {
+
+ private static final char[] ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ".toCharArray();
+
+ private CrockfordBase32() {
+ }
+
+ public static String encode(byte[] data) {
+ if (data == null || data.length == 0) {
+ return "";
+ }
+ StringBuilder encoded = new StringBuilder((data.length * 8 + 4) / 5);
+ int buffer = 0;
+ int bitsLeft = 0;
+ for (byte value : data) {
+ buffer = (buffer << 8) | (value & 0xFF);
+ bitsLeft += 8;
+ while (bitsLeft >= 5) {
+ int index = (buffer >> (bitsLeft - 5)) & 0x1F;
+ bitsLeft -= 5;
+ encoded.append(ALPHABET[index]);
+ }
+ }
+ if (bitsLeft > 0) {
+ int index = (buffer << (5 - bitsLeft)) & 0x1F;
+ encoded.append(ALPHABET[index]);
+ }
+ return encoded.toString();
+ }
+}
diff --git a/donau-verificator/src/main/java/net/taler/donauverificator/network/DonauNetworkClient.java b/donau-verificator/src/main/java/net/taler/donauverificator/network/DonauNetworkClient.java
@@ -0,0 +1,247 @@
+package net.taler.donauverificator.network;
+
+import android.net.Uri;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+public final class DonauNetworkClient {
+
+ private static final String TAG = "DonauNetworkClient";
+ private static final int TIMEOUT_MS = 5000;
+
+ private final boolean insecureScheme;
+ private final String host;
+ private final int port;
+ private final List<String> authorityPathSegments;
+
+ public DonauNetworkClient(boolean insecureScheme, String host, int port, List<String> authorityPathSegments) {
+ this.insecureScheme = insecureScheme;
+ this.host = host;
+ this.port = port;
+ if (authorityPathSegments == null || authorityPathSegments.isEmpty()) {
+ this.authorityPathSegments = Collections.emptyList();
+ } else {
+ this.authorityPathSegments = Collections.unmodifiableList(new ArrayList<>(authorityPathSegments));
+ }
+ }
+
+ public String fetchSigningKey(int targetYear) throws IOException, JSONException {
+ URL url = buildUrl("keys");
+ HttpURLConnection connection = openGetConnection(url);
+ connection.setRequestProperty("Accept", "application/json");
+ try {
+ int status = connection.getResponseCode();
+ if (status != HttpURLConnection.HTTP_OK) {
+ throw new HttpStatusException(status, "Unexpected status for /keys: " + status);
+ }
+ String body = readStream(connection);
+ JSONObject json = new JSONObject(body);
+ JSONArray signkeys = json.optJSONArray("signkeys");
+ if (signkeys == null) {
+ return null;
+ }
+ String fallback = null;
+ for (int i = 0; i < signkeys.length(); i++) {
+ Object entry = signkeys.get(i);
+ String keyCandidate = extractSigningKey(entry);
+ if (keyCandidate == null) {
+ continue;
+ }
+ int yearCandidate = extractYear(entry);
+ if (targetYear != Integer.MIN_VALUE && yearCandidate == targetYear) {
+ return keyCandidate;
+ }
+ if (fallback == null) {
+ fallback = keyCandidate;
+ }
+ }
+ return fallback;
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public DonationStatement fetchDonationStatement(int year, String hashedDonorId) throws IOException, JSONException {
+ URL url = buildUrl("donation-statement", String.valueOf(year), hashedDonorId);
+ HttpURLConnection connection = openGetConnection(url);
+ connection.setRequestProperty("Accept", "application/json");
+ try {
+ int status = connection.getResponseCode();
+ if (status != HttpURLConnection.HTTP_OK) {
+ throw new HttpStatusException(status, "Unexpected status for /donation-statement: " + status);
+ }
+ String body = readStream(connection);
+ JSONObject json = new JSONObject(body);
+ String total = trimToNull(json.optString("total", null));
+ String signature = trimToNull(json.optString("donation_statement_sig", null));
+ String pub = trimToNull(json.optString("donau_pub", null));
+ if (total == null || signature == null || pub == null) {
+ throw new JSONException("Incomplete donation statement response");
+ }
+ return new DonationStatement(total, signature, pub);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ private HttpURLConnection openGetConnection(URL url) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setConnectTimeout(TIMEOUT_MS);
+ connection.setReadTimeout(TIMEOUT_MS);
+ connection.setInstanceFollowRedirects(false);
+ connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
+ return connection;
+ }
+
+ private URL buildUrl(String... extraSegments) throws MalformedURLException {
+ if (host == null || host.isEmpty()) {
+ throw new MalformedURLException("Missing host");
+ }
+ Uri.Builder builder = new Uri.Builder()
+ .scheme(insecureScheme ? "http" : "https")
+ .encodedAuthority(port != -1 ? host + ":" + port : host);
+ for (String segment : authorityPathSegments) {
+ if (segment != null && !segment.trim().isEmpty()) {
+ builder.appendPath(segment.trim());
+ }
+ }
+ for (String segment : extraSegments) {
+ builder.appendPath(segment);
+ }
+ return new URL(builder.build().toString());
+ }
+
+ private String readStream(HttpURLConnection connection) throws IOException {
+ InputStream input = null;
+ try {
+ input = maybeWrap(connection.getInputStream(), connection.getHeaderField("Content-Encoding"));
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
+ StringBuilder builder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+ return builder.toString();
+ } finally {
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close input stream", e);
+ }
+ }
+ }
+ }
+
+ private InputStream maybeWrap(InputStream input, String encoding) throws IOException {
+ if (encoding == null) {
+ return input;
+ }
+ if ("gzip".equalsIgnoreCase(encoding)) {
+ return new GZIPInputStream(input);
+ }
+ if ("deflate".equalsIgnoreCase(encoding)) {
+ return new InflaterInputStream(input);
+ }
+ return input;
+ }
+
+ private static String extractSigningKey(Object entry) {
+ if (entry instanceof JSONObject) {
+ JSONObject object = (JSONObject) entry;
+ String direct = trimToNull(object.optString("key", null));
+ if (direct != null) {
+ return direct;
+ }
+ JSONObject keyObject = object.optJSONObject("key");
+ if (keyObject != null) {
+ String nested = trimToNull(keyObject.optString("eddsa_pub", null));
+ if (nested != null) {
+ return nested;
+ }
+ }
+ return trimToNull(object.optString("eddsa_pub", null));
+ }
+ if (entry instanceof String) {
+ return trimToNull((String) entry);
+ }
+ return null;
+ }
+
+ private static int extractYear(Object entry) {
+ if (entry instanceof JSONObject) {
+ JSONObject object = (JSONObject) entry;
+ if (object.has("year")) {
+ try {
+ return object.getInt("year");
+ } catch (JSONException e) {
+ return Integer.MIN_VALUE;
+ }
+ }
+ }
+ return Integer.MIN_VALUE;
+ }
+
+ private static String trimToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+
+ public static final class DonationStatement {
+ private final String total;
+ private final String signature;
+ private final String publicKey;
+
+ public DonationStatement(String total, String signature, String publicKey) {
+ this.total = total;
+ this.signature = signature;
+ this.publicKey = publicKey;
+ }
+
+ public String getTotal() {
+ return total;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+ }
+
+ public static final class HttpStatusException extends IOException {
+ private final int statusCode;
+
+ public HttpStatusException(int statusCode, String message) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+ }
+}
diff --git a/donau-verificator/src/main/res/layout/activity_main.xml b/donau-verificator/src/main/res/layout/activity_main.xml
@@ -11,7 +11,7 @@
android:id="@+id/appTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:text="@string/title_donau_verifier"
+ android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="@color/text_primary"
app:layout_constraintHorizontal_chainStyle="packed"
diff --git a/donau-verificator/src/main/res/values/strings.xml b/donau-verificator/src/main/res/values/strings.xml
@@ -16,6 +16,9 @@
<string name="status_insecure_http_unsupported">This build does not support insecure Donau URIs.</string>
<string name="status_key_download_failed">Unable to fetch Donau signing keys.</string>
<string name="status_key_not_found">Signing key for this donation statement not found.</string>
+ <string name="status_donation_statement_download_failed">Unable to fetch donation statement.</string>
+ <string name="status_donation_statement_not_found">Donation statement not found for this taxpayer and year.</string>
+ <string name="status_donation_statement_invalid">Donation statement response was invalid.</string>
<string name="label_host">Host</string>
<string name="label_year">Year</string>
<string name="label_tax_id">Tax ID</string>
@@ -23,9 +26,9 @@
<string name="action_back">Back</string>
<string name="action_signature_info">Signature info</string>
<string name="signature_info_title">Signature details</string>
- <string name="signature_info_message">Salt: %1$s
-Signature: %2$s
-Public key: %3$s</string>
+ <string name="signature_info_salt">Salt: %1$s</string>
+ <string name="signature_info_signature">Signature: %1$s</string>
+ <string name="signature_info_public_key">Public key: %1$s</string>
<string name="value_unknown">Unknown</string>
<string name="title_donau_verifier">Donau Verifier</string>
</resources>