wallet-core: put signing keys in separate object store
This commit is contained in:
parent
bd88dcebbc
commit
a41d1ee53e
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user