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 {
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;

View File

@ -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<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",

View File

@ -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),

View File

@ -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;
}

View File

@ -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: {

View File

@ -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));