From a41d1ee53e1dc6af0b54f085053278e039cda8dc Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 15 Oct 2022 16:03:48 +0200 Subject: [PATCH] wallet-core: put signing keys in separate object store --- packages/idb-bridge/src/MemoryBackend.test.ts | 40 +++++++++ packages/taler-wallet-core/src/db.ts | 49 +++++++--- .../src/operations/backup/export.ts | 23 +++-- .../src/operations/backup/import.ts | 30 ++++--- .../src/operations/exchanges.ts | 90 +++++++++++++------ packages/taler-wallet-core/src/util/query.ts | 1 + 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. */ @@ -445,14 +462,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. */ @@ -1892,9 +1901,29 @@ export const WalletStoresV1 = { exchangeDetails: describeStore( "exchangeDetails", describeContents({ - keyPath: ["exchangeBaseUrl", "currency", "masterPublicKey"], + keyPath: "rowId", + autoIncrement: true, }), - {}, + { + byPointer: describeIndex( + "byDetailsPointer", + ["exchangeBaseUrl", "currency", "masterPublicKey"], + { + unique: true, + }, + ), + }, + ), + exchangeSignkeys: describeStore( + "exchangeSignKeys", + describeContents({ + 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) => @@ -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( @@ -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( 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));