taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 78e11e7fef48ca7258e0c2d1ca0104cee6f9d0a5
parent 8db0d0b6d4bca8e06026ef77cbd589cf83a1ce9e
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Thu,  6 Nov 2025 15:35:12 +0100

allow deletion of messages (remotely)

Diffstat:
Mpackages/taler-util/src/http-client/mailbox.ts | 25+++++++++++++++++++------
Mpackages/taler-util/src/types-taler-mailbox.ts | 2+-
Mpackages/taler-wallet-core/src/mailbox.ts | 36+++++++++++++++++++++++++++++++-----
3 files changed, 51 insertions(+), 12 deletions(-)

diff --git a/packages/taler-util/src/http-client/mailbox.ts b/packages/taler-util/src/http-client/mailbox.ts @@ -42,6 +42,8 @@ import { sha512, decodeCrock, codecForEmptyObject, + MailboxConfiguration, + eddsaGetPublic, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -55,6 +57,11 @@ export type TalerMailboxInstanceErrorsByMethod< prop extends keyof TalerMailboxInstanceHttpClient, > = FailCasesByMethod<TalerMailboxInstanceHttpClient, prop>; +export interface MailboxMessagesResponseRaw { + messages: Uint8Array; + etag: string; +} + /** * Protocol version spoken with the service. * @@ -160,7 +167,7 @@ export class TalerMailboxInstanceHttpClient { async getMessages(args: { hMailbox: string; }): Promise< - | OperationOk<Uint8Array> + | OperationOk<MailboxMessagesResponseRaw> | OperationOk<void> | OperationFail<HttpStatusCode.TooManyRequests> >{ @@ -175,7 +182,9 @@ export class TalerMailboxInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: { const uintar = await resp.bytes() as Uint8Array; - return opFixedSuccess(uintar); + const etag = resp.headers.get("etag") + const index = etag? etag : "0"; + return opFixedSuccess({messages: uintar, etag: index}); } case HttpStatusCode.NoContent: { return opEmptySuccess(); @@ -192,19 +201,23 @@ export class TalerMailboxInstanceHttpClient { * https://docs.taler.net/core/api-mailbox.html#delete--$ADDRESS */ async deleteMessages(args: { - mailbox: string; + mailboxConf: MailboxConfiguration; + matchIf: string; body: MailboxMessageDeletionRequest; }): Promise< | OperationOk<void> | OperationFail<HttpStatusCode.Forbidden> | OperationFail<HttpStatusCode.NotFound> >{ - const { mailbox, body } = args; - const url = new URL(`${mailbox}`, this.baseUrl); + const { mailboxConf, matchIf: etag, body } = args; + const mailboxPubkeyString = encodeCrock(eddsaGetPublic(decodeCrock(mailboxConf.privateKey))); + const url = new URL(`private/${mailboxPubkeyString.toUpperCase()}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { - method: "DELETE", + method: "POST", body: body, + headers: { "If-Match" : etag }, cancellationToken: this.cancellationToken, }); diff --git a/packages/taler-util/src/types-taler-mailbox.ts b/packages/taler-util/src/types-taler-mailbox.ts @@ -173,6 +173,6 @@ export interface MailboxMessageDeletionRequest { // Signature by the mailbox's private key affirming // the deletion of the messages, of purpuse // TALER_SIGNATURE_WALLET_MAILBOX_DELETE_MESSAGES. - wallet_sig: EddsaSignatureString; + signature: EddsaSignatureString; } diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts @@ -48,6 +48,7 @@ import { eddsaSign, Duration, AbsoluteTime, + eddsaVerify, } from "@gnu-taler/taler-util"; import { WalletExecutionContext, @@ -250,19 +251,20 @@ export async function refreshMailbox( case "ok": const hpkeSk: Uint8Array = decodeCrock(mailboxConf.privateEncryptionKey); if (res.body) { + const messages = res.body.messages; const now = TalerProtocolTimestamp.now(); - if ((res.body.byteLength % message_size) !== 0) { - throw Error(`mailbox messages response not a multiple of message size! (${res.body.byteLength} % ${message_size} != 0)`); + if ((messages.byteLength % message_size) !== 0) { + throw Error(`mailbox messages response not a multiple of 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 = res.body.byteLength / message_size; + const numMessages = messages.byteLength / message_size; const records: MailboxMessageRecord[] = []; for (let i = 0; i < numMessages; i++) { const offset = i * message_size; - const msg: Uint8Array = res.body.slice(offset, + const msg: Uint8Array = messages.slice(offset, offset + message_size); const header = new Uint8Array(msg.slice(0, 4)); const ct = new Uint8Array(msg.slice(4)); @@ -282,9 +284,33 @@ export async function refreshMailbox( records.push(newMessage); await addMailboxMessage(wex, {message: newMessage}); } + // Message header: 8 byte + 64 byte SHA512 digest to sign + // We hash all messages + const messageHeader = new ArrayBuffer(8); + const vMsg = new DataView(messageHeader); + const digest = sha512(new Uint8Array(messages)); + vMsg.setUint32(0, 8 + 64); + // FIXME: Where is gana? + vMsg.setUint32(4, 1223); + const msgToSign: Uint8Array = new Uint8Array([...(new Uint8Array(messageHeader)), ...digest]); + const privateSigningKey: Uint8Array = decodeCrock(mailboxConf.privateKey); + const signature = eddsaSign(msgToSign, privateSigningKey); + const mailboxPubkeyString = encodeCrock(eddsaGetPublic(decodeCrock(mailboxConf.privateKey))); + if (!eddsaVerify(msgToSign, signature, decodeCrock(mailboxPubkeyString))) { + logger.error("Signature invalid!"); + } + succeedOrThrow(await mailboxClient.deleteMessages({ + mailboxConf: mailboxConf, + matchIf: res.body.etag, + body: { + count: numMessages, + checksum: encodeCrock(digest), + signature: encodeCrock(signature), + } + })); return records; } - throw Error("mailbox messages response empty!"); + return []; // No new messages; default: throw Error("unexpected mailbox messages response empty"); }