aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-10-15 16:03:48 +0200
committerFlorian Dold <florian@dold.me>2022-10-15 16:03:48 +0200
commita41d1ee53e1dc6af0b54f085053278e039cda8dc (patch)
tree5b6200a0cfa067c0d27743b9c6c94ceaa2efcd10
parentbd88dcebbcf90414c790a86ee13740eaf20e3334 (diff)
wallet-core: put signing keys in separate object store
-rw-r--r--packages/idb-bridge/src/MemoryBackend.test.ts40
-rw-r--r--packages/taler-wallet-core/src/db.ts49
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts23
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts30
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts90
-rw-r--r--packages/taler-wallet-core/src/util/query.ts1
6 files changed, 175 insertions, 58 deletions
diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts b/packages/idb-bridge/src/MemoryBackend.test.ts
index b36143aa2..8a544a201 100644
--- a/packages/idb-bridge/src/MemoryBackend.test.ts
+++ b/packages/idb-bridge/src/MemoryBackend.test.ts
@@ -26,6 +26,7 @@ import {
import {
IDBCursorDirection,
IDBCursorWithValue,
+ IDBDatabase,
IDBKeyRange,
IDBValidKey,
} from "./idbtypes.js";
@@ -439,6 +440,45 @@ test("update with non-existent index values", async (t) => {
t.pass();
});
+test("delete from unique index", async (t) => {
+ const backend = new MemoryBackend();
+ backend.enableTracing = true;
+ const idb = new BridgeIDBFactory(backend);
+ const request = idb.open("mydb");
+ request.onupgradeneeded = () => {
+ const db = request.result as IDBDatabase;
+ const store = db.createObjectStore("bla", { keyPath: "x" });
+ store.createIndex("by_yz", ["y", "z"], {
+ unique: true,
+ });
+ };
+
+ const db: BridgeIDBDatabase = await promiseFromRequest(request);
+
+ t.is(db.name, "mydb");
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a", z: 42 });
+ const index = store.index("by_yz");
+ const indRes = await promiseFromRequest(index.get(["a", 42]));
+ t.is(indRes.x, 0);
+ const res = await promiseFromRequest(store.get(0));
+ t.is(res.z, 42);
+ await promiseFromTransaction(tx);
+ }
+
+ {
+ const tx = db.transaction("bla", "readwrite");
+ const store = tx.objectStore("bla");
+ store.put({ x: 0, y: "a", z: 42, extra: 123 });
+ await promiseFromTransaction(tx);
+ }
+
+ t.pass();
+});
+
test("range queries", async (t) => {
const backend = new MemoryBackend();
backend.enableTracing = true;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index b785efed8..dd21aa037 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -51,6 +51,8 @@ import {
TransactionIdStr,
CoinRefreshRequest,
CoinStatus,
+ EddsaPublicKeyString,
+ EddsaSignatureString,
} from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@@ -409,11 +411,26 @@ export namespace DenominationRecord {
}
}
+export interface ExchangeSignkeysRecord {
+ stampStart: TalerProtocolTimestamp;
+ stampExpire: TalerProtocolTimestamp;
+ stampEnd: TalerProtocolTimestamp;
+ signkeyPub: EddsaPublicKeyString;
+ masterSig: EddsaSignatureString;
+
+ /**
+ * Exchange details that thiis signkeys record belongs to.
+ */
+ exchangeDetailsRowId: number;
+}
+
/**
* Exchange details for a particular
* (exchangeBaseUrl, masterPublicKey, currency) tuple.
*/
export interface ExchangeDetailsRecord {
+ rowId?: number;
+
/**
* Master public key of the exchange.
*/
@@ -446,14 +463,6 @@ export interface ExchangeDetailsRecord {
globalFees: ExchangeGlobalFees[];
/**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- *
- * FIXME: Should this be put into a separate object store?
- */
- signingKeys: ExchangeSignKeyJson[];
-
- /**
* Etag of the current ToS of the exchange.
*/
tosCurrentEtag: string;
@@ -1892,9 +1901,29 @@ export const WalletStoresV1 = {
exchangeDetails: describeStore(
"exchangeDetails",
describeContents<ExchangeDetailsRecord>({
- keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"],
+ keyPath: "rowId",
+ autoIncrement: true,
}),
- {},
+ {
+ byPointer: describeIndex(
+ "byDetailsPointer",
+ ["exchangeBaseUrl", "currency", "masterPublicKey"],
+ {
+ unique: true,
+ },
+ ),
+ },
+ ),
+ exchangeSignkeys: describeStore(
+ "exchangeSignKeys",
+ describeContents<ExchangeSignkeysRecord>({
+ keyPath: ["exchangeDetailsRowId", "signkeyPub"],
+ }),
+ {
+ byExchangeDetailsRowId: describeIndex("byExchangeDetailsRowId", [
+ "exchangeDetailsRowId",
+ ]),
+ },
),
refreshGroups: describeStore(
"refreshGroups",
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index 1472b1b90..b0f1d6ce1 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -35,6 +35,7 @@ import {
BackupDenomination,
BackupExchange,
BackupExchangeDetails,
+ BackupExchangeSignKey,
BackupExchangeWireFee,
BackupOperationStatus,
BackupPayInfo,
@@ -74,6 +75,7 @@ import {
} from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
+import { checkDbInvariant } from "../../util/invariants.js";
import { getWalletBackupState, provideBackupState } from "./state.js";
const logger = new Logger("backup/export.ts");
@@ -87,6 +89,7 @@ export async function exportBackup(
x.config,
x.exchanges,
x.exchangeDetails,
+ x.exchangeSignkeys,
x.coins,
x.contractTerms,
x.denominations,
@@ -324,6 +327,18 @@ export async function exportBackup(
});
}
});
+ checkDbInvariant(ex.rowId != null);
+ const exchangeSk =
+ await tx.exchangeSignKeys.indexes.byExchangeDetailsRowId.getAll(
+ ex.rowId,
+ );
+ let signingKeys: BackupExchangeSignKey[] = exchangeSk.map((x) => ({
+ key: x.signkeyPub,
+ master_sig: x.masterSig,
+ stamp_end: x.stampEnd,
+ stamp_expire: x.stampExpire,
+ stamp_start: x.stampStart,
+ }));
backupExchangeDetails.push({
base_url: ex.exchangeBaseUrl,
@@ -341,13 +356,7 @@ export async function exportBackup(
currency: ex.currency,
protocol_version: ex.protocolVersionRange,
wire_fees: wireFees,
- signing_keys: ex.signingKeys.map((x) => ({
- key: x.key,
- master_sig: x.master_sig,
- stamp_end: x.stamp_end,
- stamp_expire: x.stamp_expire,
- stamp_start: x.stamp_start,
- })),
+ signing_keys: signingKeys,
global_fees: ex.globalFees.map((x) => ({
accountFee: Amounts.stringify(x.accountFee),
historyFee: Amounts.stringify(x.historyFee),
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 9c5eea9af..f08d152a5 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -62,7 +62,12 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import { makeCoinAvailable, makeTombstoneId, makeTransactionId, TombstoneTag } from "../common.js";
+import {
+ makeCoinAvailable,
+ makeTombstoneId,
+ makeTransactionId,
+ TombstoneTag,
+} from "../common.js";
import { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js";
@@ -360,11 +365,12 @@ export async function importBackup(
}
for (const backupExchangeDetails of backupBlob.exchange_details) {
- const existingExchangeDetails = await tx.exchangeDetails.get([
- backupExchangeDetails.base_url,
- backupExchangeDetails.currency,
- backupExchangeDetails.master_public_key,
- ]);
+ const existingExchangeDetails =
+ await tx.exchangeDetails.indexes.byPointer.get([
+ backupExchangeDetails.base_url,
+ backupExchangeDetails.currency,
+ backupExchangeDetails.master_public_key,
+ ]);
if (!existingExchangeDetails) {
const wireInfo: WireInfo = {
@@ -422,13 +428,6 @@ export async function importBackup(
purseTimeout: x.purseTimeout,
startDate: x.startDate,
})),
- signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
- key: x.key,
- master_sig: x.master_sig,
- stamp_end: x.stamp_end,
- stamp_expire: x.stamp_expire,
- stamp_start: x.stamp_start,
- })),
});
}
@@ -789,7 +788,10 @@ export async function importBackup(
}
for (const backupTip of backupBlob.tips) {
- const ts = makeTombstoneId(TombstoneTag.DeleteTip, backupTip.wallet_tip_id);
+ const ts = makeTombstoneId(
+ TombstoneTag.DeleteTip,
+ backupTip.wallet_tip_id,
+ );
if (tombstoneSet.has(ts)) {
continue;
}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 6569cb394..e89364ad1 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -64,6 +64,7 @@ import {
readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow,
} from "../util/http.js";
+import { checkDbInvariant } from "../util/invariants.js";
import {
DbAccess,
GetReadOnlyAccess,
@@ -168,7 +169,11 @@ export async function getExchangeDetails(
return;
}
const { currency, masterPublicKey } = dp;
- return await tx.exchangeDetails.get([r.baseUrl, currency, masterPublicKey]);
+ return await tx.exchangeDetails.indexes.byPointer.get([
+ r.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
}
getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
@@ -205,7 +210,7 @@ export async function updateExchangeTermsOfService(
/**
* Mark a ToS version as accepted by the user.
- *
+ *
* @param etag version of the ToS to accept, or current ToS version of not given
*/
export async function acceptExchangeTermsOfService(
@@ -568,10 +573,14 @@ export async function updateExchangeFromUrlHandler(
const now = AbsoluteTime.now();
baseUrl = canonicalizeBaseUrl(baseUrl);
-
+ let isNewExchange = true;
const { exchange, exchangeDetails } = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => {
+ let oldExch = await tx.exchanges.get(baseUrl);
+ if (oldExch) {
+ isNewExchange = false;
+ }
return provideExchangeRecordInTx(ws, tx, baseUrl, now);
});
@@ -637,10 +646,13 @@ export async function updateExchangeFromUrlHandler(
logger.trace("updating exchange info in database");
+ let detailsPointerChanged = false;
+
const updated = await ws.db
.mktx((x) => [
x.exchanges,
x.exchangeDetails,
+ x.exchangeSignkeys,
x.denominations,
x.coins,
x.refreshGroups,
@@ -652,42 +664,63 @@ export async function updateExchangeFromUrlHandler(
logger.warn(`exchange ${baseUrl} no longer present`);
return;
}
- let details = await getExchangeDetails(tx, r.baseUrl);
- if (details) {
+ let existingDetails = await getExchangeDetails(tx, r.baseUrl);
+ let acceptedTosEtag = undefined;
+ if (!existingDetails) {
+ detailsPointerChanged = true;
+ }
+ if (existingDetails) {
+ acceptedTosEtag = existingDetails.tosAccepted?.etag;
+ if (existingDetails.masterPublicKey !== keysInfo.masterPublicKey) {
+ detailsPointerChanged = true;
+ }
+ if (existingDetails.currency !== keysInfo.currency) {
+ detailsPointerChanged = true;
+ }
// FIXME: We need to do some consistency checks!
}
- // FIXME: validate signing keys and merge with old set
- details = {
+ let existingTosAccepted = existingDetails?.tosAccepted;
+ const newDetails = {
+ rowId: existingDetails?.rowId,
auditors: keysInfo.auditors,
currency: keysInfo.currency,
masterPublicKey: keysInfo.masterPublicKey,
protocolVersionRange: keysInfo.protocolVersion,
- signingKeys: keysInfo.signingKeys,
reserveClosingDelay: keysInfo.reserveClosingDelay,
globalFees,
exchangeBaseUrl: r.baseUrl,
wireInfo,
- tosCurrentEtag: tosDownload.tosContentType,
- tosAccepted: tosHasBeenAccepted
- ? {
- etag: tosDownload.tosEtag,
- timestamp: TalerProtocolTimestamp.now(),
- }
- : undefined,
+ tosCurrentEtag: tosDownload.tosEtag,
+ tosAccepted: existingTosAccepted,
};
- // FIXME: only update if pointer got updated
r.lastUpdate = TalerProtocolTimestamp.now();
r.nextUpdate = keysInfo.expiry;
// New denominations might be available.
r.nextRefreshCheck = TalerProtocolTimestamp.now();
- r.detailsPointer = {
- currency: details.currency,
- masterPublicKey: details.masterPublicKey,
- // FIXME: only change if pointer really changed
- updateClock: TalerProtocolTimestamp.now(),
- };
+ if (detailsPointerChanged) {
+ r.detailsPointer = {
+ currency: newDetails.currency,
+ masterPublicKey: newDetails.masterPublicKey,
+ updateClock: TalerProtocolTimestamp.now(),
+ };
+ }
await tx.exchanges.put(r);
- await tx.exchangeDetails.put(details);
+ logger.info(`existing details ${j2s(existingDetails)}`);
+ logger.info(`inserting new details ${j2s(newDetails)}`);
+ const drRowId = await tx.exchangeDetails.put(newDetails);
+ checkDbInvariant(typeof drRowId.key === "number");
+
+ for (const sk of keysInfo.signingKeys) {
+ // FIXME: validate signing keys before inserting them
+ await tx.exchangeSignKeys.put({
+ exchangeDetailsRowId: drRowId.key,
+ masterSig: sk.master_sig,
+ signkeyPub: sk.key,
+ stampEnd: sk.stamp_end,
+ stampExpire: sk.stamp_expire,
+ stampStart: sk.stamp_start,
+ });
+ }
logger.info("updating denominations in database");
const currentDenomSet = new Set<string>(
@@ -773,7 +806,7 @@ export async function updateExchangeFromUrlHandler(
}
return {
exchange: r,
- exchangeDetails: details,
+ exchangeDetails: newDetails,
};
});
@@ -791,9 +824,12 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating exchange info in database");
- ws.notify({
- type: NotificationType.ExchangeAdded,
- });
+ if (isNewExchange) {
+ ws.notify({
+ type: NotificationType.ExchangeAdded,
+ });
+ }
+
return {
type: OperationAttemptResultType.Finished,
result: {
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
index 47f38a3a1..9e960821d 100644
--- a/packages/taler-wallet-core/src/util/query.ts
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -494,6 +494,7 @@ function runTx<Arg, Res>(
msg = "Transaction aborted (no DB error)";
}
logger.error(msg);
+ logger.error(`${stack.stack}`);
reject(new TransactionAbortedError(msg));
};
const resP = Promise.resolve().then(() => f(arg, tx));