commit 50c7c31d3ec59a73c43f86277ba0d5c6a7fff984
parent 4ddf87d438a480f6fef6ed373bea58f97ef19f2b
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:46 +0200
wallet-core: drain paginated mailbox responses
Diffstat:
2 files changed, 109 insertions(+), 74 deletions(-)
diff --git a/packages/taler-wallet-core/src/mailbox.test.ts b/packages/taler-wallet-core/src/mailbox.test.ts
@@ -23,7 +23,10 @@ import {
} from "@gnu-taler/taler-util";
import assert from "node:assert";
import { test } from "node:test";
-import { mailboxMetadataMatchesAddress } from "./mailbox.js";
+import {
+ mailboxBatchMayHaveMoreMessages,
+ mailboxMetadataMatchesAddress,
+} from "./mailbox.js";
test("mailbox metadata is bound to the requested address", () => {
const keys = createEddsaKeyPair();
@@ -45,3 +48,8 @@ test("mailbox metadata is bound to the requested address", () => {
false,
);
});
+
+test("a full mailbox response requires another fetch", () => {
+ assert.strictEqual(mailboxBatchMayHaveMoreMessages(10, 10), true);
+ assert.strictEqual(mailboxBatchMayHaveMoreMessages(9, 10), false);
+});
diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts
@@ -62,6 +62,14 @@ import {
import { WalletExecutionContext } from "./wallet.js";
const logger = new Logger("mailbox.ts");
+export const MAX_MAILBOX_REFRESH_BATCHES = 100;
+
+export function mailboxBatchMayHaveMoreMessages(
+ numMessages: number,
+ responseLimit: number,
+): boolean {
+ return numMessages >= responseLimit;
+}
export function mailboxMetadataMatchesAddress(
mailboxAddress: string,
@@ -294,10 +302,12 @@ export async function refreshMailbox(
);
// Refresh message size
var message_size;
+ var message_response_limit;
const resConf = await mailboxClient.getConfig();
switch (resConf.case) {
case "ok":
message_size = resConf.body.message_body_bytes;
+ message_response_limit = resConf.body.message_response_limit;
break;
default:
throw TalerError.fromDetail(
@@ -306,85 +316,102 @@ export async function refreshMailbox(
"could not read the configuration of the mailbox service",
);
}
- const res = await mailboxClient.getMessages({
- hMailbox: mailboxConf.hAddress,
- });
- switch (res.case) {
- case "ok": {
- const hpkeSk: Uint8Array = decodeCrock(mailboxConf.privateEncryptionKey);
- if (res.body) {
- const messages = res.body.messages;
- const now = TalerProtocolTimestamp.now();
- if (messages.byteLength % message_size !== 0) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl: mailboxConf.mailboxBaseUrl,
- requestMethod: "GET",
- httpStatusCode: HttpStatusCode.Ok,
- },
- `mailbox messages response is not a multiple of the message size (${messages.byteLength} % ${message_size} != 0)`,
- );
- }
- // FIXME: if we have reached the maximum number of
- // messages that the service returns at a time,
- // we probably have to call again until no more messages to
- // download.
- const numMessages = messages.byteLength / message_size;
- const records: MailboxMessageRecord[] = [];
- for (let i = 0; i < numMessages; i++) {
- const offset = i * message_size;
- const msg: Uint8Array = messages.slice(offset, offset + message_size);
- const uri = decryptTalerUriMessage(hpkeSk, msg);
- if (!uri) {
- logger.warn(`unable to decrypt message number ${i}`);
- continue;
- }
- // Find start of padding
- const newMessage = {
- originMailboxBaseUrl: mailboxConf.mailboxBaseUrl,
- talerUri: uri,
- downloadedAt: now,
- };
- records.push(newMessage);
- await addMailboxMessage(wex, { message: newMessage });
- }
- // Message header: 8 byte + 64 byte SHA512 digest to sign
- // We hash all messages
- const messageBuffer = new ArrayBuffer(16);
- const vMsg = new DataView(messageBuffer);
- vMsg.setUint32(0, 4 * 4);
- vMsg.setUint32(4, TalerSignaturePurpose.MAILBOX_MESSAGES_DELETE);
- vMsg.setUint32(8, parseInt(res.body.etag));
- vMsg.setUint32(12, numMessages);
- const msgToSign: Uint8Array = new Uint8Array(messageBuffer);
- const privateSigningKey: Uint8Array = decodeCrock(
- mailboxConf.privateKey,
- );
- const signature = eddsaSign(msgToSign, privateSigningKey);
- succeedOrThrow(
- await mailboxClient.deleteMessages({
- mailboxConf: mailboxConf,
- matchIf: res.body.etag,
- count: numMessages,
- signature: encodeCrock(signature),
- }),
- );
- return {
- messages: records,
- };
- }
- return {
- messages: [],
- }; // No new messages;
- }
- default:
+ if (
+ !Number.isSafeInteger(message_response_limit) ||
+ message_response_limit <= 0
+ ) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: mailboxConf.mailboxBaseUrl,
+ requestMethod: "GET",
+ httpStatusCode: HttpStatusCode.Ok,
+ },
+ "mailbox service advertised an invalid message response limit",
+ );
+ }
+
+ const hpkeSk: Uint8Array = decodeCrock(mailboxConf.privateEncryptionKey);
+ const privateSigningKey: Uint8Array = decodeCrock(mailboxConf.privateKey);
+ const records: MailboxMessageRecord[] = [];
+ for (
+ let batchIndex = 0;
+ batchIndex < MAX_MAILBOX_REFRESH_BATCHES;
+ batchIndex++
+ ) {
+ const res = await mailboxClient.getMessages({
+ hMailbox: mailboxConf.hAddress,
+ });
+ if (res.case !== "ok") {
throw TalerError.fromDetail(
TalerErrorCode.WALLET_MAILBOX_UNAVAILABLE,
{ mailboxBaseUrl: mailboxConf.mailboxBaseUrl },
"the mailbox service did not return the messages",
);
+ }
+
+ const messages = res.body.messages;
+ const now = TalerProtocolTimestamp.now();
+ if (messages.byteLength % message_size !== 0) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: mailboxConf.mailboxBaseUrl,
+ requestMethod: "GET",
+ httpStatusCode: HttpStatusCode.Ok,
+ },
+ `mailbox messages response is not a multiple of the message size (${messages.byteLength} % ${message_size} != 0)`,
+ );
+ }
+ const numMessages = messages.byteLength / message_size;
+ if (numMessages === 0) {
+ return { messages: records };
+ }
+
+ for (let i = 0; i < numMessages; i++) {
+ const offset = i * message_size;
+ const msg: Uint8Array = messages.slice(offset, offset + message_size);
+ const uri = decryptTalerUriMessage(hpkeSk, msg);
+ if (!uri) {
+ logger.warn(`unable to decrypt message number ${i}`);
+ continue;
+ }
+ const newMessage = {
+ originMailboxBaseUrl: mailboxConf.mailboxBaseUrl,
+ talerUri: uri,
+ downloadedAt: now,
+ };
+ records.push(newMessage);
+ await addMailboxMessage(wex, { message: newMessage });
+ }
+
+ const messageBuffer = new ArrayBuffer(16);
+ const vMsg = new DataView(messageBuffer);
+ vMsg.setUint32(0, 4 * 4);
+ vMsg.setUint32(4, TalerSignaturePurpose.MAILBOX_MESSAGES_DELETE);
+ vMsg.setUint32(8, parseInt(res.body.etag));
+ vMsg.setUint32(12, numMessages);
+ const signature = eddsaSign(
+ new Uint8Array(messageBuffer),
+ privateSigningKey,
+ );
+ succeedOrThrow(
+ await mailboxClient.deleteMessages({
+ mailboxConf,
+ matchIf: res.body.etag,
+ count: numMessages,
+ signature: encodeCrock(signature),
+ }),
+ );
+
+ if (!mailboxBatchMayHaveMoreMessages(numMessages, message_response_limit)) {
+ return { messages: records };
+ }
}
+ logger.warn(
+ `mailbox refresh stopped after ${MAX_MAILBOX_REFRESH_BATCHES} full batches`,
+ );
+ return { messages: records };
}
function encryptTalerUriMessage(