commit cb54440ec7e53d9e6aef489b053ea497cbfed092
parent 1225aa0deda9ea5a00e45fe129ff62d82674ae5a
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Wed, 5 Nov 2025 23:06:35 +0100
push wip
Diffstat:
7 files changed, 211 insertions(+), 32 deletions(-)
diff --git a/packages/taler-util/src/http-client/mailbox.ts b/packages/taler-util/src/http-client/mailbox.ts
@@ -33,9 +33,14 @@ import {
opKnownHttpFailure,
opUnknownHttpFailure,
TalerMailboxConfigResponse,
- MailboxMessageKeysResponse,
- codecForTalerMailboxMessageKeysResponse,
+ MailboxMessageKeys,
+ codecForTalerMailboxMessageKeys,
opSuccessFromHttp,
+ EmptyObject,
+ MailboxMessageKeysUpdateRequest,
+ encodeCrock,
+ sha512,
+ decodeCrock,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
@@ -218,7 +223,7 @@ export class TalerMailboxInstanceHttpClient {
* https://docs.taler.net/core/api-mailbox.html#key-directory
*/
async getKeys(hMailbox: string) : Promise<
- | OperationOk<MailboxMessageKeysResponse>
+ | OperationOk<MailboxMessageKeys>
| OperationFail<HttpStatusCode.NotFound>
| OperationFail<HttpStatusCode.TooManyRequests>
>{
@@ -232,7 +237,7 @@ export class TalerMailboxInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok: {
return opSuccessFromHttp(resp,
- codecForTalerMailboxMessageKeysResponse());
+ codecForTalerMailboxMessageKeys());
}
case HttpStatusCode.TooManyRequests: {
return opKnownAlternativeHttpFailure(resp, resp.status, codecForTalerMailboxRateLimitedResponse());
@@ -242,4 +247,30 @@ export class TalerMailboxInstanceHttpClient {
}
}
+ /**
+ * https://docs.taler.net/core/api-mailbox.html#post--keys
+ */
+ async updateKeys(req: MailboxMessageKeysUpdateRequest) : Promise<
+ | OperationOk<void>
+ | OperationFail<HttpStatusCode.Forbidden>
+ >{
+ const url = new URL(`keys`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ cancellationToken: this.cancellationToken,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ return opEmptySuccess();
+ }
+ case HttpStatusCode.Forbidden: {
+ return opKnownHttpFailure(resp.status, resp);
+ }
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
}
diff --git a/packages/taler-util/src/types-taler-mailbox.ts b/packages/taler-util/src/types-taler-mailbox.ts
@@ -77,18 +77,40 @@ export interface TalerMailboxConfigResponse {
delivery_period: RelativeTime;
}
-export const codecForTalerMailboxMessageKeysResponse =
- (): Codec<MailboxMessageKeysResponse> =>
-buildCodecForObject<MailboxMessageKeysResponse>()
+export const codecForTalerMailboxMessageKeysUpdateRequest =
+ (): Codec<MailboxMessageKeysUpdateRequest> =>
+buildCodecForObject<MailboxMessageKeysUpdateRequest>()
+ .property("keys", codecForTalerMailboxMessageKeys())
+ .property("signature", codecForString())
+ .build("TalerMailboxApi.MailboxMessageKeysUpdateRequest");
+
+
+export interface MailboxMessageKeysUpdateRequest {
+
+ // Keys to add/update for a mailbox.
+ keys: MailboxMessageKeys;
+
+ // Signature by the mailbox's signing key affirming
+ // the update of keys, of purpuse
+ // TALER_SIGNATURE_WALLET_MAILBOX_UPDATE_KEYS.
+ // The signature is created over the SHA-512 hash
+ // of (encryptionKeyType||encryptionKey||expiration)
+ signature: EddsaSignatureString;
+
+}
+
+export const codecForTalerMailboxMessageKeys =
+ (): Codec<MailboxMessageKeys> =>
+buildCodecForObject<MailboxMessageKeys>()
.property("signingKey", codecForEddsaPublicKey())
.property("signingKeyType", codecForConstString("EdDSA"))
.property("encryptionKey", codecForString())
.property("encryptionKeyType", codecForConstString("X25519"))
.property("expiration", codecForTimestamp)
- .build("TalerMailboxApi.MailboxMessageKeysResponse");
+ .build("TalerMailboxApi.MailboxMessageKeys");
-export interface MailboxMessageKeysResponse {
+export interface MailboxMessageKeys {
// The mailbox signing key.
// Note that $H_MAILBOX == H(singingKey).
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -1375,13 +1375,15 @@ export interface MailboxConfiguration {
mailboxBaseUrl: string;
privateKey: EddsaPrivateKeyString;
privateEncryptionKey: string;
+ expiration: Timestamp;
}
export const codecForMailboxConfiguration = (): Codec<MailboxConfiguration> =>
- buildCodecForObject<MailboxConfiguration>()
+buildCodecForObject<MailboxConfiguration>()
.property("mailboxBaseUrl", codecForString())
.property("privateEncryptionKey", codecForString())
.property("privateKey", codecForEddsaPrivateKey())
+ .property("expiration", codecForTimestamp)
.build("MailboxConfiguration");
diff --git a/packages/taler-wallet-core/src/mailbox.ts b/packages/taler-wallet-core/src/mailbox.ts
@@ -45,12 +45,22 @@ import {
hpkeCreateSecretKey,
SendTalerUriMailboxMessageRequest,
hpkeSealOneshot,
+ TalerError,
+ hpkeSecretKeyGetPublic,
+ MailboxMessageKeys,
+ MailboxMessageKeysUpdateRequest,
+ eddsaSign,
+ Duration,
+ TalerPreciseTimestamp,
+ AbsoluteTime,
} from "@gnu-taler/taler-util";
import {
WalletExecutionContext,
} from "./wallet.js";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import { OperationType } from "./instructedAmountConversion.js";
+import { AsyncDeflate } from "fflate";
+import { timestampPreciseToDb } from "./db.js";
const logger = new Logger("mailbox.ts");
@@ -124,13 +134,66 @@ export async function listMailboxMessages(
}
/**
- * Get and optionally create mailbox key from the database.
+ * Update mailbox keys
+ * Does not actually update key material in
+ * mailbox configuration, only updates service
+ * with new key info
*/
-export async function initMailbox(
+export async function updateMailboxKeys(
+ wex: WalletExecutionContext,
+ mailboxConf: MailboxConfiguration,
+): Promise<void> {
+ const signingKey = eddsaGetPublic(decodeCrock(mailboxConf.privateKey));
+ const encryptionKey = hpkeSecretKeyGetPublic(decodeCrock(mailboxConf.privateEncryptionKey));
+ // Message header: 8 byte + 64 byte SHA512 digest to sign
+ // We hash ktype|key|encKtype|encKey|expiration
+ const messageHeader = new ArrayBuffer(8);
+ const expNboBuffer = new ArrayBuffer(8);
+ const vMsg = new DataView(messageHeader);
+ const vExpNbo = new DataView(expNboBuffer);
+ if (mailboxConf.expiration.t_s == "never") {
+ throw Error("mailbox can not expire, invalid")
+ }
+ vExpNbo.setBigUint64(0, BigInt(mailboxConf.expiration.t_s));
+ const digestBuffer = new Uint8Array([...stringToBytes("EdDSA"),
+ ...signingKey,
+ ...stringToBytes("X25519"),
+ ...encryptionKey,
+ ...(new Uint8Array(expNboBuffer))]);
+ vMsg.setUint32(0, vMsg.byteLength);
+ // FIXME: Where is gana?
+ vMsg.setUint32(4, 1224);
+ const msgToSign = new Uint8Array([...(new Uint8Array(messageHeader)), ...sha512(digestBuffer)]);
+ // FIXME sign
+ const signature = eddsaSign(msgToSign, signingKey)
+ const keys: MailboxMessageKeys = {
+ signingKey: encodeCrock(signingKey),
+ signingKeyType: "EdDSA", // FIXME only supported key type ATM,
+ encryptionKey: encodeCrock(encryptionKey),
+ encryptionKeyType: "X25519", // FIXME only supported encryption key type ATM
+ expiration: mailboxConf.expiration,
+ };
+ const req: MailboxMessageKeysUpdateRequest = {
+ keys: keys,
+ signature: encodeCrock(signature),
+ };
+ const mailboxClient = new TalerMailboxInstanceHttpClient(mailboxConf.mailboxBaseUrl, wex.http);
+
+ const resp = await mailboxClient.updateKeys(req);
+ if (resp.case != "ok") {
+ throw Error("failed to update mailbox keys");
+ }
+ return;
+}
+
+/**
+ * Get mailbox from the database.
+ */
+export async function getMailbox(
wex: WalletExecutionContext,
mailboxBaseUrl: string,
-): Promise<MailboxConfiguration> {
- const res = await wex.db.runReadOnlyTx(
+): Promise<MailboxConfiguration | undefined> {
+ return await wex.db.runReadOnlyTx(
{
storeNames: [
"mailboxConfigurations",
@@ -140,17 +203,31 @@ export async function initMailbox(
return await tx.mailboxConfigurations.get(mailboxBaseUrl);
},
);
- if (res) {
- return res;
- }
+}
+
+
+/**
+ * Initialize mailbox.
+ */
+export async function initMailbox(
+ wex: WalletExecutionContext,
+ mailboxBaseUrl: string,
+): Promise<MailboxConfiguration> {
const keys = createEddsaKeyPair();
const hpkeKey = encodeCrock(hpkeCreateSecretKey());
const privKey = encodeCrock(keys.eddsaPriv);
+ const nowInAYear = AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({
+ years: 1}));
const mailboxConf: MailboxConfiguration = {
mailboxBaseUrl: mailboxBaseUrl,
privateKey: privKey,
privateEncryptionKey: hpkeKey,
+ expiration: AbsoluteTime.toProtocolTimestamp(nowInAYear)
};
+ logger.error(mailboxBaseUrl);
+ logger.error("before update keys");
+ await updateMailboxKeys(wex, mailboxConf);
+ logger.error("after update keys");
await wex.db.runReadWriteTx(
{
storeNames: ["mailboxConfigurations"],
@@ -181,18 +258,21 @@ export async function refreshMailbox(
message_size = resConf.body.message_body_bytes;
break;
default:
- return [];
+ throw Error("unable to get mailbox service config");
}
const res = await mailboxClient.getMessages({hMailbox: hAddress});
switch (res.case) {
case "ok":
- // FIXME this only parses one message
const hpkeSk = decodeCrock(mailboxConf.privateEncryptionKey);
if (res.body) {
const now = TalerProtocolTimestamp.now();
if ((res.body.length % message_size) !== 0) {
- return []; //FIXME error
+ throw Error("mailbox messages response not a multiple of message size!");
}
+ // 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.length / message_size;
const records: MailboxMessageRecord[] = [];
for (let i = 0; i < numMessages; i++) {
@@ -214,9 +294,9 @@ export async function refreshMailbox(
}
return records;
}
- return [];
+ throw Error("mailbox messages response empty!");
default:
- return [];
+ throw Error("unexpected mailbox messages response empty");
}
}
@@ -241,7 +321,7 @@ export async function sendTalerUriMessage(
keys = resKeys.body;
break;
default:
- return {};
+ throw Error("unable to get mailbox keys");
}
const headerBuf = new ArrayBuffer(4);
const paddingLength = message_size - req.talerUri.length - headerBuf.byteLength;
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -251,6 +251,7 @@ export enum WalletApiOperation {
DeleteContact = "deleteContact",
GetContacts = "getContacts",
GetMailbox = "getMailbox",
+ InitializeMailbox = "initializeMailbox",
GetMailboxMessages = "getMailboxMessage",
AddMailboxMessage = "addMailboxMessage",
DeleteMailboxMessage = "deleteMailboxMessage",
@@ -486,10 +487,19 @@ export type GetContactsOp = {
/**
* Initialize messages mailbox Op.
*/
+export type InitializeMailboxOp = {
+ op: WalletApiOperation.InitializeMailbox;
+ request: string;
+ response: MailboxConfiguration;
+};
+
+/**
+ * Get messages mailbox Op.
+ */
export type GetMailboxOp = {
op: WalletApiOperation.GetMailbox;
request: string;
- response: MailboxConfiguration;
+ response: MailboxConfiguration | undefined;
};
@@ -1713,6 +1723,7 @@ export type WalletOperations = {
[WalletApiOperation.DeleteMailboxMessage]: DeleteMailboxMessageOp;
[WalletApiOperation.AddMailboxMessage]: AddMailboxMessageOp;
[WalletApiOperation.GetMailbox]: GetMailboxOp;
+ [WalletApiOperation.InitializeMailbox]: InitializeMailboxOp;
[WalletApiOperation.SendTalerUriMailboxMessage]: SendTalerUriMailboxMessageOp;
};
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -324,7 +324,8 @@ import {
addMailboxMessage,
deleteMailboxMessage,
initMailbox,
- sendTalerUriMessage
+ sendTalerUriMessage,
+ getMailbox
} from "./mailbox.js";
import {
ReadyExchangeSummary,
@@ -1930,10 +1931,14 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = {
codec: codecForEmptyObject(),
handler: listContacts,
},
- [WalletApiOperation.GetMailbox]: {
+ [WalletApiOperation.InitializeMailbox]: {
codec: codecForString(),
handler: initMailbox,
},
+ [WalletApiOperation.GetMailbox]: {
+ codec: codecForString(),
+ handler: getMailbox,
+ },
[WalletApiOperation.SendTalerUriMailboxMessage]: {
codec: codecForSendTalerUriMailboxMessageRequest(),
handler: sendTalerUriMessage,
diff --git a/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx b/packages/taler-wallet-webextension/src/wallet/Mailbox.tsx
@@ -61,7 +61,7 @@ export function MailboxPage({
const api = useBackendContext();
const [search, setSearch] = useState<string>();
// FIXME get from settings;
- const mailboxBaseUrl = "http://localhost:11000";
+ const mailboxBaseUrl = "https://mailbox.gnunet.org";
var mailboxClient: TalerMailboxInstanceHttpClient;
var activeMailbox: MailboxConfiguration | undefined;
const httpClient = new BrowserFetchHttpLib();
@@ -71,10 +71,13 @@ export function MailboxPage({
const b = await api.wallet.call(WalletApiOperation.GetMailboxMessages, {});
activeMailbox = await api.wallet.call(WalletApiOperation.GetMailbox, mailboxBaseUrl);
+ var mailboxUrl;
+ if (activeMailbox) {
// FIXME put this into the mailbox config directly?
- const privKey = decodeCrock(activeMailbox.privateKey);
- const pubKey = eddsaGetPublic(privKey);
- const mailboxUrl = activeMailbox.mailboxBaseUrl + '/' + encodeCrock(sha512(pubKey));
+ const privKey = decodeCrock(activeMailbox.privateKey);
+ const pubKey = eddsaGetPublic(privKey);
+ mailboxUrl = activeMailbox.mailboxBaseUrl + '/' + encodeCrock(sha512(pubKey));
+ }
return {
messages: b.messages,
mailbox: activeMailbox,
@@ -106,6 +109,10 @@ export function MailboxPage({
);
}
+ const onInitializeMailbox = async () => {
+ await api.wallet.call(WalletApiOperation.InitializeMailbox, mailboxBaseUrl).then();
+ state?.retry();
+ };
const onDeleteMessage = async (m: MailboxMessageRecord) => {
api.wallet.call(WalletApiOperation.DeleteMailboxMessage, { message: m }).then();
};
@@ -133,6 +140,7 @@ export function MailboxPage({
mailboxUrl={state.response.mailboxUrl}
onFetchMessages={onFetchMessages}
onDeleteMessage={onDeleteMessage}
+ onInitializeMailbox={onInitializeMailbox}
/>
);
}
@@ -167,20 +175,25 @@ export function MessagesView({
mailboxUrl,
onFetchMessages,
onDeleteMessage,
+ onInitializeMailbox,
}: {
search: TextFieldHandler;
messages: MailboxMessageRecord[];
- mailboxUrl: string;
+ mailboxUrl: string | undefined;
onFetchMessages: (url: string) => Promise<void>;
onDeleteMessage: (m: MailboxMessageRecord) => Promise<void>;
+ onInitializeMailbox: () => Promise<void>;
}): VNode {
const { i18n } = useTranslationContext();
async function copy(): Promise<void> {
- navigator.clipboard.writeText(mailboxUrl);
+ if (mailboxUrl) {
+ navigator.clipboard.writeText(mailboxUrl);
+ }
}
return (
<Fragment>
<section>
+ {mailboxUrl ? (
<Centered style={{ margin: 10 }}>
<p>
<SmallText style={{ marginTop: 5 }}>
@@ -196,6 +209,21 @@ export function MessagesView({
<i18n.Translate>Fetch messages</i18n.Translate>
</Button>
</Centered>
+ ) : (
+ <Centered style={{ margin: 10 }}>
+ <p>
+ <SmallText style={{ marginTop: 5 }}>
+ <LightText>
+ <i18n.Translate>Mailbox not initialized</i18n.Translate>
+ </LightText>
+ </SmallText>
+ </p>
+ <Button variant="contained" color="error" onClick={() => { return onInitializeMailbox() }}>
+ <i18n.Translate>Initialize mailbox</i18n.Translate>
+ </Button>
+ </Centered>
+
+ )}
{(messages.length == 0) ? (
<Centered style={{ marginTop: 20 }}>
<BoldLight>