diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-04-26 03:10:52 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-04-26 03:10:52 +0200 |
commit | 79a2eed5589468c2df3e4ee2d190d9fc43b80fe9 (patch) | |
tree | b9250369bb2b54a2fb298f611ff7142a0c825999 /src | |
parent | 9aab9fd6134351bba9445df2b24d3d2c6deedf0e (diff) |
remove dependency in taler-wallet-lib, implement pay signature check/storage
Diffstat (limited to 'src')
-rw-r--r-- | src/content_scripts/notify.ts | 768 | ||||
-rw-r--r-- | src/cryptoApi.ts | 4 | ||||
-rw-r--r-- | src/cryptoWorker.ts | 14 | ||||
-rw-r--r-- | src/emscriptif.ts | 21 | ||||
l--------- | src/taler-wallet-lib.ts | 1 | ||||
-rw-r--r-- | src/wallet.ts | 9 | ||||
-rw-r--r-- | src/wxBackend.ts | 6 |
7 files changed, 512 insertions, 311 deletions
diff --git a/src/content_scripts/notify.ts b/src/content_scripts/notify.ts index 8a25dd500..a0f76cf26 100644 --- a/src/content_scripts/notify.ts +++ b/src/content_scripts/notify.ts @@ -23,378 +23,530 @@ */ -"use strict"; - import URI = require("urijs"); declare var cloneInto: any; -// Make sure we don't pollute the namespace too much. -namespace TalerNotify { - const PROTOCOL_VERSION = 1; +const PROTOCOL_VERSION = 1; - let logVerbose: boolean = false; - try { - logVerbose = !!localStorage.getItem("taler-log-verbose"); - } catch (e) { - // can't read from local storage - } +let logVerbose: boolean = false; +try { + logVerbose = !!localStorage.getItem("taler-log-verbose"); +} catch (e) { + // can't read from local storage +} - if (!taler) { - console.error("Taler wallet lib not included, HTTP 402 payments not" + - " supported"); - } +if (document.documentElement.getAttribute("data-taler-nojs")) { + document.dispatchEvent(new Event("taler-probe-result")); +} - if (document.documentElement.getAttribute("data-taler-nojs")) { - document.dispatchEvent(new Event("taler-probe-result")); - } +function subst(url: string, H_contract: string) { + url = url.replace("${H_contract}", H_contract); + url = url.replace("${$}", "$"); + return url; +} - function subst(url: string, H_contract: string) { - url = url.replace("${H_contract}", H_contract); - url = url.replace("${$}", "$"); - return url; - } +interface Handler { + type: string; + listener: (e: CustomEvent) => void|Promise<void>; +} +const handlers: Handler[] = []; + +function hashContract(contract: string): Promise<string> { + let walletHashContractMsg = { + type: "hash-contract", + detail: {contract} + }; + return new Promise<string>((resolve, reject) => { + chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { + if (!resp.hash) { + console.log("error", resp); + reject(Error("hashing failed")); + } + resolve(resp.hash); + }); + }); +} - interface Handler { - type: string; - listener: (e: CustomEvent) => void|Promise<void>; - } - const handlers: Handler[] = []; +function queryPayment(url: string): Promise<any> { + const walletMsg = { + type: "query-payment", + detail: { url }, + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(resp); + }); + }); +} - function hashContract(contract: string): Promise<string> { - let walletHashContractMsg = { - type: "hash-contract", - detail: {contract} - }; - return new Promise<string>((resolve, reject) => { - chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { - if (!resp.hash) { - console.log("error", resp); - reject(Error("hashing failed")); - } - resolve(resp.hash); - }); +function putHistory(historyEntry: any): Promise<void> { + const walletMsg = { + type: "put-history-entry", + detail: { + historyEntry, + }, + }; + return new Promise<void>((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(); }); - } + }); +} - function queryPayment(url: string): Promise<any> { - const walletMsg = { - type: "query-payment", - detail: { url }, - }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { +function saveOffer(offer: any): Promise<number> { + const walletMsg = { + type: "save-offer", + detail: { + offer: { + contract: offer.data, + merchant_sig: offer.sig, + H_contract: offer.hash, + offer_time: new Date().getTime() / 1000 + }, + }, + }; + return new Promise<number>((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + if (resp && resp.error) { + reject(resp); + } else { resolve(resp); - }); + } }); + }); +} + + + + +let sheet: CSSStyleSheet|null; + +function initStyle() { + logVerbose && console.log("taking over styles"); + const name = "taler-presence-stylesheet"; + const content = "/* Taler stylesheet controlled by JS */"; + let style = document.getElementById(name) as HTMLStyleElement|null; + if (!style) { + style = document.createElement("style"); + // Needed by WebKit + style.appendChild(document.createTextNode(content)); + style.id = name; + document.head.appendChild(style); + sheet = style.sheet as CSSStyleSheet; + } else { + // We've taken over the stylesheet now, + // make it clear by clearing all the rules in it + // and making it obvious in the DOM. + if (style.tagName.toLowerCase() === "style") { + style.innerText = content; + } + if (!style.sheet) { + throw Error("taler-presence-stylesheet should be a style sheet (<link> or <style>)"); + } + sheet = style.sheet as CSSStyleSheet; + while (sheet.cssRules.length > 0) { + sheet.deleteRule(0); + } } +} - function putHistory(historyEntry: any): Promise<void> { - const walletMsg = { - type: "put-history-entry", - detail: { - historyEntry, - }, - }; - return new Promise<void>((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - resolve(); - }); - }); + +function setStyles(installed: boolean) { + if (!sheet || !sheet.cssRules) { + return; + } + while (sheet.cssRules.length > 0) { + sheet.deleteRule(0); } + if (installed) { + sheet.insertRule(".taler-installed-hide { display: none; }", 0); + sheet.insertRule(".taler-probed-hide { display: none; }", 0); + } else { + sheet.insertRule(".taler-installed-show { display: none; }", 0); + } +} - function saveOffer(offer: any): Promise<number> { - const walletMsg = { - type: "save-offer", - detail: { - offer: { - contract: offer.data, - merchant_sig: offer.sig, - H_contract: offer.hash, - offer_time: new Date().getTime() / 1000 - }, - }, - }; - return new Promise<number>((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - if (resp && resp.error) { - reject(resp); - } else { - resolve(resp); - } - }); - }); + + +function handlePaymentResponse(walletResp: any) { + /** + * Handle a failed payment. + * + * Try to notify the wallet first, before we show a potentially + * synchronous error message (such as an alert) or leave the page. + */ + function handleFailedPayment(r: XMLHttpRequest) { + let timeoutHandle: number|null = null; + function err() { + // FIXME: proper error reporting! + console.log("pay-failed", {status: r.status, response: r.responseText}); + } + function onTimeout() { + timeoutHandle = null + err(); + } + talerPaymentFailed(walletResp.H_contract).then(() => { + if (timeoutHandle != null) { + clearTimeout(timeoutHandle); + timeoutHandle = null; + } + err(); + }) + timeoutHandle = setTimeout(onTimeout, 200); } - function init() { - chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => { - if (chrome.runtime.lastError) { - logVerbose && console.log("extension not yet ready"); - window.setTimeout(init, 200); + + logVerbose && console.log("handling taler-notify-payment: ", walletResp); + // Payment timeout in ms. + let timeout_ms = 1000; + // Current request. + let r: XMLHttpRequest|null; + let timeoutHandle: number|null = null; + function sendPay() { + r = new XMLHttpRequest(); + r.open("post", walletResp.contract.pay_url); + r.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + r.send(JSON.stringify(walletResp.payReq)); + r.onload = function() { + if (!r) { return; } - registerHandlers(); - // Hack to know when the extension is unloaded - let port = chrome.runtime.connect(); - - port.onDisconnect.addListener(() => { - logVerbose && console.log("chrome runtime disconnected, removing handlers"); - for (let handler of handlers) { - document.removeEventListener(handler.type, handler.listener); - } - }); + switch (r.status) { + case 200: + const merchantResp = JSON.parse(r.responseText); + logVerbose && console.log("got success from pay_url"); + talerPaymentSucceeded({H_contract: walletResp.H_contract, merchantSig: merchantResp.sig}).then(() => { + let nextUrl = walletResp.contract.fulfillment_url; + logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl); + window.location.href = nextUrl; + window.location.reload(true); + }); + break; + default: + handleFailedPayment(r); + break; + } + r = null; + if (timeoutHandle != null) { + clearTimeout(timeoutHandle!); + timeoutHandle = null; + } + }; + function retry() { + if (r) { + r.abort(); + r = null; + } + timeout_ms = Math.min(timeout_ms * 2, 10 * 1000); + logVerbose && console.log("sendPay timed out, retrying in ", timeout_ms, "ms"); + sendPay(); + } + timeoutHandle = setTimeout(retry, timeout_ms); + } + sendPay(); +} - if (resp && resp.type == "pay") { - logVerbose && console.log("doing taler.pay with", resp.payDetail); - taler.internalPay(resp.payDetail); - document.documentElement.style.visibility = "hidden"; + +function init() { + chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => { + if (chrome.runtime.lastError) { + logVerbose && console.log("extension not yet ready"); + window.setTimeout(init, 200); + return; + } + initStyle(); + setStyles(true); + registerHandlers(); + // Hack to know when the extension is unloaded + let port = chrome.runtime.connect(); + + port.onDisconnect.addListener(() => { + logVerbose && console.log("chrome runtime disconnected, removing handlers"); + setStyles(false); + for (let handler of handlers) { + document.removeEventListener(handler.type, handler.listener); } }); - } - logVerbose && console.log("loading Taler content script"); - init(); + if (resp && resp.type == "pay") { + logVerbose && console.log("doing taler.pay with", resp.payDetail); + talerPay(resp.payDetail).then(handlePaymentResponse); + document.documentElement.style.visibility = "hidden"; + } + }); +} - interface HandlerFn { - (detail: any, sendResponse: (msg: any) => void): void; - } +interface HandlerFn { + (detail: any, sendResponse: (msg: any) => void): void; +} - function generateNonce(): Promise<string> { - const walletMsg = { - type: "generate-nonce", - }; - return new Promise<string>((resolve, reject) => { - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - resolve(resp); - }); +function generateNonce(): Promise<string> { + const walletMsg = { + type: "generate-nonce", + }; + return new Promise<string>((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(resp); }); - } + }); +} - function downloadContract(url: string, nonce: string): Promise<any> { - let parsed_url = new URI(url); - url = parsed_url.setQuery({nonce}).href(); - // FIXME: include and check nonce! - return new Promise((resolve, reject) => { - const contract_request = new XMLHttpRequest(); - console.log("downloading contract from '" + url + "'") - contract_request.open("GET", url, true); - contract_request.onload = function (e) { - if (contract_request.readyState == 4) { - if (contract_request.status == 200) { - console.log("response text:", - contract_request.responseText); - var contract_wrapper = JSON.parse(contract_request.responseText); - if (!contract_wrapper) { - console.error("response text was invalid json"); - let detail = {hint: "invalid json", status: contract_request.status, body: contract_request.responseText}; - reject(detail); - return; - } - resolve(contract_wrapper); - } else { - let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; +function downloadContract(url: string, nonce: string): Promise<any> { + let parsed_url = new URI(url); + url = parsed_url.setQuery({nonce}).href(); + // FIXME: include and check nonce! + return new Promise((resolve, reject) => { + const contract_request = new XMLHttpRequest(); + console.log("downloading contract from '" + url + "'") + contract_request.open("GET", url, true); + contract_request.onload = function (e) { + if (contract_request.readyState == 4) { + if (contract_request.status == 200) { + console.log("response text:", + contract_request.responseText); + var contract_wrapper = JSON.parse(contract_request.responseText); + if (!contract_wrapper) { + console.error("response text was invalid json"); + let detail = {hint: "invalid json", status: contract_request.status, body: contract_request.responseText}; reject(detail); return; } + resolve(contract_wrapper); + } else { + let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; + reject(detail); + return; } - }; - contract_request.onerror = function (e) { - let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; - reject(detail); - return; - }; - contract_request.send(); - }); + } + }; + contract_request.onerror = function (e) { + let detail = {hint: "contract download failed", status: contract_request.status, body: contract_request.responseText}; + reject(detail); + return; + }; + contract_request.send(); + }); +} + +async function processProposal(proposal: any) { + if (!proposal.data) { + console.error("field proposal.data field missing"); + return; } - async function processProposal(proposal: any) { - if (!proposal.data) { - console.error("field proposal.data field missing"); - return; + if (!proposal.hash) { + console.error("proposal.hash field missing"); + return; + } + + let contractHash = await hashContract(proposal.data); + + if (contractHash != proposal.hash) { + console.error("merchant-supplied contract hash is wrong"); + return; + } + + let merchantName = "(unknown)"; + try { + merchantName = proposal.data.merchant.name; + } catch (e) { + // bad contract / name not included + } + + let historyEntry = { + timestamp: (new Date).getTime(), + subjectId: `contract-${contractHash}`, + type: "offer-contract", + detail: { + contractHash, + merchantName, } + }; + await putHistory(historyEntry); + let offerId = await saveOffer(proposal); + + const uri = new URI(chrome.extension.getURL( + "/src/pages/confirm-contract.html")); + const params = { + offerId: offerId.toString(), + }; + const target = uri.query(params).href(); + document.location.replace(target); +} - if (!proposal.hash) { - console.error("proposal.hash field missing"); +function talerPay(msg: any): Promise<any> { + return new Promise(async(resolve, reject) => { + // current URL without fragment + let url = new URI(document.location.href).fragment("").href(); + let res = await queryPayment(url); + logVerbose && console.log("taler-pay: got response", res); + if (res && res.payReq) { + resolve(res); return; } - - let contractHash = await hashContract(proposal.data); - - if (contractHash != proposal.hash) { - console.error("merchant-supplied contract hash is wrong"); + if (msg.contract_url) { + let nonce = await generateNonce(); + let proposal = await downloadContract(msg.contract_url, nonce); + if (proposal.data.nonce != nonce) { + console.error("stale contract"); + return; + } + await processProposal(proposal); return; } - let merchantName = "(unknown)"; - try { - merchantName = proposal.data.merchant.name; - } catch (e) { - // bad contract / name not included + if (msg.offer_url) { + document.location.href = msg.offer_url; + return; } - let historyEntry = { - timestamp: (new Date).getTime(), - subjectId: `contract-${contractHash}`, - type: "offer-contract", - detail: { - contractHash, - merchantName, - } - }; - await putHistory(historyEntry); - let offerId = await saveOffer(proposal); + console.log("can't proceed with payment, no way to get contract specified"); + }); +} - const uri = new URI(chrome.extension.getURL( - "/src/pages/confirm-contract.html")); - const params = { - offerId: offerId.toString(), +function talerPaymentFailed(H_contract: string) { + return new Promise(async(resolve, reject) => { + const walletMsg = { + type: "payment-failed", + detail: { + contractHash: H_contract + }, }; - const target = uri.query(params).href(); - document.location.replace(target); - } + chrome.runtime.sendMessage(walletMsg, (resp) => { + resolve(); + }); + }); +} - function registerHandlers() { - /** - * Add a handler for a DOM event, which automatically - * handles adding sequence numbers to responses. - */ - function addHandler(type: string, handler: HandlerFn) { - let handlerWrap = (e: CustomEvent) => { - if (e.type != type) { - throw Error(`invariant violated`); - } - let callId: number|undefined = undefined; - if (e.detail && e.detail.callId != undefined) { - callId = e.detail.callId; - } - let responder = (msg?: any) => { - let fullMsg = Object.assign({}, msg, {callId}); - let opts = { detail: fullMsg }; - if ("function" == typeof cloneInto) { - opts = cloneInto(opts, document.defaultView); - } - let evt = new CustomEvent(type + "-result", opts); - document.dispatchEvent(evt); - }; - handler(e.detail, responder); - }; - document.addEventListener(type, handlerWrap); - handlers.push({type, listener: handlerWrap}); +function talerPaymentSucceeded(msg: any) { + return new Promise((resolve, reject) => { + if (!msg.H_contract) { + console.error("H_contract missing in taler-payment-succeeded"); + return; } - - - addHandler("taler-query-id", (msg: any, sendResponse: any) => { - // FIXME: maybe include this info in taoer-probe? - sendResponse({id: chrome.runtime.id}) + if (!msg.merchantSig) { + console.error("merchantSig missing in taler-payment-succeeded"); + return; + } + logVerbose && console.log("got taler-payment-succeeded"); + const walletMsg = { + type: "payment-succeeded", + detail: { + merchantSig: msg.merchantSig, + contractHash: msg.H_contract, + }, + }; + chrome.runtime.sendMessage(walletMsg, (resp) => { + resolve(); }); + }); +} - addHandler("taler-probe", (msg: any, sendResponse: any) => { - sendResponse(); - }); - addHandler("taler-create-reserve", (msg: any) => { - let params = { - amount: JSON.stringify(msg.amount), - callback_url: new URI(msg.callback_url) - .absoluteTo(document.location.href), - bank_url: document.location.href, - wt_types: JSON.stringify(msg.wt_types), - suggested_exchange_url: msg.suggested_exchange_url, +function registerHandlers() { + /** + * Add a handler for a DOM event, which automatically + * handles adding sequence numbers to responses. + */ + function addHandler(type: string, handler: HandlerFn) { + let handlerWrap = (e: CustomEvent) => { + if (e.type != type) { + throw Error(`invariant violated`); + } + let callId: number|undefined = undefined; + if (e.detail && e.detail.callId != undefined) { + callId = e.detail.callId; + } + let responder = (msg?: any) => { + let fullMsg = Object.assign({}, msg, {callId}); + let opts = { detail: fullMsg }; + if ("function" == typeof cloneInto) { + opts = cloneInto(opts, document.defaultView); + } + let evt = new CustomEvent(type + "-result", opts); + document.dispatchEvent(evt); }; - let uri = new URI(chrome.extension.getURL("/src/pages/confirm-create-reserve.html")); - let redirectUrl = uri.query(params).href(); - window.location.href = redirectUrl; - }); + handler(e.detail, responder); + }; + document.addEventListener(type, handlerWrap); + handlers.push({type, listener: handlerWrap}); + } - addHandler("taler-add-auditor", (msg: any) => { - let params = { - req: JSON.stringify(msg), - }; - let uri = new URI(chrome.extension.getURL("/src/pages/add-auditor.html")); - let redirectUrl = uri.query(params).href(); - window.location.href = redirectUrl; - }); - addHandler("taler-confirm-reserve", (msg: any, sendResponse: any) => { - let walletMsg = { - type: "confirm-reserve", - detail: { - reservePub: msg.reserve_pub - } - }; - chrome.runtime.sendMessage(walletMsg, (resp) => { - sendResponse(); - }); - }); + addHandler("taler-query-id", (msg: any, sendResponse: any) => { + // FIXME: maybe include this info in taoer-probe? + sendResponse({id: chrome.runtime.id}) + }); + addHandler("taler-probe", (msg: any, sendResponse: any) => { + sendResponse(); + }); - addHandler("taler-confirm-contract", async(msg: any) => { - if (!msg.contract_wrapper) { - console.error("contract wrapper missing"); - return; + addHandler("taler-create-reserve", (msg: any) => { + let params = { + amount: JSON.stringify(msg.amount), + callback_url: new URI(msg.callback_url) + .absoluteTo(document.location.href), + bank_url: document.location.href, + wt_types: JSON.stringify(msg.wt_types), + suggested_exchange_url: msg.suggested_exchange_url, + }; + let uri = new URI(chrome.extension.getURL("/src/pages/confirm-create-reserve.html")); + let redirectUrl = uri.query(params).href(); + window.location.href = redirectUrl; + }); + + addHandler("taler-add-auditor", (msg: any) => { + let params = { + req: JSON.stringify(msg), + }; + let uri = new URI(chrome.extension.getURL("/src/pages/add-auditor.html")); + let redirectUrl = uri.query(params).href(); + window.location.href = redirectUrl; + }); + + addHandler("taler-confirm-reserve", (msg: any, sendResponse: any) => { + let walletMsg = { + type: "confirm-reserve", + detail: { + reservePub: msg.reserve_pub } + }; + chrome.runtime.sendMessage(walletMsg, (resp) => { + sendResponse(); + }); + }); - const proposal = msg.contract_wrapper; - processProposal(proposal); - }); + addHandler("taler-confirm-contract", async(msg: any) => { + if (!msg.contract_wrapper) { + console.error("contract wrapper missing"); + return; + } - addHandler("taler-pay", async(msg: any, sendResponse: any) => { - // current URL without fragment - let url = new URI(document.location.href).fragment("").href(); - let res = await queryPayment(url); - logVerbose && console.log("taler-pay: got response", res); - if (res && res.payReq) { - sendResponse(res); - return; - } - if (msg.contract_url) { - let nonce = await generateNonce(); - let proposal = await downloadContract(msg.contract_url, nonce); - if (proposal.data.nonce != nonce) { - console.error("stale contract"); - return; - } - await processProposal(proposal); - return; - } + const proposal = msg.contract_wrapper; - if (msg.offer_url) { - document.location.href = msg.offer_url; - return; - } + processProposal(proposal); + }); - console.log("can't proceed with payment, no way to get contract specified"); - }); + addHandler("taler-pay", async(msg: any, sendResponse: any) => { + let resp = await talerPay(msg); + sendResponse(resp); + }); - addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { - const walletMsg = { - type: "payment-failed", - detail: { - contractHash: msg.H_contract - }, - }; - chrome.runtime.sendMessage(walletMsg, (resp) => { - sendResponse(); - }) - }); + addHandler("taler-payment-failed", async(msg: any, sendResponse: any) => { + await talerPaymentFailed(msg.H_contract); + sendResponse(); + }); - addHandler("taler-payment-succeeded", (msg: any, sendResponse: any) => { - if (!msg.H_contract) { - console.error("H_contract missing in taler-payment-succeeded"); - return; - } - logVerbose && console.log("got taler-payment-succeeded"); - const walletMsg = { - type: "payment-succeeded", - detail: { - contractHash: msg.H_contract, - }, - }; - chrome.runtime.sendMessage(walletMsg, (resp) => { - sendResponse(); - }) - }); - } + addHandler("taler-payment-succeeded", async(msg: any, sendResponse: any) => { + await talerPaymentSucceeded(msg); + sendResponse(); + }); } + +logVerbose && console.log("loading Taler content script"); +init(); + diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts index 672b90d74..98fc2c66a 100644 --- a/src/cryptoApi.ts +++ b/src/cryptoApi.ts @@ -235,6 +235,10 @@ export class CryptoApi { return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); } + isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) { + return this.doRpc<PayCoinInfo>("isValidPaymentSignature", 1, sig, contractHash, merchantPub); + } + signDeposit(offer: OfferRecord, cds: CoinWithDenom[]): Promise<PayCoinInfo> { return this.doRpc<PayCoinInfo>("signDeposit", 3, offer, cds); diff --git a/src/cryptoWorker.ts b/src/cryptoWorker.ts index 0abcb36ff..cb7bee40b 100644 --- a/src/cryptoWorker.ts +++ b/src/cryptoWorker.ts @@ -97,6 +97,20 @@ namespace RpcFunctions { } + export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) { + let p = new native.PaymentSignaturePS({ + contract_hash: native.HashCode.fromCrock(contractHash), + }); + let nativeSig = new native.EddsaSignature(); + nativeSig.loadCrock(sig); + let nativePub = native.EddsaPublicKey.fromCrock(merchantPub); + return native.eddsaVerify(native.SignaturePurpose.MERCHANT_PAYMENT_OK, + p.toPurpose(), + nativeSig, + nativePub); + } + + export function isValidDenom(denom: DenominationRecord, masterPub: string): boolean { let p = new native.DenominationKeyValidityPS({ diff --git a/src/emscriptif.ts b/src/emscriptif.ts index 0b3f2ae71..3a34f6451 100644 --- a/src/emscriptif.ts +++ b/src/emscriptif.ts @@ -206,6 +206,7 @@ export enum SignaturePurpose { MASTER_DENOMINATION_KEY_VALIDITY = 1025, WALLET_COIN_MELT = 1202, TEST = 4242, + MERCHANT_PAYMENT_OK = 1104, } @@ -1134,6 +1135,26 @@ export class DenominationKeyValidityPS extends SignatureStruct { } } +export interface PaymentSignaturePS_args { + contract_hash: HashCode; +} + +export class PaymentSignaturePS extends SignatureStruct { + constructor(w: PaymentSignaturePS_args) { + super(w); + } + + purpose() { + return SignaturePurpose.MERCHANT_PAYMENT_OK; + } + + fieldTypes() { + return [ + ["contract_hash", HashCode], + ]; + } +} + export class RsaPublicKey extends MallocArenaObject { static fromCrock(s: string): RsaPublicKey { diff --git a/src/taler-wallet-lib.ts b/src/taler-wallet-lib.ts deleted file mode 120000 index 20e599359..000000000 --- a/src/taler-wallet-lib.ts +++ /dev/null @@ -1 +0,0 @@ -../web-common/taler-wallet-lib.ts
\ No newline at end of file diff --git a/src/wallet.ts b/src/wallet.ts index a809b94cc..51046159b 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -1787,7 +1787,7 @@ export class Wallet { } - async paymentSucceeded(contractHash: string): Promise<any> { + async paymentSucceeded(contractHash: string, merchantSig: string): Promise<any> { const doPaymentSucceeded = async() => { let t = await this.q().get<TransactionRecord>(Stores.transactions, contractHash); @@ -1795,6 +1795,13 @@ export class Wallet { console.error("contract not found"); return; } + let merchantPub = t.contract.merchant_pub; + let valid = this.cryptoApi.isValidPaymentSignature(merchantSig, contractHash, merchantPub); + if (!valid) { + console.error("merchant payment signature invalid"); + // FIXME: properly display error + return; + } t.finished = true; let modifiedCoins: CoinRecord[] = []; for (let pc of t.payReq.coins) { diff --git a/src/wxBackend.ts b/src/wxBackend.ts index 1aa10ce4c..984cad21a 100644 --- a/src/wxBackend.ts +++ b/src/wxBackend.ts @@ -254,10 +254,14 @@ function makeHandlers(db: IDBDatabase, }, ["payment-succeeded"]: function (detail, sender) { let contractHash = detail.contractHash; + let merchantSig = detail.merchantSig; if (!contractHash) { return Promise.reject(Error("contractHash missing")); } - return wallet.paymentSucceeded(contractHash); + if (!merchantSig) { + return Promise.reject(Error("merchantSig missing")); + } + return wallet.paymentSucceeded(contractHash, merchantSig); }, }; } |