commit 33b1ea8d0e5159d4d9b2ffac016d72e2daa417fc
parent 8b89769ed6216a24590eaa0482abd0652cab68f2
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:48 +0200
wallet-core: test private-data log redaction
Diffstat:
2 files changed, 152 insertions(+), 2 deletions(-)
diff --git a/packages/taler-wallet-core/src/donau.test.ts b/packages/taler-wallet-core/src/donau.test.ts
@@ -13,11 +13,17 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts } from "@gnu-taler/taler-util";
+import {
+ Amounts,
+ encodeCrock,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import { HeadersImpl, HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import assert from "node:assert";
import { test } from "node:test";
import {
CandidateDonationUnit,
+ generateDonauPlanchets,
selectDonationUnitsExact,
} from "./donau.js";
@@ -41,3 +47,96 @@ test("donation units must represent the requested amount exactly", () => {
[two, two],
);
});
+
+test("Donau planchet generation does not log taxpayer or blinding secrets", async () => {
+ const taxId = "DE-SECRET-TAXPAYER-ID";
+ const taxIdHash = encodeCrock(new Uint8Array(64).fill(1));
+ const taxIdSalt = encodeCrock(new Uint8Array(32).fill(2));
+ const purchase = {
+ choiceIndex: 0,
+ donauAmount: "EUR:1",
+ donauBaseUrl: "https://donau.example/",
+ donauOutputIndex: 0,
+ donauTaxId: taxId,
+ donauTaxIdHash: taxIdHash,
+ donauTaxIdSalt: taxIdSalt,
+ donauYear: 2026,
+ proposalId: "proposal",
+ };
+ const http: HttpRequestLibrary = {
+ async fetch(url, opt) {
+ const body = {
+ base_url: "https://donau.example/",
+ currency: "EUR",
+ donation_units: [
+ {
+ donation_unit_pub: {
+ cipher: "RSA",
+ rsa_public_key: "040000W1",
+ },
+ lost: false,
+ value: "EUR:1",
+ year: 2026,
+ },
+ ],
+ signkeys: [],
+ version: "0:0:0",
+ };
+ return {
+ requestMethod: opt?.method ?? "GET",
+ requestUrl: url,
+ status: 200,
+ headers: new HeadersImpl(),
+ async bytes() {
+ return new TextEncoder().encode(JSON.stringify(body));
+ },
+ async json() {
+ return body;
+ },
+ async text() {
+ return JSON.stringify(body);
+ },
+ };
+ },
+ };
+ let blindingKey: string | undefined;
+ const wex = {
+ http,
+ cryptoApi: {
+ async rsaBlind(req: { bks: string }) {
+ blindingKey = req.bks;
+ return { blinded: "blinded-identifier" };
+ },
+ },
+ async runWalletDbTx(fn: (tx: any) => Promise<unknown>) {
+ return await fn({
+ async countDonationPlanchetsByProposal() {
+ return 0;
+ },
+ async getPurchase() {
+ return purchase;
+ },
+ async upsertDonationPlanchet() {},
+ });
+ },
+ };
+
+ const originalWrite = process.stderr.write;
+ let output = "";
+ process.stderr.write = ((chunk: Uint8Array | string) => {
+ output += chunk.toString();
+ return true;
+ }) as typeof process.stderr.write;
+ setGlobalLogLevelFromString("trace");
+ try {
+ await generateDonauPlanchets(wex as any, purchase.proposalId);
+ } finally {
+ process.stderr.write = originalWrite;
+ setGlobalLogLevelFromString("info");
+ }
+
+ assert.ok(blindingKey);
+ for (const secret of [taxId, taxIdHash, taxIdSalt, blindingKey]) {
+ assert.ok(!output.includes(secret), secret);
+ }
+});
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.test.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.test.ts
@@ -1,7 +1,13 @@
import assert from "node:assert/strict";
import test from "node:test";
-import { ScopeType, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
import {
+ ScopeType,
+ TalerProtocolTimestamp,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import { PeerPushDebitStatus } from "./db-common.js";
+import {
+ PeerPushDebitTransactionContext,
decodePeerPushDebitQuote,
encodePeerPushDebitQuote,
} from "./pay-peer-push-debit.js";
@@ -50,3 +56,48 @@ test("peer push debit quote rejects malformed and unknown versions", () => {
),
);
});
+
+test("peer push debit metadata logging excludes private capabilities", async () => {
+ const privateValues = [
+ "contract-private-capability",
+ "merge-private-capability",
+ "purse-private-capability",
+ ];
+ const rec = {
+ amount: "CHF:1",
+ contractPriv: privateValues[0],
+ exchangeBaseUrl: "https://exchange.example/",
+ mergePriv: privateValues[1],
+ pursePriv: privateValues[2],
+ status: PeerPushDebitStatus.PendingReady,
+ timestampCreated: 1,
+ };
+ const tx = {
+ async getPeerPushDebit() {
+ return rec;
+ },
+ async upsertTransactionMeta() {},
+ };
+ const ctx = new PeerPushDebitTransactionContext(
+ undefined as any,
+ "purse-public-key",
+ );
+ const originalWrite = process.stderr.write;
+ let output = "";
+ process.stderr.write = ((chunk: Uint8Array | string) => {
+ output += chunk.toString();
+ return true;
+ }) as typeof process.stderr.write;
+ setGlobalLogLevelFromString("info");
+ try {
+ await ctx.updateTransactionMeta(tx as any);
+ } finally {
+ process.stderr.write = originalWrite;
+ setGlobalLogLevelFromString("info");
+ }
+
+ assert.match(output, /purse-public-key/);
+ for (const privateValue of privateValues) {
+ assert.ok(!output.includes(privateValue), privateValue);
+ }
+});