wallet-core: put signing keys in separate object store

This commit is contained in:
Florian Dold 2022-10-15 16:03:48 +02:00
parent bd88dcebbc
commit a41d1ee53e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 175 additions and 58 deletions

View File

@ -26,6 +26,7 @@ import {
import { import {
IDBCursorDirection, IDBCursorDirection,
IDBCursorWithValue, IDBCursorWithValue,
IDBDatabase,
IDBKeyRange, IDBKeyRange,
IDBValidKey, IDBValidKey,
} from "./idbtypes.js"; } from "./idbtypes.js";
@ -439,6 +440,45 @@ test("update with non-existent index values", async (t) => {
t.pass(); 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) => { test("range queries", async (t) => {
const backend = new MemoryBackend(); const backend = new MemoryBackend();
backend.enableTracing = true; backend.enableTracing = true;

View File

@ -51,6 +51,8 @@ import {
TransactionIdStr, TransactionIdStr,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
EddsaPublicKeyString,
EddsaSignatureString,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js"; import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; 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 * Exchange details for a particular
* (exchangeBaseUrl, masterPublicKey, currency) tuple. * (exchangeBaseUrl, masterPublicKey, currency) tuple.
*/ */
export interface ExchangeDetailsRecord { export interface ExchangeDetailsRecord {
rowId?: number;
/** /**
* Master public key of the exchange. * Master public key of the exchange.
*/ */
@ -445,14 +462,6 @@ export interface ExchangeDetailsRecord {
*/ */
globalFees: ExchangeGlobalFees[]; 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. * Etag of the current ToS of the exchange.
*/ */
@ -1892,9 +1901,29 @@ export const WalletStoresV1 = {
exchangeDetails: describeStore( exchangeDetails: describeStore(
"exchangeDetails", "exchangeDetails",
describeContents<ExchangeDetailsRecord>({ 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: describeStore(
"refreshGroups", "refreshGroups",

View File

@ -35,6 +35,7 @@ import {
BackupDenomination, BackupDenomination,
BackupExchange, BackupExchange,
BackupExchangeDetails, BackupExchangeDetails,
BackupExchangeSignKey,
BackupExchangeWireFee, BackupExchangeWireFee,
BackupOperationStatus, BackupOperationStatus,
BackupPayInfo, BackupPayInfo,
@ -74,6 +75,7 @@ import {
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkDbInvariant } from "../../util/invariants.js";
import { getWalletBackupState, provideBackupState } from "./state.js"; import { getWalletBackupState, provideBackupState } from "./state.js";
const logger = new Logger("backup/export.ts"); const logger = new Logger("backup/export.ts");
@ -87,6 +89,7 @@ export async function exportBackup(
x.config, x.config,
x.exchanges, x.exchanges,
x.exchangeDetails, x.exchangeDetails,
x.exchangeSignkeys,
x.coins, x.coins,
x.contractTerms, x.contractTerms,
x.denominations, 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({ backupExchangeDetails.push({
base_url: ex.exchangeBaseUrl, base_url: ex.exchangeBaseUrl,
@ -341,13 +356,7 @@ export async function exportBackup(
currency: ex.currency, currency: ex.currency,
protocol_version: ex.protocolVersionRange, protocol_version: ex.protocolVersionRange,
wire_fees: wireFees, wire_fees: wireFees,
signing_keys: ex.signingKeys.map((x) => ({ signing_keys: signingKeys,
key: x.key,
master_sig: x.master_sig,
stamp_end: x.stamp_end,
stamp_expire: x.stamp_expire,
stamp_start: x.stamp_start,
})),
global_fees: ex.globalFees.map((x) => ({ global_fees: ex.globalFees.map((x) => ({
accountFee: Amounts.stringify(x.accountFee), accountFee: Amounts.stringify(x.accountFee),
historyFee: Amounts.stringify(x.historyFee), historyFee: Amounts.stringify(x.historyFee),

View File

@ -62,7 +62,12 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkLogicInvariant } from "../../util/invariants.js"; import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.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 { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js"; import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js"; import { provideBackupState } from "./state.js";
@ -360,7 +365,8 @@ export async function importBackup(
} }
for (const backupExchangeDetails of backupBlob.exchange_details) { for (const backupExchangeDetails of backupBlob.exchange_details) {
const existingExchangeDetails = await tx.exchangeDetails.get([ const existingExchangeDetails =
await tx.exchangeDetails.indexes.byPointer.get([
backupExchangeDetails.base_url, backupExchangeDetails.base_url,
backupExchangeDetails.currency, backupExchangeDetails.currency,
backupExchangeDetails.master_public_key, backupExchangeDetails.master_public_key,
@ -422,13 +428,6 @@ export async function importBackup(
purseTimeout: x.purseTimeout, purseTimeout: x.purseTimeout,
startDate: x.startDate, 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) { 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)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }

View File

@ -64,6 +64,7 @@ import {
readSuccessResponseJsonOrThrow, readSuccessResponseJsonOrThrow,
readSuccessResponseTextOrThrow, readSuccessResponseTextOrThrow,
} from "../util/http.js"; } from "../util/http.js";
import { checkDbInvariant } from "../util/invariants.js";
import { import {
DbAccess, DbAccess,
GetReadOnlyAccess, GetReadOnlyAccess,
@ -168,7 +169,11 @@ export async function getExchangeDetails(
return; return;
} }
const { currency, masterPublicKey } = dp; 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>) => getExchangeDetails.makeContext = (db: DbAccess<typeof WalletStoresV1>) =>
@ -568,10 +573,14 @@ export async function updateExchangeFromUrlHandler(
const now = AbsoluteTime.now(); const now = AbsoluteTime.now();
baseUrl = canonicalizeBaseUrl(baseUrl); baseUrl = canonicalizeBaseUrl(baseUrl);
let isNewExchange = true;
const { exchange, exchangeDetails } = await ws.db const { exchange, exchangeDetails } = await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails]) .mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
let oldExch = await tx.exchanges.get(baseUrl);
if (oldExch) {
isNewExchange = false;
}
return provideExchangeRecordInTx(ws, tx, baseUrl, now); return provideExchangeRecordInTx(ws, tx, baseUrl, now);
}); });
@ -637,10 +646,13 @@ export async function updateExchangeFromUrlHandler(
logger.trace("updating exchange info in database"); logger.trace("updating exchange info in database");
let detailsPointerChanged = false;
const updated = await ws.db const updated = await ws.db
.mktx((x) => [ .mktx((x) => [
x.exchanges, x.exchanges,
x.exchangeDetails, x.exchangeDetails,
x.exchangeSignkeys,
x.denominations, x.denominations,
x.coins, x.coins,
x.refreshGroups, x.refreshGroups,
@ -652,42 +664,63 @@ export async function updateExchangeFromUrlHandler(
logger.warn(`exchange ${baseUrl} no longer present`); logger.warn(`exchange ${baseUrl} no longer present`);
return; return;
} }
let details = await getExchangeDetails(tx, r.baseUrl); let existingDetails = await getExchangeDetails(tx, r.baseUrl);
if (details) { 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: We need to do some consistency checks!
} }
// FIXME: validate signing keys and merge with old set let existingTosAccepted = existingDetails?.tosAccepted;
details = { const newDetails = {
rowId: existingDetails?.rowId,
auditors: keysInfo.auditors, auditors: keysInfo.auditors,
currency: keysInfo.currency, currency: keysInfo.currency,
masterPublicKey: keysInfo.masterPublicKey, masterPublicKey: keysInfo.masterPublicKey,
protocolVersionRange: keysInfo.protocolVersion, protocolVersionRange: keysInfo.protocolVersion,
signingKeys: keysInfo.signingKeys,
reserveClosingDelay: keysInfo.reserveClosingDelay, reserveClosingDelay: keysInfo.reserveClosingDelay,
globalFees, globalFees,
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: r.baseUrl,
wireInfo, wireInfo,
tosCurrentEtag: tosDownload.tosContentType, tosCurrentEtag: tosDownload.tosEtag,
tosAccepted: tosHasBeenAccepted tosAccepted: existingTosAccepted,
? {
etag: tosDownload.tosEtag,
timestamp: TalerProtocolTimestamp.now(),
}
: undefined,
}; };
// FIXME: only update if pointer got updated
r.lastUpdate = TalerProtocolTimestamp.now(); r.lastUpdate = TalerProtocolTimestamp.now();
r.nextUpdate = keysInfo.expiry; r.nextUpdate = keysInfo.expiry;
// New denominations might be available. // New denominations might be available.
r.nextRefreshCheck = TalerProtocolTimestamp.now(); r.nextRefreshCheck = TalerProtocolTimestamp.now();
if (detailsPointerChanged) {
r.detailsPointer = { r.detailsPointer = {
currency: details.currency, currency: newDetails.currency,
masterPublicKey: details.masterPublicKey, masterPublicKey: newDetails.masterPublicKey,
// FIXME: only change if pointer really changed
updateClock: TalerProtocolTimestamp.now(), updateClock: TalerProtocolTimestamp.now(),
}; };
}
await tx.exchanges.put(r); 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"); logger.info("updating denominations in database");
const currentDenomSet = new Set<string>( const currentDenomSet = new Set<string>(
@ -773,7 +806,7 @@ export async function updateExchangeFromUrlHandler(
} }
return { return {
exchange: r, exchange: r,
exchangeDetails: details, exchangeDetails: newDetails,
}; };
}); });
@ -791,9 +824,12 @@ export async function updateExchangeFromUrlHandler(
logger.trace("done updating exchange info in database"); logger.trace("done updating exchange info in database");
if (isNewExchange) {
ws.notify({ ws.notify({
type: NotificationType.ExchangeAdded, type: NotificationType.ExchangeAdded,
}); });
}
return { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: { result: {

View File

@ -494,6 +494,7 @@ function runTx<Arg, Res>(
msg = "Transaction aborted (no DB error)"; msg = "Transaction aborted (no DB error)";
} }
logger.error(msg); logger.error(msg);
logger.error(`${stack.stack}`);
reject(new TransactionAbortedError(msg)); reject(new TransactionAbortedError(msg));
}; };
const resP = Promise.resolve().then(() => f(arg, tx)); const resP = Promise.resolve().then(() => f(arg, tx));