diff --git a/content_scripts/notify.ts b/content_scripts/notify.ts index 8e70b44c4..4a65434af 100644 --- a/content_scripts/notify.ts +++ b/content_scripts/notify.ts @@ -165,9 +165,22 @@ namespace TalerNotify { }); }); + addHandler("taler-payment-failed", (e: CustomEvent) => { + const msg = { + type: "payment-failed", + detail: {}, + }; + chrome.runtime.sendMessage(msg, (resp) => { + let evt = new CustomEvent("taler-payment-failed-ok", { + detail: {} + }); + document.dispatchEvent(evt); + }); + }); + // Should be: taler-request-payment, taler-result-payment - addHandler("taler-execute-contract", function(e: CustomEvent) { + addHandler("taler-execute-contract", (e: CustomEvent) => { console.log("got taler-execute-contract in content page"); const msg = { type: "execute-payment", @@ -195,6 +208,9 @@ namespace TalerNotify { throw Error("contract missing"); } + // We have the details for then payment, the merchant page + // is responsible to give it to the merchant. + let evt = new CustomEvent("taler-notify-payment", { detail: { H_contract: e.detail.H_contract, diff --git a/lib/wallet/db.ts b/lib/wallet/db.ts index 9374aa447..0111a6c6e 100644 --- a/lib/wallet/db.ts +++ b/lib/wallet/db.ts @@ -25,7 +25,7 @@ */ const DB_NAME = "taler"; -const DB_VERSION = 5; +const DB_VERSION = 6; /** * Return a promise that resolves @@ -41,7 +41,7 @@ export function openTalerDb(): Promise { resolve(req.result); }; req.onupgradeneeded = (e) => { - let db = req.result; + const db = req.result; console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); switch (e.oldVersion) { case 0: // DB does not exist yet @@ -49,7 +49,6 @@ export function openTalerDb(): Promise { {keyPath: "baseUrl"}); exchanges.createIndex("pubKey", "masterPublicKey"); db.createObjectStore("reserves", {keyPath: "reserve_pub"}); - db.createObjectStore("denoms", {keyPath: "denomPub"}); const coins = db.createObjectStore("coins", {keyPath: "coinPub"}); coins.createIndex("exchangeBaseUrl", "exchangeBaseUrl"); const transactions = db.createObjectStore("transactions", diff --git a/lib/wallet/types.ts b/lib/wallet/types.ts index d3c1c781a..8cecc1e8e 100644 --- a/lib/wallet/types.ts +++ b/lib/wallet/types.ts @@ -93,9 +93,6 @@ export class Denomination { @Checkable.String master_sig: string; - @Checkable.Optional(Checkable.String) - pub_hash: string; - static checked: (obj: any) => Denomination; } @@ -103,7 +100,23 @@ export class Denomination { export interface IExchangeInfo { baseUrl: string; masterPublicKey: string; - denoms: Denomination[]; + + /** + * All denominations we ever received from the exchange. + * Expired denominations may be garbage collected. + */ + all_denoms: Denomination[]; + + /** + * Denominations we received with the last update. + * Subset of "denoms". + */ + active_denoms: Denomination[]; + + /** + * Timestamp for last update. + */ + last_update_time: number; } export interface WireInfo { @@ -151,13 +164,48 @@ export interface CoinPaySig { } +/** + * Coin as stored in the "coins" data store + * of the wallet database. + */ export interface Coin { + /** + * Public key of the coin. + */ coinPub: string; + + /** + * Private key to authorize operations on the coin. + */ coinPriv: string; + + /** + * Key used by the exchange used to sign the coin. + */ denomPub: string; + + /** + * Unblinded signature by the exchange. + */ denomSig: string; + + /** + * Amount that's left on the coin. + */ currentAmount: AmountJson; + + /** + * Base URL that identifies the exchange from which we got the + * coin. + */ exchangeBaseUrl: string; + + /** + * We have withdrawn the coin, but it's not accepted by the exchange anymore. + * We have to tell an auditor and wait for compensation or for the exchange + * to fix it. + */ + suspended?: boolean; } diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts index 39e3aba90..a27a8f1a6 100644 --- a/lib/wallet/wallet.ts +++ b/lib/wallet/wallet.ts @@ -79,91 +79,6 @@ export class KeysJson { } -class ExchangeInfo implements IExchangeInfo { - baseUrl: string; - masterPublicKey: string; - denoms: Denomination[]; - - constructor(obj: {baseUrl: string} & any) { - this.baseUrl = obj.baseUrl; - - if (obj.denoms) { - this.denoms = Array.from(obj.denoms); - } else { - this.denoms = []; - } - - if (typeof obj.masterPublicKey === "string") { - this.masterPublicKey = obj.masterPublicKey; - } - } - - static fresh(baseUrl: string): ExchangeInfo { - return new ExchangeInfo({baseUrl}); - } - - /** - * Merge new key information into the exchange info. - * If the new key information is invalid (missing fields, - * invalid signatures), an exception is thrown, but the - * exchange info is updated with the new information up until - * the first error. - */ - mergeKeys(newKeys: KeysJson, cryptoApi: CryptoApi): Promise { - if (!this.masterPublicKey) { - this.masterPublicKey = newKeys.master_public_key; - } - - if (this.masterPublicKey != newKeys.master_public_key) { - throw Error("public keys do not match"); - } - - let ps = newKeys.denoms.map((newDenom) => { - let found = false; - for (let oldDenom of this.denoms) { - if (oldDenom.denom_pub === newDenom.denom_pub) { - let a = Object.assign({}, oldDenom); - let b = Object.assign({}, newDenom); - // pub hash is only there for convenience in the wallet - delete a["pub_hash"]; - delete b["pub_hash"]; - if (!deepEquals(a, b)) { - console.log("old/new:"); - console.dir(a); - console.dir(b); - throw Error("denomination modified"); - } - found = true; - break; - } - } - - if (found) { - return Promise.resolve(); - } - - return cryptoApi - .isValidDenom(newDenom, this.masterPublicKey) - .then((valid) => { - if (!valid) { - console.error("invalid denomination", - newDenom, - "with key", - this.masterPublicKey); - throw Error("signature on denomination invalid"); - } - return cryptoApi.hashRsaPub(newDenom.denom_pub); - }) - .then((h) => { - this.denoms.push(Object.assign({}, newDenom, {pub_hash: h})); - }); - }); - - return Promise.all(ps).then(() => void 0); - } -} - - @Checkable.Class export class CreateReserveRequest { /** @@ -264,7 +179,7 @@ function flatMap(xs: T[], f: (x: T) => U[]): U[] { } -function getTalerStampSec(stamp: string) { +function getTalerStampSec(stamp: string): number { const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); if (!m) { return null; @@ -306,6 +221,16 @@ function copy(o) { return JSON.parse(JSON.stringify(o)); } +/** + * Result of updating exisiting information + * about an exchange with a new '/keys' response. + */ +interface KeyUpdateInfo { + updatedExchangeInfo: IExchangeInfo; + addedDenominations: Denomination[]; + removedDenominations: Denomination[]; +} + /** * Get a list of denominations (with repetitions possible) @@ -381,6 +306,19 @@ export class Wallet { } } + updateExchanges(): void { + console.log("updating exchanges"); + + Query(this.db) + .iter("exchanges") + .reduce((exchange: IExchangeInfo) => { + this.updateExchangeFromUrl(exchange.baseUrl) + .catch((e) => { + console.error("updating exchange failed", e); + }); + }); + } + /** * Resume various pending operations that are pending * by looking at the database. @@ -421,12 +359,20 @@ export class Wallet { let exchange: IExchangeInfo = mc[0]; console.log("got coin for exchange", url); let coin: Coin = mc[1]; + if (coin.suspended) { + console.log("skipping suspended coin", + coin.denomPub, + "from exchange", + exchange.baseUrl); + return; + } let cd = { coin: coin, - denom: exchange.denoms.find((e) => e.denom_pub === coin.denomPub) + denom: exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub) }; if (!cd.denom) { - throw Error("denom not found (database inconsistent)"); + console.warn("denom not found (database inconsistent)"); + return; } if (cd.denom.value.currency !== paymentAmount.currency) { console.warn("same pubkey for different currencies"); @@ -588,7 +534,9 @@ export class Wallet { let exchangeUrl = Object.keys(mcs)[0]; return this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]) - .then((ds) => this.recordConfirmPay(offer, ds, exchangeUrl)) + .then((ds) => this.recordConfirmPay(offer, + ds, + exchangeUrl)) .then(() => ({})); }); }); @@ -856,8 +804,8 @@ export class Wallet { /** * Withdraw coins from a reserve until it is empty. */ - private depleteReserve(reserve, exchange: ExchangeInfo): Promise { - let denomsAvailable: Denomination[] = copy(exchange.denoms); + private depleteReserve(reserve, exchange: IExchangeInfo): Promise { + let denomsAvailable: Denomination[] = copy(exchange.active_denoms); let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, denomsAvailable); @@ -877,7 +825,7 @@ export class Wallet { * by quering the reserve's exchange. */ private updateReserve(reservePub: string, - exchange: ExchangeInfo): Promise { + exchange: IExchangeInfo): Promise { return Query(this.db) .get("reserves", reservePub) .then((reserve) => { @@ -935,7 +883,8 @@ export class Wallet { amount: AmountJson): Promise { let p = this.updateExchangeFromUrl(baseUrl); return p.then((exchangeInfo: IExchangeInfo) => { - let selectedDenoms = getWithdrawDenomList(amount, exchangeInfo.denoms); + let selectedDenoms = getWithdrawDenomList(amount, + exchangeInfo.active_denoms); let acc = Amounts.getZero(amount.currency); for (let d of selectedDenoms) { acc = Amounts.add(acc, d.fee_withdraw).amount; @@ -964,7 +913,7 @@ export class Wallet { * Optionally link the reserve entry to the new or existing * exchange entry in then DB. */ - updateExchangeFromUrl(baseUrl): Promise { + updateExchangeFromUrl(baseUrl): Promise { baseUrl = canonicalizeBaseUrl(baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl); return this.http.get(reqUrl).then((resp) => { @@ -972,38 +921,138 @@ export class Wallet { throw Error("/keys request failed"); } let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); - - return Query(this.db).get("exchanges", baseUrl).then((r) => { - let exchangeInfo; - console.dir(r); - - if (!r) { - exchangeInfo = ExchangeInfo.fresh(baseUrl); - console.log("making fresh exchange"); - } else { - exchangeInfo = new ExchangeInfo(r); - console.log("using old exchange"); - } - - return exchangeInfo.mergeKeys(exchangeKeysJson, this.cryptoApi) - .then(() => { - return Query(this.db) - .put("exchanges", exchangeInfo) - .finish() - .then(() => exchangeInfo); - }); - - }); + return this.updateExchangeFromJson(baseUrl, exchangeKeysJson); }); } + private updateExchangeFromJson(baseUrl: string, + exchangeKeysJson: KeysJson): Promise { + let updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); + if (!updateTimeSec) { + throw Error("invalid update time"); + } + + return Query(this.db).get("exchanges", baseUrl).then((r) => { + let exchangeInfo: IExchangeInfo; + console.dir(r); + + if (!r) { + exchangeInfo = { + baseUrl, + all_denoms: [], + active_denoms: [], + last_update_time: updateTimeSec, + masterPublicKey: exchangeKeysJson.master_public_key, + }; + console.log("making fresh exchange"); + } else { + if (updateTimeSec < r.last_update_time) { + console.log("outdated /keys, not updating") + return Promise.resolve(r); + } + exchangeInfo = r; + console.log("updating old exchange"); + } + + return this.updateExchangeInfo(exchangeInfo, exchangeKeysJson) + .then((updatedExchangeInfo: IExchangeInfo) => { + let q1 = Query(this.db) + .put("exchanges", updatedExchangeInfo) + .finish() + .then(() => updatedExchangeInfo); + + let q2 = Query(this.db) + .iter("coins", + {indexName: "exchangeBaseUrl", only: baseUrl}) + .reduce((coin: Coin, suspendedCoins: Coin[]) => { + if (!updatedExchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { + return [].concat(suspendedCoins, [coin]); + } + return [].concat(suspendedCoins); + }, []) + .then((suspendedCoins: Coin[]) => { + let q = Query(this.db); + suspendedCoins.map((c) => { + console.log("suspending coin", c); + c.suspended = true; + q.put("coins", c); + }); + return q.finish(); + }); + return Promise.all([q1, q2]).then(() => updatedExchangeInfo); + }); + }); + } + + + private updateExchangeInfo(exchangeInfo: IExchangeInfo, + newKeys: KeysJson): Promise { + + if (exchangeInfo.masterPublicKey != newKeys.master_public_key) { + throw Error("public keys do not match"); + } + + exchangeInfo.active_denoms = []; + + let ps = newKeys.denoms.map((newDenom) => { + // did we find the new denom in the list of all (old) denoms? + let found = false; + for (let oldDenom of exchangeInfo.all_denoms) { + if (oldDenom.denom_pub === newDenom.denom_pub) { + let a = Object.assign({}, oldDenom); + let b = Object.assign({}, newDenom); + // pub hash is only there for convenience in the wallet + delete a["pub_hash"]; + delete b["pub_hash"]; + if (!deepEquals(a, b)) { + console.error("denomination parameters were modified, old/new:"); + console.dir(a); + console.dir(b); + // FIXME: report to auditors + } + found = true; + break; + } + } + + if (found) { + exchangeInfo.active_denoms.push(newDenom); + // No need to check signatures + return Promise.resolve(); + } + + return this.cryptoApi + .isValidDenom(newDenom, exchangeInfo.masterPublicKey) + .then((valid) => { + if (!valid) { + console.error("invalid denomination", + newDenom, + "with key", + exchangeInfo.masterPublicKey); + // FIXME: report to auditors + } + return this.cryptoApi.hashRsaPub(newDenom.denom_pub); + }) + .then((h) => { + exchangeInfo.active_denoms.push(newDenom); + exchangeInfo.all_denoms.push(newDenom); + }); + }); + + return Promise.all(ps).then(() => exchangeInfo); + } + + /** * Retrieve a mapping from currency name to the amount * that is currenctly available for spending in the wallet. */ getBalances(): Promise { function collectBalances(c: Coin, byCurrency) { + if (c.suspended) { + return byCurrency; + } let acc: AmountJson = byCurrency[c.currentAmount.currency]; if (!acc) { acc = Amounts.getZero(c.currentAmount.currency); diff --git a/lib/wallet/wxMessaging.ts b/lib/wallet/wxMessaging.ts index 522a87285..d5c32229d 100644 --- a/lib/wallet/wxMessaging.ts +++ b/lib/wallet/wxMessaging.ts @@ -141,6 +141,13 @@ function makeHandlers(db: IDBDatabase, // TODO: limit history length return wallet.getHistory(); }, + ["payment-failed"]: function(detail, sender) { + // For now we just update exchanges (maybe the exchange did something + // wrong and the keys were messed up). + // FIXME: in the future we should look at what actually went wrong. + wallet.updateExchanges(); + return Promise.resolve(); + }, }; } @@ -155,6 +162,7 @@ class ChromeBadge implements Badge { } startBusy() { + this.setColor("#00F"); this.setText("..."); }