taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit c9180a8b3c629c009a9ac33f353737f6dc1ab871
parent 833a2cf41d000b335b4d5b8c8b1cb67fa111d27c
Author: Florian Dold <florian.dold@gmail.com>
Date:   Sun, 13 Dec 2015 18:10:33 +0100

Towards withdrawal in the WebExtension.

Diffstat:
Mextension/background/emscriptif.js | 36+++++++++++++++++-------------------
Textension/background/libwrapper.js | 0
Mextension/background/wallet.js | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mextension/pages/confirm-create-reserve.html | 2+-
Aextension/pages/debug.html | 10++++++++++
Mextension/popup/wallet.html | 1+
Mextension/popup/wallet.js | 35+++++++++++++++++++----------------
7 files changed, 270 insertions(+), 82 deletions(-)

diff --git a/extension/background/emscriptif.js b/extension/background/emscriptif.js @@ -18,23 +18,6 @@ "use strict"; -/* The following definition is needed to make emscripted library to remain - 'alive' after its loading. Otherwise, the normal behaviour would be: - loading -> look for a 'main()' -> if one is found execute it then exit, - otherwise just exit. See https://kripken.github.io/emscripten-site/docs/getting_started/FAQ.html - DO NOTE: this definition MUST precede the importing/loading of the emscripted - library */ - -/* FIXME -getLastWindow().Module = { - - onRuntimeInitialized: function() { - - } - -}; -*/ - /* According to emscripten's design, we need our emscripted library to be executed with a 'window' object as its global scope. Note: that holds on emscripten's functions too, that is they need to be *explicitly* @@ -64,6 +47,8 @@ getLastWindow().Module = { respective functions come from (the emscripted version of) TALER_* realm. */ +const PTR_SIZE = 4; + // shortcut to emscr's 'malloc' function emscMalloc(size) { var ptr = Module._malloc(size); @@ -299,8 +284,13 @@ var GCALLrsaPrivateKeyCreate = getEmsc('GNUNET_CRYPTO_rsa_private_key_create', ['number']); var GCALLrsaBlindingKeyCreate = getEmsc('GNUNET_CRYPTO_rsa_blinding_key_create', + 'number', + ['number']); + + +var GCALLrsaBlindingKeyEncode = getEmsc('GNUNET_CRYPTO_rsa_blinding_key_encode', 'number', - ['number']); + ['number', 'number']); var GCrsaBlindingKeyFree = getEmsc('GNUNET_CRYPTO_rsa_blinding_key_free', 'void', @@ -514,7 +504,6 @@ let d2s = getEmsc('GNUNET_STRINGS_data_to_string_alloc', let sizeof_EddsaPrivateKey = 32; let sizeof_EddsaPublicKey = 32; - function createEddsaKeyPair() { let privPtr = GCALLeddsaKeyCreate(); let pubPtr = emscMalloc(sizeof_EddsaPublicKey); @@ -523,3 +512,12 @@ function createEddsaKeyPair() { let pubStr = d2s(pubPtr, sizeof_EddsaPublicKey); return {priv: privStr, pub: pubStr}; } + +function createRsaBlindingKey() { + let blindFac = GCALLrsaBlindingKeyCreate(1024); + let bufPtr = emscMalloc(PTR_SIZE); + let size = GCALLrsaBlindingKeyEncode (blindFac, bufPtr); + let key = d2s(Module.getValue(bufPtr, '*'), size); + emscFree(bufPtr); + return key; +} diff --git a/extension/background/libwrapper.js b/extension/background/libwrapper.js diff --git a/extension/background/wallet.js b/extension/background/wallet.js @@ -1,47 +1,58 @@ 'use strict'; -const DONE = 4; const DB_NAME = "taler"; const DB_VERSION = 1; -// Shown in the UI. -let backendFailed = false; /** - * Run a function with the opened taler DB. + * Return a promise that resolves + * to the taler wallet db. */ -function withTalerDb(f) { - let req = indexedDB.open(DB_NAME, DB_VERSION); - req.addEventListener("error", (e) => { - // XXX: more details - backendFailed = true; - }); - req.addEventListener("success", (e) => { - var db = e.target.result; - f(db); - }); - req.addEventListener("upgradeneeded", (e) => { - console.log ("DB: upgrade needed: oldVersion = "+ event.oldVersion); - db = event.target.result; - switch (event.oldVersion) { - case 0: // DB does not exist yet - db.createObjectStore("mints", { keyPath: "mint_pub" }); - db.createObjectStore("reserves", { keyPath: "reserve_pub"}); - db.createObjectStore("denoms", { keyPath: "denom_pub" }); - db.createObjectStore("coins", { keyPath: "coin_pub" }); - db.createObjectStore("withdrawals", { keyPath: "id", autoIncrement: true }); - db.createObjectStore("transactions", { keyPath: "id", autoIncrement: true }); - break; - } +function openTalerDb(cont) { + return new Promise((resolve, reject) => { + let req = indexedDB.open(DB_NAME, DB_VERSION); + req.addEventListener("error", (e) => { + reject(e); + }); + req.addEventListener("success", (e) => { + resolve(e.target.result); + }); + req.addEventListener("upgradeneeded", (e) => { + let db = e.target.result; + console.log ("DB: upgrade needed: oldVersion = " + event.oldVersion); + db = event.target.result; + switch (event.oldVersion) { + case 0: // DB does not exist yet + db.createObjectStore("mints", { keyPath: "baseUrl" }); + db.createObjectStore("reserves", { keyPath: "reserve_pub"}); + db.createObjectStore("denoms", { keyPath: "denom_pub" }); + db.createObjectStore("coins", { keyPath: "coin_pub" }); + db.createObjectStore("withdrawals", { keyPath: "id", autoIncrement: true }); + db.createObjectStore("transactions", { keyPath: "id", autoIncrement: true }); + break; + } + }); }); } +function canonicalizeBaseUrl(url) { + let x = new URI(url); + if (!x.protocol()) { + x.protocol("https"); + } + x.path(x.path() + "/").normalizePath(); + x.fragment(); + x.query(); + return x.href() +} + + function confirmReserve(db, detail, sendResponse) { console.log('detail: ' + JSON.stringify(detail)); let keypair = createEddsaKeyPair(); let form = new FormData(); - let now = new Date(); + let now = (new Date()).toString(); form.append(detail.field_amount, detail.amount_str); form.append(detail.field_reserve_pub, keypair.pub); form.append(detail.field_mint, detail.mint); @@ -50,15 +61,16 @@ function confirmReserve(db, detail, sendResponse) { console.log("making request to " + detail.post_url); myRequest.open('post', detail.post_url); myRequest.send(form); + let mintBaseUrl = canonicalizeBaseUrl(detail.mint); myRequest.addEventListener('readystatechange', (e) => { - if (myRequest.readyState == DONE) { + if (myRequest.readyState == XMLHttpRequest.DONE) { let resp = {}; resp.status = myRequest.status; resp.text = myRequest.responseText; let reserveRecord = { reserve_pub: keypair.pub, reserve_priv: keypair.priv, - mint_base_url: detail.mint, + mint_base_url: mintBaseUrl, created: now, last_query: null, current_amount: null, @@ -77,8 +89,10 @@ function confirmReserve(db, detail, sendResponse) { tx.addEventListener('complete', (e) => { console.log('tx complete, pk was ' + reserveRecord.reserve_pub); sendResponse(resp); - updateReserveMints(db); - // We have to update the mints now ... + var mint; + updateMintFromUrl(db, reserveRecord.mint_base_url) + .then((m) => { mint = m; return updateReserve(db, keypair.pub, mint); }) + .then((reserve) => depleteReserve(db, reserve, mint)); }); break; default: @@ -92,13 +106,179 @@ function confirmReserve(db, detail, sendResponse) { } +function sumAmounts() { + let v = 0; + let f = 0; + let c; + let xs = arguments; + if (xs.length == 0) { + throw "no arguments given"; + } + for (let x of xs) { + v = (v + x.value) + Math.floor((f + x.value) / 10e6); + f = (f + x.fraction) % 10e6; + c = x.currency; + } + return {value: v, fraction:f, currency: c}; +} + +function subtractAmount(a1, a2) { + if (compareAmount(a1, a2) < 0) { + throw "negative result"; + } + let r = { + currency: a1.currency, + value: a1.value, + fraction: a1.fraction + }; + if (a2.fraction > a1.fraction) { + r.value--; + r.fraction += 1000000; + } + r.value -= a2.value; + r.fraction -= a2.fraction; + return r; +} + + +function compareAmount(a1, a2) { + if (a1.currency !== a2.currency) { + throw "can't compare different currencies"; + } + let v1 = [a1.value + Math.floor(a1.fraction / 10e6), a1.fraction % 10e6]; + let v2 = [a2.value + Math.floor(a2.fraction / 10e6), a2.fraction % 10e6]; + for (let i in v1) { + if (v1[i] < v2[i]) { + return -1; + } + if (v1[i] > v2[i]) { + return 1; + } + } + return 0; +} + +function copy(o) { + return JSON.parse(JSON.stringify(o)); +} + + +function rankDenom(o1, o2) { + return (-1) * compareAmount(o1.value, o2.value); +} + + +function withdraw(denom, reserve, mint) { + let wd = {}; + wd.denom_pub = denom.denom_pub; + wd.reserve_pub = reserve.reserve_pub; + let coin_key = createEddsaKeyPair(); + // create RSA blinding key + // blind coin + // generate signature +} + + /** - * Fetch information for mints that - * are referenced in a reserve and that were not - * updated recently. + * Withdraw coins from a reserve until it is empty. */ -function updateReserveMints(db) { +function depleteReserve(db, reserve, mint) { + let denoms = copy(mint.keys.denoms); + let remaining = copy(reserve.current_amount); + denoms.sort(rankDenom); + for (let i = 0; i < 1000; i++) { + let found = false; + for (let d of denoms) { + let cost = sumAmounts(d.value, d.fee_withdraw); + if (compareAmount (remaining, cost) < 0) { + continue; + } + found = true; + remaining = subtractAmount(remaining, cost); + withdraw(d, reserve, mint); + } + if (!found) { + break; + } + } + +} + +function updateReserve(db, reservePub, mint) { + let reserve; + return new Promise((resolve, reject) => { + let tx = db.transaction(['reserves']); + tx.objectStore('reserves').get(reservePub).onsuccess = (e) => { + let reserve = e.target.result; + let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); + reqUrl.query({'reserve_pub': reservePub}); + let myRequest = new XMLHttpRequest(); + console.log("making request to " + reqUrl.href()); + myRequest.open('get', reqUrl.href()); + myRequest.send(); + myRequest.addEventListener('readystatechange', (e) => { + if (myRequest.readyState == XMLHttpRequest.DONE) { + if (myRequest.status != 200) { + reject(); + return; + } + let reserveInfo = JSON.parse(myRequest.responseText); + console.log("got response " + JSON.stringify(reserveInfo)); + reserve.current_amount = reserveInfo.balance; + let tx = db.transaction(['reserves'], 'readwrite'); + console.log("putting updated reserve " + JSON.stringify(reserve)); + tx.objectStore('reserves').put(reserve); + tx.oncomplete = (e) => { + resolve(reserve); + }; + } + }); + }; + }); + +} + + +/** + * Update or add mint DB entry by fetching the /keys information. + * Optionally link the reserve entry to the new or existing + * mint entry in then DB. + */ +function updateMintFromUrl(db, baseUrl) { + console.log("base url is " + baseUrl); + let reqUrl = URI("keys").absoluteTo(baseUrl); + let myRequest = new XMLHttpRequest(); + myRequest.open('get', reqUrl.href()); + myRequest.send(); + return new Promise((resolve, reject) => { + myRequest.addEventListener('readystatechange', (e) => { + console.log("state change to " + myRequest.readyState); + if (myRequest.readyState == XMLHttpRequest.DONE) { + if (myRequest.status == 200) { + console.log("got /keys"); + let mintKeysJson = JSON.parse(myRequest.responseText); + if (!mintKeysJson) { + console.log("keys invalid"); + reject(); + } else { + let mint = {}; + mint.baseUrl = baseUrl; + mint.keys = mintKeysJson; + let tx = db.transaction(['mints'], 'readwrite'); + tx.objectStore('mints').put(mint); + tx.oncomplete = (e) => { + resolve(mint); + }; + } + } else { + console.log("/keys request failed with status " + myRequest.status); + // XXX: also write last error to DB to show in the UI + reject(); + } + } + }); + }); } @@ -127,20 +307,16 @@ function dumpDb(db, detail, sendResponse) { return true; } -withTalerDb((db) => { + +openTalerDb().then((db) => { console.log("db loaded"); chrome.runtime.onMessage.addListener( function (req, sender, onresponse) { - // XXX: use assoc. instead of switch? - switch (req.type) - { - case "confirm-reserve": - return confirmReserve(db, req.detail, onresponse) - break; - case "dump-db": - console.log('dumping db'); - return dumpDb(db, req.detail, onresponse); + let dispatch = { + "confirm-reserve": confirmReserve, + "dump-db": dumpDb } + return dispatch[req.type](db, req.detail, onresponse); }); }); diff --git a/extension/pages/confirm-create-reserve.html b/extension/pages/confirm-create-reserve.html @@ -15,7 +15,7 @@ <div class='formish'> <div class='form-row'> <label for='mint-url'>Mint URL</label> - <input class='url' id='mint-url' type="text"></input> + <input class='url' id='mint-url' type="text" value="http://mint.demo.taler.net/"></input> </div> <button id='confirm'>Confirm Reserve</button> </div> diff --git a/extension/pages/debug.html b/extension/pages/debug.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> + <head> + <title>Taler Wallet Debugging</title> + </head> + <body> + <h1>Debug Pages</h1> + <a href="show-db.html">Show DB</a> + </body> +</html> diff --git a/extension/popup/wallet.html b/extension/popup/wallet.html @@ -13,6 +13,7 @@ <a href="wallet.html" class="active">Wallet</a> <a href="transactions.html">Transactions</a> <a href="reserves.html">Reserves</a> + <button id="debug">Debug!</button> </div> <div id="content"> diff --git a/extension/popup/wallet.js b/extension/popup/wallet.js @@ -78,20 +78,23 @@ function update_currency (currency, amount) checkbox._amount = amount; } -document.addEventListener( - 'DOMContentLoaded', - function () { - chrome.runtime.sendMessage({type: "WALLET_GET"}, function(wallet) { - for (let currency in wallet) - { - let amount = amount_format(wallet[currency]); - add_currency(currency, amount); - } - }); - - // FIXME: remove - add_currency('EUR', 42); - add_currency('USD', 17); - add_currency('KUD', 1337); - update_currency('USD', 23); +document.addEventListener('DOMContentLoaded', (e) => { + chrome.runtime.sendMessage({type: "WALLET_GET"}, function(wallet) { + for (let currency in wallet) { + let amount = amount_format(wallet[currency]); + add_currency(currency, amount); + } + }); + + // FIXME: remove + add_currency('EUR', 42); + add_currency('USD', 17); + add_currency('KUD', 1337); + update_currency('USD', 23); + + document.getElementById("debug").addEventListener("click", (e) => { + chrome.tabs.create({ + "url": chrome.extension.getURL("pages/debug.html") + }); }); +});