diff --git a/extension/lib/wallet/cryptoLib.ts b/extension/lib/wallet/cryptoLib.ts new file mode 100644 index 000000000..2c32b3a63 --- /dev/null +++ b/extension/lib/wallet/cryptoLib.ts @@ -0,0 +1,137 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see + */ + +import {Denomination} from "./types"; +/** + * Web worker for crypto operations. + * @author Florian Dold + */ + +"use strict"; + +import * as native from "./emscriptif"; +import {PreCoin, Reserve} from "./types"; +import create = chrome.alarms.create; + + +export function main(worker: Worker) { + worker.onmessage = (msg: MessageEvent) => { + console.log("got data", msg.data); + if (!Array.isArray(msg.data.args)) { + console.error("args must be array"); + return; + } + if (typeof msg.data.id != "number") { + console.error("RPC id must be number"); + } + if (typeof msg.data.operation != "string") { + console.error("RPC operation must be string"); + } + let f = RpcFunctions[msg.data.operation]; + if (!f) { + console.error(`unknown operation: '${msg.data.operation}'`); + return; + } + let res = f(...msg.data.args); + worker.postMessage({result: res, id: msg.data.id}); + } +} + +console.log("hello, this is the crypto lib"); + +namespace RpcFunctions { + + /** + * Create a pre-coin of the given denomination to be withdrawn from then given + * reserve. + */ + export function createPreCoin(denom: Denomination, reserve: Reserve): PreCoin { + let reservePriv = new native.EddsaPrivateKey(); + reservePriv.loadCrock(reserve.reserve_priv); + let reservePub = new native.EddsaPublicKey(); + reservePub.loadCrock(reserve.reserve_pub); + let denomPub = native.RsaPublicKey.fromCrock(denom.denom_pub); + let coinPriv = native.EddsaPrivateKey.create(); + let coinPub = coinPriv.getPublicKey(); + let blindingFactor = native.RsaBlindingKey.create(1024); + let pubHash: native.HashCode = coinPub.hash(); + let ev: native.ByteArray = native.rsaBlind(pubHash, + blindingFactor, + denomPub); + + if (!denom.fee_withdraw) { + throw Error("Field fee_withdraw missing"); + } + + let amountWithFee = new native.Amount(denom.value); + amountWithFee.add(new native.Amount(denom.fee_withdraw)); + let withdrawFee = new native.Amount(denom.fee_withdraw); + + // Signature + let withdrawRequest = new native.WithdrawRequestPS({ + reserve_pub: reservePub, + amount_with_fee: amountWithFee.toNbo(), + withdraw_fee: withdrawFee.toNbo(), + h_denomination_pub: denomPub.encode().hash(), + h_coin_envelope: ev.hash() + }); + + var sig = native.eddsaSign(withdrawRequest.toPurpose(), reservePriv); + + let preCoin: PreCoin = { + reservePub: reservePub.toCrock(), + blindingKey: blindingFactor.toCrock(), + coinPub: coinPub.toCrock(), + coinPriv: coinPriv.toCrock(), + denomPub: denomPub.encode().toCrock(), + mintBaseUrl: reserve.mint_base_url, + withdrawSig: sig.toCrock(), + coinEv: ev.toCrock(), + coinValue: denom.value + }; + return preCoin; + } + + + export function isValidDenom(denom: Denomination, + masterPub: string): boolean { + let p = new native.DenominationKeyValidityPS({ + master: native.EddsaPublicKey.fromCrock(masterPub), + denom_hash: native.RsaPublicKey.fromCrock(denom.denom_pub) + .encode() + .hash(), + expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_legal), + expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_deposit), + expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_withdraw), + start: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_start), + value: (new native.Amount(denom.value)).toNbo(), + fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(), + fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(), + fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(), + }); + + let nativeSig = new native.EddsaSignature(); + nativeSig.loadCrock(denom.master_sig); + + let nativePub = native.EddsaPublicKey.fromCrock(masterPub); + + return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, + p.toPurpose(), + nativeSig, + nativePub); + + } +} diff --git a/extension/lib/wallet/cryptoWorker.ts b/extension/lib/wallet/cryptoWorker.ts new file mode 100644 index 000000000..958c2de74 --- /dev/null +++ b/extension/lib/wallet/cryptoWorker.ts @@ -0,0 +1,65 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see + */ + +/** + * Web worker for crypto operations. + * @author Florian Dold + */ + +"use strict"; + + +importScripts("../emscripten/libwrapper.js", + "../vendor/system-csp-production.src.js"); + + +// TypeScript does not allow ".js" extensions in the +// module name, so SystemJS must add it. +System.config({ + defaultJSExtensions: true, + }); + +// We expect that in the manifest, the emscripten js is loaded +// becore the background page. +// Currently it is not possible to use SystemJS to load the emscripten js. +declare var Module: any; +if ("object" !== typeof Module) { + throw Error("emscripten not loaded, no 'Module' defined"); +} + + +// Manually register the emscripten js as a SystemJS, so that +// we can use it from TypeScript by importing it. + +{ + let mod = System.newModule({Module: Module}); + let modName = System.normalizeSync("../emscripten/emsc"); + console.log("registering", modName); + System.set(modName, mod); +} + +System.import("./cryptoLib") + .then((m) => { + m.main(self); + console.log("loaded"); + }) + .catch((e) => { + console.log("crypto worker failed"); + console.error(e.stack); + }); + +console.log("in worker thread"); + diff --git a/extension/lib/wallet/types.ts b/extension/lib/wallet/types.ts index 3b0cb2638..d18bd95f4 100644 --- a/extension/lib/wallet/types.ts +++ b/extension/lib/wallet/types.ts @@ -107,6 +107,25 @@ export interface ReserveCreationInfo { withdrawFee: AmountJson; } + +export interface PreCoin { + coinPub: string; + coinPriv: string; + reservePub: string; + denomPub: string; + blindingKey: string; + withdrawSig: string; + coinEv: string; + mintBaseUrl: string; + coinValue: AmountJson; +} + +export interface Reserve { + mint_base_url: string + reserve_priv: string; + reserve_pub: string; +} + export interface Notifier { notify(); } \ No newline at end of file diff --git a/extension/lib/wallet/wallet.ts b/extension/lib/wallet/wallet.ts index 2d4a29c8d..475d316e3 100644 --- a/extension/lib/wallet/wallet.ts +++ b/extension/lib/wallet/wallet.ts @@ -28,6 +28,8 @@ import {Query} from "./query"; import {Checkable} from "./checkable"; import {canonicalizeBaseUrl} from "./helpers"; import {ReserveCreationInfo} from "./types"; +import {PreCoin} from "./types"; +import {Reserve} from "./types"; "use strict"; @@ -65,18 +67,6 @@ export class KeysJson { } -export interface PreCoin { - coinPub: string; - coinPriv: string; - reservePub: string; - denomPub: string; - blindingKey: string; - withdrawSig: string; - coinEv: string; - mintBaseUrl: string; - coinValue: AmountJson; -} - export interface Coin { coinPub: string; coinPriv: string; @@ -87,34 +77,6 @@ export interface Coin { } -function isValidDenom(denom: Denomination, - masterPub: string): boolean { - let p = new native.DenominationKeyValidityPS({ - master: native.EddsaPublicKey.fromCrock(masterPub), - denom_hash: native.RsaPublicKey.fromCrock(denom.denom_pub).encode().hash(), - expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_legal), - expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_deposit), - expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_withdraw), - start: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_start), - value: (new native.Amount(denom.value)).toNbo(), - fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(), - fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(), - fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(), - }); - - let nativeSig = new native.EddsaSignature(); - nativeSig.loadCrock(denom.master_sig); - - let nativePub = native.EddsaPublicKey.fromCrock(masterPub); - - return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, - p.toPurpose(), - nativeSig, - nativePub); - -} - - class MintInfo implements IMintInfo { baseUrl: string; masterPublicKey: string; @@ -145,53 +107,61 @@ class MintInfo implements IMintInfo { * mint info is updated with the new information up until * the first error. */ - mergeKeys(newKeys: KeysJson) { - if (!this.masterPublicKey) { - this.masterPublicKey = newKeys.master_public_key; - } + mergeKeys(newKeys: KeysJson, wallet: Wallet): Promise { + return Promise.resolve().then(() => { + if (!this.masterPublicKey) { + this.masterPublicKey = newKeys.master_public_key; + } - if (this.masterPublicKey != newKeys.master_public_key) { - throw Error("public keys do not match"); - } + if (this.masterPublicKey != newKeys.master_public_key) { + throw Error("public keys do not match"); + } - for (let newDenom of newKeys.denoms) { - 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 (!_.isEqual(a, b)) { - console.log("old/new:"); - console.dir(a); - console.dir(b); - throw Error("denomination modified"); + for (let newDenom of newKeys.denoms) { + 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 (!_.isEqual(a, b)) { + console.log("old/new:"); + console.dir(a); + console.dir(b); + throw Error("denomination modified"); + } + // TODO: check if info still matches + found = true; + break; } - // TODO: check if info still matches - found = true; - break; } + + if (found) { + continue; + } + + console.log("validating denomination"); + + return wallet.isValidDenom(newDenom, this.masterPublicKey) + .then((valid) => { + if (!valid) { + throw Error("signature on denomination invalid"); + } + + let d: Denomination = Object.assign({}, newDenom); + d.pub_hash = native.RsaPublicKey.fromCrock(d.denom_pub) + .encode() + .hash() + .toCrock(); + this.denoms.push(d); + + }); + } - - if (found) { - continue; - } - - console.log("validating denomination"); - - if (!isValidDenom(newDenom, this.masterPublicKey)) { - throw Error("signature on denomination invalid"); - } - - let d: Denomination = Object.assign({}, newDenom); - d.pub_hash = native.RsaPublicKey.fromCrock(d.denom_pub) - .encode() - .hash() - .toCrock(); - this.denoms.push(d); - } + return; + }); } } @@ -330,13 +300,6 @@ interface Transaction { } -interface Reserve { - mint_base_url: string - reserve_priv: string; - reserve_pub: string; -} - - export interface Badge { setText(s: string): void; setColor(c: string): void; @@ -395,62 +358,6 @@ function rankDenom(denom1: any, denom2: any) { } -function mergeMintKeys(oldKeys: KeysJson, newKeys: KeysJson) { -} - - -/** - * Create a pre-coin of the given denomination to be withdrawn from then given - * reserve. - */ -function createPreCoin(denom: Denomination, reserve: Reserve): PreCoin { - let reservePriv = new native.EddsaPrivateKey(); - reservePriv.loadCrock(reserve.reserve_priv); - let reservePub = new native.EddsaPublicKey(); - reservePub.loadCrock(reserve.reserve_pub); - let denomPub = native.RsaPublicKey.fromCrock(denom.denom_pub); - let coinPriv = native.EddsaPrivateKey.create(); - let coinPub = coinPriv.getPublicKey(); - let blindingFactor = native.RsaBlindingKey.create(1024); - let pubHash: native.HashCode = coinPub.hash(); - let ev: native.ByteArray = native.rsaBlind(pubHash, - blindingFactor, - denomPub); - - if (!denom.fee_withdraw) { - throw Error("Field fee_withdraw missing"); - } - - let amountWithFee = new native.Amount(denom.value); - amountWithFee.add(new native.Amount(denom.fee_withdraw)); - let withdrawFee = new native.Amount(denom.fee_withdraw); - - // Signature - let withdrawRequest = new native.WithdrawRequestPS({ - reserve_pub: reservePub, - amount_with_fee: amountWithFee.toNbo(), - withdraw_fee: withdrawFee.toNbo(), - h_denomination_pub: denomPub.encode().hash(), - h_coin_envelope: ev.hash() - }); - - var sig = native.eddsaSign(withdrawRequest.toPurpose(), reservePriv); - - let preCoin: PreCoin = { - reservePub: reservePub.toCrock(), - blindingKey: blindingFactor.toCrock(), - coinPub: coinPub.toCrock(), - coinPriv: coinPriv.toCrock(), - denomPub: denomPub.encode().toCrock(), - mintBaseUrl: reserve.mint_base_url, - withdrawSig: sig.toCrock(), - coinEv: ev.toCrock(), - coinValue: denom.value - }; - return preCoin; -} - - /** * Get a list of denominations (with repetitions possible) * whose total value is as close as possible to the available @@ -493,6 +400,9 @@ export class Wallet { private http: HttpRequestLibrary; private badge: Badge; private notifier: Notifier; + private cryptoWorker: Worker; + private nextRpcId: number = 1; + private rpcRegistry = {}; constructor(db: IDBDatabase, @@ -503,6 +413,21 @@ export class Wallet { this.http = http; this.badge = badge; this.notifier = notifier; + this.cryptoWorker = new Worker("/lib/wallet/cryptoWorker.js"); + + this.cryptoWorker.onmessage = (msg: MessageEvent) => { + let id = msg.data.id; + if (typeof id !== "number") { + console.error("rpc id must be number"); + return; + } + if (!this.rpcRegistry[id]) { + console.error(`RPC with id ${id} has no registry entry`); + return; + } + let {resolve, reject} = this.rpcRegistry[id]; + resolve(msg.data.result); + } } @@ -693,7 +618,9 @@ export class Wallet { .put("history", historyEntry) .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) .finish() - .then(() => { this.notifier.notify(); }); + .then(() => { + this.notifier.notify(); + }); } @@ -903,7 +830,9 @@ export class Wallet { .add("coins", coin) .add("history", historyEntry) .finish() - .then(() => { this.notifier.notify(); }); + .then(() => { + this.notifier.notify(); + }); } @@ -912,12 +841,15 @@ export class Wallet { */ private withdraw(denom: Denomination, reserve: Reserve): Promise { console.log("creating pre coin at", new Date()); - let preCoin = createPreCoin(denom, reserve); - return Query(this.db) - .put("precoins", preCoin) - .finish() - .then(() => this.withdrawExecute(preCoin)) - .then((c) => this.storeCoin(c)); + return this.createPreCoin(denom, reserve) + .then((preCoin) => { + return Query(this.db) + .put("precoins", preCoin) + .finish() + .then(() => this.withdrawExecute(preCoin)) + .then((c) => this.storeCoin(c)); + }); + } @@ -1028,8 +960,14 @@ export class Wallet { console.log("using old mint"); } - mint.mergeKeys(mintKeysJson); - return Query(this.db).put("mints", mint).finish().then(() => mint); + return mint.mergeKeys(mintKeysJson) + .then(() => { + return Query(this.db) + .put("mints", mint) + .finish() + .then(() => mint); + }); + }); }); } @@ -1070,4 +1008,34 @@ export class Wallet { .iter("history", {indexName: "timestamp"}) .reduce(collect, []) } + + registerRpcId(resolve, reject): number { + let id = this.nextRpcId++; + this.rpcRegistry[id] = {resolve, reject}; + return id; + } + + + createPreCoin(denom: Denomination, reserve: Reserve): Promise { + return new Promise((resolve, reject) => { + let msg = { + operation: "createPreCoin", + id: this.registerRpcId(resolve, reject), + args: [denom, reserve] + }; + this.cryptoWorker.postMessage(msg); + }); + } + + isValidDenom(denom: Denomination, + masterPub: string): Promise { + return new Promise((resolve, reject) => { + let msg = { + operation: "isValidDenom", + id: this.registerRpcId(resolve, reject), + args: [denom, masterPub] + }; + this.cryptoWorker.postMessage(msg); + }); + } } \ No newline at end of file diff --git a/extension/tsconfig.json b/extension/tsconfig.json index a3eea3a70..96f360370 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -13,6 +13,8 @@ "lib/i18n.ts", "lib/refs.ts", "lib/wallet/checkable.ts", + "lib/wallet/cryptoLib.ts", + "lib/wallet/cryptoWorker.ts", "lib/wallet/db.ts", "lib/wallet/emscriptif.ts", "lib/wallet/helpers.ts", @@ -29,4 +31,4 @@ "pages/confirm-create-reserve.tsx", "test/tests/taler.ts" ] -} +} \ No newline at end of file