diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-05-27 18:43:11 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-05-27 18:43:34 +0200 |
commit | d381226f21f1d0605d06ccae56c38ab6b12f88f0 (patch) | |
tree | d431cd99d34227c321daa786a33499cc41a8806b /src/crypto | |
parent | 67a50513219dd8536ce2d7888a99cbfc3c3cabcb (diff) | |
download | wallet-core-d381226f21f1d0605d06ccae56c38ab6b12f88f0.tar.xz |
Simplify loading of the emscripten lib.
This removes an ugly hack and makes it possible to access the emscripten
compiled library from within nodejs test cases more easily.
Diffstat (limited to 'src/crypto')
-rw-r--r-- | src/crypto/cryptoApi-test.ts | 84 | ||||
-rw-r--r-- | src/crypto/cryptoApi.ts | 292 | ||||
-rw-r--r-- | src/crypto/cryptoWorker.ts | 431 | ||||
-rw-r--r-- | src/crypto/emscInterface-test.ts | 102 | ||||
-rw-r--r-- | src/crypto/emscInterface.ts | 1392 | ||||
-rw-r--r-- | src/crypto/emscLoader.d.ts | 54 | ||||
-rw-r--r-- | src/crypto/emscLoader.js | 38 |
7 files changed, 2393 insertions, 0 deletions
diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts new file mode 100644 index 000000000..89b74d776 --- /dev/null +++ b/src/crypto/cryptoApi-test.ts @@ -0,0 +1,84 @@ +import {CryptoApi} from "./cryptoApi"; +import {ReserveRecord, DenominationRecord, DenominationStatus} from "../types"; +import {test, TestLib} from "talertest"; + +let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; + +let denomValid1: DenominationRecord = { + masterSig: "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", + stampStart: "/Date(1473148381)/", + stampExpireWithdraw: "/Date(2482300381)/", + stampExpireDeposit: "/Date(1851580381)/", + denomPub: "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", + stampExpireLegal: "/Date(1567756381)/", + value: { + "currency": "PUDOS", + "value": 0, + "fraction": 100000 + }, + feeWithdraw: { + "currency": "PUDOS", + "value": 0, + "fraction": 10000 + }, + feeDeposit: { + "currency": "PUDOS", + "value": 0, + "fraction": 10000 + }, + feeRefresh: { + "currency": "PUDOS", + "value": 0, + "fraction": 10000 + }, + feeRefund: { + "currency": "PUDOS", + "value": 0, + "fraction": 10000 + }, + denomPubHash: "dummy", + status: DenominationStatus.Unverified, + isOffered: true, + exchangeBaseUrl: "https://exchange.example.com/", +}; + +let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1)); +denomInvalid1.value.value += 1; + +test("string hashing", async (t: TestLib) => { + let crypto = new CryptoApi(); + let s = await crypto.hashString("hello taler"); + let sh = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; + t.assert(s == sh); + t.pass(); +}); + +test("precoin creation", async (t: TestLib) => { + let crypto = new CryptoApi(); + let {priv, pub} = await crypto.createEddsaKeypair(); + let r: ReserveRecord = { + reserve_pub: pub, + reserve_priv: priv, + hasPayback: false, + exchange_base_url: "https://example.com/exchange", + created: 0, + requested_amount: {currency: "PUDOS", value: 0, fraction: 0}, + precoin_amount: {currency: "PUDOS", value: 0, fraction: 0}, + current_amount: null, + confirmed: false, + last_query: null, + }; + + let precoin = await crypto.createPreCoin(denomValid1, r); + t.pass(); +}); + +test("denom validation", async (t: TestLib) => { + let crypto = new CryptoApi(); + let v: boolean; + v = await crypto.isValidDenom(denomValid1, masterPub1); + t.assert(v); + v = await crypto.isValidDenom(denomInvalid1, masterPub1); + t.assert(!v); + t.pass(); +}); diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts new file mode 100644 index 000000000..a386eab42 --- /dev/null +++ b/src/crypto/cryptoApi.ts @@ -0,0 +1,292 @@ +/* + 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, see <http://www.gnu.org/licenses/> + */ + + +/** + * API to access the Taler crypto worker thread. + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + PreCoinRecord, + CoinRecord, + ReserveRecord, + AmountJson, + DenominationRecord, + PaybackRequest, + RefreshSessionRecord, + WireFee, + PayCoinInfo, +} from "../types"; +import { + OfferRecord, + CoinWithDenom, +} from "../wallet"; + + +/** + * State of a crypto worker. + */ +interface WorkerState { + /** + * The actual worker thread. + */ + w: Worker|null; + + /** + * Work we're currently executing or null if not busy. + */ + currentWorkItem: WorkItem|null; + + /** + * Timer to terminate the worker if it's not busy enough. + */ + terminationTimerHandle: number|null; +} + +interface WorkItem { + operation: string; + args: any[]; + resolve: any; + reject: any; + + /** + * Serial id to identify a matching response. + */ + rpcId: number; +} + + +/** + * Number of different priorities. Each priority p + * must be 0 <= p < NUM_PRIO. + */ +const NUM_PRIO = 5; + +export class CryptoApi { + private nextRpcId: number = 1; + private workers: WorkerState[]; + private workQueues: WorkItem[][]; + /** + * Number of busy workers. + */ + private numBusy: number = 0; + + /** + * Start a worker (if not started) and set as busy. + */ + wake<T>(ws: WorkerState, work: WorkItem): void { + if (ws.currentWorkItem != null) { + throw Error("assertion failed"); + } + ws.currentWorkItem = work; + this.numBusy++; + if (!ws.w) { + let w = new Worker("/dist/cryptoWorker-bundle.js"); + w.onmessage = (m: MessageEvent) => this.handleWorkerMessage(ws, m); + w.onerror = (e: ErrorEvent) => this.handleWorkerError(ws, e); + ws.w = w; + } + + let msg: any = { + operation: work.operation, args: work.args, + id: work.rpcId + }; + this.resetWorkerTimeout(ws); + ws.w!.postMessage(msg); + } + + resetWorkerTimeout(ws: WorkerState) { + if (ws.terminationTimerHandle != null) { + clearTimeout(ws.terminationTimerHandle); + } + let destroy = () => { + // terminate worker if it's idle + if (ws.w && ws.currentWorkItem == null) { + ws.w!.terminate(); + ws.w = null; + } + }; + ws.terminationTimerHandle = window.setTimeout(destroy, 20 * 1000); + } + + handleWorkerError(ws: WorkerState, e: ErrorEvent) { + if (ws.currentWorkItem) { + console.error(`error in worker during ${ws.currentWorkItem!.operation}`, + e); + } else { + console.error("error in worker", e); + } + console.error(e.message); + try { + ws.w!.terminate(); + ws.w = null; + } catch (e) { + console.error(e); + } + if (ws.currentWorkItem != null) { + ws.currentWorkItem.reject(e); + ws.currentWorkItem = null; + this.numBusy--; + } + this.findWork(ws); + } + + findWork(ws: WorkerState) { + // try to find more work for this worker + for (let i = 0; i < NUM_PRIO; i++) { + let q = this.workQueues[NUM_PRIO - i - 1]; + if (q.length != 0) { + let work: WorkItem = q.shift()!; + this.wake(ws, work); + return; + } + } + } + + handleWorkerMessage(ws: WorkerState, msg: MessageEvent) { + let id = msg.data.id; + if (typeof id !== "number") { + console.error("rpc id must be number"); + return; + } + let currentWorkItem = ws.currentWorkItem; + ws.currentWorkItem = null; + this.numBusy--; + this.findWork(ws); + if (!currentWorkItem) { + console.error("unsolicited response from worker"); + return; + } + if (id != currentWorkItem.rpcId) { + console.error(`RPC with id ${id} has no registry entry`); + return; + } + currentWorkItem.resolve(msg.data.result); + } + + constructor() { + this.workers = new Array<WorkerState>((navigator as any)["hardwareConcurrency"] || 2); + + for (let i = 0; i < this.workers.length; i++) { + this.workers[i] = { + w: null, + terminationTimerHandle: null, + currentWorkItem: null, + }; + } + this.workQueues = []; + for (let i = 0; i < NUM_PRIO; i++) { + this.workQueues.push([]); + } + } + + private doRpc<T>(operation: string, priority: number, + ...args: any[]): Promise<T> { + let start = performance.now(); + + let p = new Promise((resolve, reject) => { + let rpcId = this.nextRpcId++; + let workItem: WorkItem = {operation, args, resolve, reject, rpcId}; + + if (this.numBusy == this.workers.length) { + let q = this.workQueues[priority]; + if (!q) { + throw Error("assertion failed"); + } + this.workQueues[priority].push(workItem); + return; + } + + for (let i = 0; i < this.workers.length; i++) { + let ws = this.workers[i]; + if (ws.currentWorkItem != null) { + continue; + } + + this.wake<T>(ws, workItem); + return; + } + + throw Error("assertion failed"); + }); + + return p.then((r: T) => { + console.log(`rpc ${operation} took ${performance.now() - start}ms`); + return r; + }); + } + + + createPreCoin(denom: DenominationRecord, reserve: ReserveRecord): Promise<PreCoinRecord> { + return this.doRpc<PreCoinRecord>("createPreCoin", 1, denom, reserve); + } + + hashString(str: string): Promise<string> { + return this.doRpc<string>("hashString", 1, str); + } + + hashDenomPub(denomPub: string): Promise<string> { + return this.doRpc<string>("hashDenomPub", 1, denomPub); + } + + isValidDenom(denom: DenominationRecord, + masterPub: string): Promise<boolean> { + return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); + } + + isValidWireFee(type: string, wf: WireFee, masterPub: string): Promise<boolean> { + return this.doRpc<boolean>("isValidWireFee", 2, type, wf, 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); + } + + createEddsaKeypair(): Promise<{priv: string, pub: string}> { + return this.doRpc<{priv: string, pub: string}>("createEddsaKeypair", 1); + } + + rsaUnblind(sig: string, bk: string, pk: string): Promise<string> { + return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk); + } + + createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> { + return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin); + } + + createRefreshSession(exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson): Promise<RefreshSessionRecord> { + return this.doRpc<RefreshSessionRecord>("createRefreshSession", + 4, + exchangeBaseUrl, + kappa, + meltCoin, + newCoinDenoms, + meltFee); + } +} diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts new file mode 100644 index 000000000..36b3b924a --- /dev/null +++ b/src/crypto/cryptoWorker.ts @@ -0,0 +1,431 @@ +/* + 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, see <http://www.gnu.org/licenses/> + */ + +/** + * Web worker for crypto operations. + */ + + +/** + * Imports. + */ +import { + AmountJson, + Amounts, + CoinPaySig, + CoinRecord, + CoinStatus, + DenominationRecord, + PayCoinInfo, + PaybackRequest, + PreCoinRecord, + RefreshPreCoinRecord, + RefreshSessionRecord, + ReserveRecord, + WireFee, +} from "../types"; +import create = chrome.alarms.create; +import { + CoinWithDenom, + OfferRecord, +} from "../wallet"; +import * as native from "./emscInterface"; +import { + Amount, + EddsaPublicKey, + HashCode, + HashContext, + RefreshMeltCoinAffirmationPS, +} from "./emscInterface"; + + +namespace RpcFunctions { + + /** + * Create a pre-coin of the given denomination to be withdrawn from then given + * reserve. + */ + export function createPreCoin(denom: DenominationRecord, + reserve: ReserveRecord): PreCoinRecord { + 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.denomPub); + let coinPriv = native.EddsaPrivateKey.create(); + let coinPub = coinPriv.getPublicKey(); + let blindingFactor = native.RsaBlindingKeySecret.create(); + let pubHash: native.HashCode = coinPub.hash(); + let ev = native.rsaBlind(pubHash, + blindingFactor, + denomPub); + + if (!ev) { + throw Error("couldn't blind (malicious exchange key?)"); + } + + if (!denom.feeWithdraw) { + throw Error("Field fee_withdraw missing"); + } + + let amountWithFee = new native.Amount(denom.value); + amountWithFee.add(new native.Amount(denom.feeWithdraw)); + let withdrawFee = new native.Amount(denom.feeWithdraw); + + // 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: PreCoinRecord = { + reservePub: reservePub.toCrock(), + blindingKey: blindingFactor.toCrock(), + coinPub: coinPub.toCrock(), + coinPriv: coinPriv.toCrock(), + denomPub: denomPub.encode().toCrock(), + exchangeBaseUrl: reserve.exchange_base_url, + withdrawSig: sig.toCrock(), + coinEv: ev.toCrock(), + coinValue: denom.value + }; + return preCoin; + } + + export function createPaybackRequest(coin: CoinRecord): PaybackRequest { + let p = new native.PaybackRequestPS({ + coin_pub: native.EddsaPublicKey.fromCrock(coin.coinPub), + h_denom_pub: native.RsaPublicKey.fromCrock(coin.denomPub).encode().hash(), + coin_blind: native.RsaBlindingKeySecret.fromCrock(coin.blindingKey), + }); + let coinPriv = native.EddsaPrivateKey.fromCrock(coin.coinPriv); + let coinSig = native.eddsaSign(p.toPurpose(), coinPriv); + let paybackRequest: PaybackRequest = { + denom_pub: coin.denomPub, + denom_sig: coin.denomSig, + coin_blind_key_secret: coin.blindingKey, + coin_pub: coin.coinPub, + coin_sig: coinSig.toCrock(), + }; + return paybackRequest; + } + + + export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): boolean { + 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 isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { + let p = new native.MasterWireFeePS({ + h_wire_method: native.ByteArray.fromStringWithNull(type).hash(), + start_date: native.AbsoluteTimeNbo.fromStampSeconds(wf.startStamp), + end_date: native.AbsoluteTimeNbo.fromStampSeconds(wf.endStamp), + wire_fee: (new native.Amount(wf.wireFee)).toNbo(), + closing_fee: (new native.Amount(wf.closingFee)).toNbo(), + }); + + let nativeSig = new native.EddsaSignature(); + nativeSig.loadCrock(wf.sig); + let nativePub = native.EddsaPublicKey.fromCrock(masterPub); + + return native.eddsaVerify(native.SignaturePurpose.MASTER_WIRE_FEES, + p.toPurpose(), + nativeSig, + nativePub); + } + + + export function isValidDenom(denom: DenominationRecord, + masterPub: string): boolean { + let p = new native.DenominationKeyValidityPS({ + master: native.EddsaPublicKey.fromCrock(masterPub), + denom_hash: native.RsaPublicKey.fromCrock(denom.denomPub) + .encode() + .hash(), + expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireLegal), + expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireDeposit), + expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stampExpireWithdraw), + start: native.AbsoluteTimeNbo.fromTalerString(denom.stampStart), + value: (new native.Amount(denom.value)).toNbo(), + fee_deposit: (new native.Amount(denom.feeDeposit)).toNbo(), + fee_refresh: (new native.Amount(denom.feeRefresh)).toNbo(), + fee_withdraw: (new native.Amount(denom.feeWithdraw)).toNbo(), + fee_refund: (new native.Amount(denom.feeRefund)).toNbo(), + }); + + let nativeSig = new native.EddsaSignature(); + nativeSig.loadCrock(denom.masterSig); + + let nativePub = native.EddsaPublicKey.fromCrock(masterPub); + + return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, + p.toPurpose(), + nativeSig, + nativePub); + + } + + + export function createEddsaKeypair(): {priv: string, pub: string} { + const priv = native.EddsaPrivateKey.create(); + const pub = priv.getPublicKey(); + return {priv: priv.toCrock(), pub: pub.toCrock()}; + } + + + export function rsaUnblind(sig: string, bk: string, pk: string): string { + let denomSig = native.rsaUnblind(native.RsaSignature.fromCrock(sig), + native.RsaBlindingKeySecret.fromCrock(bk), + native.RsaPublicKey.fromCrock(pk)); + return denomSig.encode().toCrock() + } + + + /** + * Generate updated coins (to store in the database) + * and deposit permissions for each given coin. + */ + export function signDeposit(offer: OfferRecord, + cds: CoinWithDenom[]): PayCoinInfo { + let ret: PayCoinInfo = []; + + + let feeList: AmountJson[] = cds.map((x) => x.denom.feeDeposit); + let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount; + // okay if saturates + fees = Amounts.sub(fees, offer.contract.max_fee).amount; + let total = Amounts.add(fees, offer.contract.amount).amount; + + let amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); + let amountRemaining = new native.Amount(total); + for (let cd of cds) { + let coinSpend: Amount; + + if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { + break; + } + + if (amountRemaining.cmp(new native.Amount(cd.coin.currentAmount)) < 0) { + coinSpend = new native.Amount(amountRemaining.toJson()); + } else { + coinSpend = new native.Amount(cd.coin.currentAmount); + } + + amountSpent.add(coinSpend); + amountRemaining.sub(coinSpend); + + let feeDeposit: Amount = new native.Amount(cd.denom.feeDeposit); + + // Give the merchant at least the deposit fee, otherwise it'll reject + // the coin. + if (coinSpend.cmp(feeDeposit) < 0) { + coinSpend = feeDeposit; + } + + let newAmount = new native.Amount(cd.coin.currentAmount); + newAmount.sub(coinSpend); + cd.coin.currentAmount = newAmount.toJson(); + cd.coin.status = CoinStatus.TransactionPending; + + let d = new native.DepositRequestPS({ + h_contract: native.HashCode.fromCrock(offer.H_contract), + h_wire: native.HashCode.fromCrock(offer.contract.H_wire), + amount_with_fee: coinSpend.toNbo(), + coin_pub: native.EddsaPublicKey.fromCrock(cd.coin.coinPub), + deposit_fee: new native.Amount(cd.denom.feeDeposit).toNbo(), + merchant: native.EddsaPublicKey.fromCrock(offer.contract.merchant_pub), + refund_deadline: native.AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), + timestamp: native.AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), + }); + + let coinSig = native.eddsaSign(d.toPurpose(), + native.EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) + .toCrock(); + + let s: CoinPaySig = { + coin_sig: coinSig, + coin_pub: cd.coin.coinPub, + ub_sig: cd.coin.denomSig, + denom_pub: cd.coin.denomPub, + f: coinSpend.toJson(), + }; + ret.push({sig: s, updatedCoin: cd.coin}); + } + return ret; + } + + + export function createRefreshSession(exchangeBaseUrl: string, + kappa: number, + meltCoin: CoinRecord, + newCoinDenoms: DenominationRecord[], + meltFee: AmountJson): RefreshSessionRecord { + + let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); + + for (let ncd of newCoinDenoms) { + valueWithFee = Amounts.add(valueWithFee, + ncd.value, + ncd.feeWithdraw).amount; + } + + // melt fee + valueWithFee = Amounts.add(valueWithFee, meltFee).amount; + + let sessionHc = new HashContext(); + + let transferPubs: string[] = []; + let transferPrivs: string[] = []; + + let preCoinsForGammas: RefreshPreCoinRecord[][] = []; + + for (let i = 0; i < kappa; i++) { + let t = native.EcdhePrivateKey.create(); + let pub = t.getPublicKey(); + sessionHc.read(pub); + transferPrivs.push(t.toCrock()); + transferPubs.push(pub.toCrock()); + } + + for (let i = 0; i < newCoinDenoms.length; i++) { + let r = native.RsaPublicKey.fromCrock(newCoinDenoms[i].denomPub); + sessionHc.read(r.encode()); + } + + sessionHc.read(native.EddsaPublicKey.fromCrock(meltCoin.coinPub)); + sessionHc.read((new native.Amount(valueWithFee)).toNbo()); + + for (let i = 0; i < kappa; i++) { + let preCoins: RefreshPreCoinRecord[] = []; + for (let j = 0; j < newCoinDenoms.length; j++) { + + let transferPriv = native.EcdhePrivateKey.fromCrock(transferPrivs[i]); + let oldCoinPub = native.EddsaPublicKey.fromCrock(meltCoin.coinPub); + let transferSecret = native.ecdhEddsa(transferPriv, oldCoinPub); + + let fresh = native.setupFreshCoin(transferSecret, j); + + let coinPriv = fresh.priv; + let coinPub = coinPriv.getPublicKey(); + let blindingFactor = fresh.blindingKey; + let pubHash: native.HashCode = coinPub.hash(); + let denomPub = native.RsaPublicKey.fromCrock(newCoinDenoms[j].denomPub); + let ev = native.rsaBlind(pubHash, + blindingFactor, + denomPub); + if (!ev) { + throw Error("couldn't blind (malicious exchange key?)"); + } + let preCoin: RefreshPreCoinRecord = { + blindingKey: blindingFactor.toCrock(), + coinEv: ev.toCrock(), + publicKey: coinPub.toCrock(), + privateKey: coinPriv.toCrock(), + }; + preCoins.push(preCoin); + sessionHc.read(ev); + } + preCoinsForGammas.push(preCoins); + } + + let sessionHash = new HashCode(); + sessionHash.alloc(); + sessionHc.finish(sessionHash); + + let confirmData = new RefreshMeltCoinAffirmationPS({ + coin_pub: EddsaPublicKey.fromCrock(meltCoin.coinPub), + amount_with_fee: (new Amount(valueWithFee)).toNbo(), + session_hash: sessionHash, + melt_fee: (new Amount(meltFee)).toNbo() + }); + + + let confirmSig: string = native.eddsaSign(confirmData.toPurpose(), + native.EddsaPrivateKey.fromCrock( + meltCoin.coinPriv)).toCrock(); + + let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency); + for (let denom of newCoinDenoms) { + valueOutput = Amounts.add(valueOutput, denom.value).amount; + } + + let refreshSession: RefreshSessionRecord = { + meltCoinPub: meltCoin.coinPub, + newDenoms: newCoinDenoms.map((d) => d.denomPub), + confirmSig, + valueWithFee, + transferPubs, + preCoinsForGammas, + hash: sessionHash.toCrock(), + norevealIndex: undefined, + exchangeBaseUrl, + transferPrivs, + finished: false, + valueOutput, + }; + + return refreshSession; + } + + /** + * Hash a string including the zero terminator. + */ + export function hashString(str: string): string { + const b = native.ByteArray.fromStringWithNull(str); + return b.hash().toCrock(); + } + + export function hashDenomPub(denomPub: string): string { + return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock(); + } +} + + +let worker: Worker = (self as any) as Worker; + +worker.onmessage = (msg: MessageEvent) => { + 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 as any)[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}); +} diff --git a/src/crypto/emscInterface-test.ts b/src/crypto/emscInterface-test.ts new file mode 100644 index 000000000..4f57bf802 --- /dev/null +++ b/src/crypto/emscInterface-test.ts @@ -0,0 +1,102 @@ +import {test, TestLib} from "talertest"; +import * as native from "./emscInterface"; + +test("string hashing", (t: TestLib) => { + let x = native.ByteArray.fromStringWithNull("hello taler"); + let h = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR" + let hc = x.hash().toCrock(); + console.log(`# hc ${hc}`); + t.assert(h === hc, "must equal"); + t.pass(); +}); + +test("signing", (t: TestLib) => { + let x = native.ByteArray.fromStringWithNull("hello taler"); + let priv = native.EddsaPrivateKey.create(); + let pub = priv.getPublicKey(); + let purpose = new native.EccSignaturePurpose(native.SignaturePurpose.TEST, x); + let sig = native.eddsaSign(purpose, priv); + t.assert(native.eddsaVerify(native.SignaturePurpose.TEST, purpose, sig, pub)); + t.pass(); +}); + +test("signing-fixed-data", (t: TestLib) => { + let x = native.ByteArray.fromStringWithNull("hello taler"); + let purpose = new native.EccSignaturePurpose(native.SignaturePurpose.TEST, x); + const privStr = "G9R8KRRCAFKPD0KW7PW48CC2T03VQ8K2AN9J6J6K2YW27J5MHN90"; + const pubStr = "YHCZB442FQFJ0ET20MWA8YJ53M61EZGJ6QKV1KTJZMRNXDY45WT0"; + const sigStr = "7V6XY4QGC1406GPMT305MZQ1HDCR7R0S5BP02GTGDQFPSXB6YD2YDN5ZS7NJQCNP61Y39MRHXNXQ1Z15JY4CJY4CPDA6CKQ3313WG38"; + let priv = native.EddsaPrivateKey.fromCrock(privStr); + t.assert(privStr == priv.toCrock()) + let pub = priv.getPublicKey(); + t.assert(pubStr == pub.toCrock()); + let sig = native.EddsaSignature.fromCrock(sigStr); + t.assert(sigStr == sig.toCrock()) + let sig2 = native.eddsaSign(purpose, priv); + t.assert(sig.toCrock() == sig2.toCrock()); + t.assert(native.eddsaVerify(native.SignaturePurpose.TEST, purpose, sig, pub)); + t.pass(); +}); + +const denomPubStr1 = "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30G9R64VK6HHS6MW42DSN8MVKJGHK6WR3CGT18MWMCDSM75138E1K8S0MADSQ68W34DHH6MW4CHA270W4CG9J6GW48DHG8MVK4E9S7523GEA56H0K4E1Q891KCCSG752KGC1M88VMCDSQ6D23CHHG8H33AGHG6MSK8GT26CRKAC1M64V3JCJ56CVKCC228MWMCHA26MS30H1J8MVKEDHJ70TMADHK892KJC1H60TKJDHM710KGGT584T38H9K851KCDHG60W30HJ28CT4CC1G8CR3JGJ28H236DJ28H330H9S890M2D9S8S14AGA369344GA36S248CHS70RKEDSS6MWKGDJ26D136GT465348CSS8S232CHM6GS34C9N8CS3GD9H60W36H1R8MSK2GSQ8MSM6C9R70SKCHHN6MW3ACJ28N0K2CA58RS3GCA26MV42G9P891KAG9Q8N0KGD9M850KEHJ16S130CA27124AE1G852KJCHR6S1KGDSJ8RTKED1S8RR3CCHP68W4CH9Q6GT34GT18GS36EA46N24AGSP6933GCHM60VMAE1S8GV3EHHN74W3GC1J651KEH9N8MSK0CSG6S2KEEA460R32C1M8D144GSR6RWKEC218S0KEGJ4611KEEA36CSKJC2564TM4CSJ6H230E1N74TM8C1P61342CSG60WKCGHH64VK2G9S8CRKAHHK88W30HJ388R3CH1Q6X2K2DHK8GSM4D1Q74WM4HA461146H9S6D33JDJ26D234C9Q6923ECSS60RM6CT46CSKCH1M6S13EH9J8S33GCSN4CMGM81051JJ08SG64R30C1H4CMGM81054520A8A00"; + +test("rsa-encode", (t: TestLib) => { + const pubHashStr = "JM63YM5X7X547164QJ3MGJZ4WDD47GEQR5DW5SH35G4JFZXEJBHE5JBNZM5K8XN5C4BRW25BE6GSVAYBF790G2BZZ13VW91D41S4DS0" + let denomPub = native.RsaPublicKey.fromCrock(denomPubStr1); + let pubHash = denomPub.encode().hash(); + t.assert(pubHashStr == pubHash.toCrock()); + t.pass(); +}); + + +test("withdraw-request", (t: TestLib) => { + const reservePrivStr = "G9R8KRRCAFKPD0KW7PW48CC2T03VQ8K2AN9J6J6K2YW27J5MHN90"; + const reservePriv = native.EddsaPrivateKey.fromCrock(reservePrivStr); + const reservePub = reservePriv.getPublicKey(); + const amountWithFee = new native.Amount({currency: "KUDOS", value: 1, fraction: 10000}); + amountWithFee.add(new native.Amount({currency: "KUDOS", value: 0, fraction: 20000})); + const withdrawFee = new native.Amount({currency: "KUDOS", value: 0, fraction: 20000}) + const denomPub = native.RsaPublicKey.fromCrock(denomPubStr1); + const ev = native.ByteArray.fromStringWithNull("hello, world"); + + + // 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 sigStr = "AD3T8W44NV193J19RAN3NAJHPP6RVB0R3NWV7ZK5G8Q946YDK0B6F8YJBNRRBXSPVTKY31S7BVZPJFFTJJRMY61DH51X4JSXK677428"; + + var sig = native.eddsaSign(withdrawRequest.toPurpose(), reservePriv); + t.assert(native.eddsaVerify(native.SignaturePurpose.RESERVE_WITHDRAW, withdrawRequest.toPurpose(), sig, reservePub)); + t.assert(sig.toCrock() == sigStr); + t.pass(); +}); + +test("withdraw-request", (t: TestLib) => { + const a1 = new native.Amount({currency: "KUDOS", value: 1, fraction: 50000000}); + const a2 = new native.Amount({currency: "KUDOS", value: 1, fraction: 50000000}); + a1.add(a2); + let x = a1.toJson(); + t.assert(x.currency == "KUDOS"); + t.assert(x.fraction == 0); + t.assert(x.value == 3); + t.pass(); +}); + + +test("ecdsa", (t: TestLib) => { + const priv = native.EcdsaPrivateKey.create(); + const pub1 = priv.getPublicKey(); + t.pass(); +}); + +test("ecdhe", (t: TestLib) => { + const priv = native.EcdhePrivateKey.create(); + const pub = priv.getPublicKey(); + t.pass(); +}); diff --git a/src/crypto/emscInterface.ts b/src/crypto/emscInterface.ts new file mode 100644 index 000000000..52c6c965e --- /dev/null +++ b/src/crypto/emscInterface.ts @@ -0,0 +1,1392 @@ +/* + This file is part of TALER + (C) 2015 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, see <http://www.gnu.org/licenses/> + */ + + +/** + * Medium-level interface to emscripten-compiled modules used + * by the wallet. Handles memory management by allocating by allocating + * objects in arenas that then can be disposed of all at once. + * + * The high-level interface (using WebWorkers) is exposed in src/cryptoApi.ts. + */ + +/** + * Imports. + */ +import {AmountJson} from "../types"; +import {getLib, EmscFunGen} from "./emscLoader"; + + +const emscLib = getLib(); + + +/** + * Size of a native pointer. Must match the size + * use when compiling via emscripten. + */ +const PTR_SIZE = 4; + +const GNUNET_OK = 1; +const GNUNET_YES = 1; +const GNUNET_NO = 0; +const GNUNET_SYSERR = -1; + + +/** + * Get an emscripten-compiled function. + */ +const getEmsc: EmscFunGen = (name: string, ret: any, argTypes: any[]) => { + return (...args: any[]) => { + return emscLib.ccall(name, ret, argTypes, args); + } +}; + + +/** + * Wrapped emscripten functions that do not allocate any memory. + */ +const emsc = { + free: (ptr: number) => emscLib._free(ptr), + get_value: getEmsc("TALER_WR_get_value", + "number", + ["number"]), + get_fraction: getEmsc("TALER_WR_get_fraction", + "number", + ["number"]), + get_currency: getEmsc("TALER_WR_get_currency", + "string", + ["number"]), + amount_add: getEmsc("TALER_amount_add", + "number", + ["number", "number", "number"]), + amount_subtract: getEmsc("TALER_amount_subtract", + "number", + ["number", "number", "number"]), + amount_normalize: getEmsc("TALER_amount_normalize", + "void", + ["number"]), + amount_get_zero: getEmsc("TALER_amount_get_zero", + "number", + ["string", "number"]), + amount_cmp: getEmsc("TALER_amount_cmp", + "number", + ["number", "number"]), + amount_hton: getEmsc("TALER_amount_hton", + "void", + ["number", "number"]), + amount_ntoh: getEmsc("TALER_amount_ntoh", + "void", + ["number", "number"]), + hash: getEmsc("GNUNET_CRYPTO_hash", + "void", + ["number", "number", "number"]), + memmove: getEmsc("memmove", + "number", + ["number", "number", "number"]), + rsa_public_key_free: getEmsc("GNUNET_CRYPTO_rsa_public_key_free", + "void", + ["number"]), + rsa_signature_free: getEmsc("GNUNET_CRYPTO_rsa_signature_free", + "void", + ["number"]), + string_to_data: getEmsc("GNUNET_STRINGS_string_to_data", + "number", + ["number", "number", "number", "number"]), + eddsa_sign: getEmsc("GNUNET_CRYPTO_eddsa_sign", + "number", + ["number", "number", "number"]), + eddsa_verify: getEmsc("GNUNET_CRYPTO_eddsa_verify", + "number", + ["number", "number", "number", "number"]), + hash_create_random: getEmsc("GNUNET_CRYPTO_hash_create_random", + "void", + ["number", "number"]), + rsa_blinding_key_destroy: getEmsc("GNUNET_CRYPTO_rsa_blinding_key_free", + "void", + ["number"]), + random_block: getEmsc("GNUNET_CRYPTO_random_block", + "void", + ["number", "number", "number"]), + hash_context_abort: getEmsc("GNUNET_CRYPTO_hash_context_abort", + "void", + ["number"]), + hash_context_read: getEmsc("GNUNET_CRYPTO_hash_context_read", + "void", + ["number", "number", "number"]), + hash_context_finish: getEmsc("GNUNET_CRYPTO_hash_context_finish", + "void", + ["number", "number"]), + ecdh_eddsa: getEmsc("GNUNET_CRYPTO_ecdh_eddsa", + "number", + ["number", "number", "number"]), + + setup_fresh_coin: getEmsc( + "TALER_setup_fresh_coin", + "void", + ["number", "number", "number"]), +}; + + +/** + * Emscripten functions that allocate memory. + */ +const emscAlloc = { + get_amount: getEmsc("TALER_WRALL_get_amount", + "number", + ["number", "number", "number", "string"]), + eddsa_key_create: getEmsc("GNUNET_CRYPTO_eddsa_key_create", + "number", []), + ecdsa_key_create: getEmsc("GNUNET_CRYPTO_ecdsa_key_create", + "number", []), + ecdhe_key_create: getEmsc("GNUNET_CRYPTO_ecdhe_key_create", + "number", []), + eddsa_public_key_from_private: getEmsc( + "TALER_WRALL_eddsa_public_key_from_private", + "number", + ["number"]), + ecdsa_public_key_from_private: getEmsc( + "TALER_WRALL_ecdsa_public_key_from_private", + "number", + ["number"]), + ecdhe_public_key_from_private: getEmsc( + "TALER_WRALL_ecdhe_public_key_from_private", + "number", + ["number"]), + data_to_string_alloc: getEmsc("GNUNET_STRINGS_data_to_string_alloc", + "number", + ["number", "number"]), + purpose_create: getEmsc("TALER_WRALL_purpose_create", + "number", + ["number", "number", "number"]), + rsa_blind: getEmsc("GNUNET_CRYPTO_rsa_blind", + "number", + ["number", "number", "number", "number", "number"]), + rsa_blinding_key_create: getEmsc("GNUNET_CRYPTO_rsa_blinding_key_create", + "number", + ["number"]), + rsa_blinding_key_encode: getEmsc("GNUNET_CRYPTO_rsa_blinding_key_encode", + "number", + ["number", "number"]), + rsa_signature_encode: getEmsc("GNUNET_CRYPTO_rsa_signature_encode", + "number", + ["number", "number"]), + rsa_blinding_key_decode: getEmsc("GNUNET_CRYPTO_rsa_blinding_key_decode", + "number", + ["number", "number"]), + rsa_public_key_decode: getEmsc("GNUNET_CRYPTO_rsa_public_key_decode", + "number", + ["number", "number"]), + rsa_signature_decode: getEmsc("GNUNET_CRYPTO_rsa_signature_decode", + "number", + ["number", "number"]), + rsa_public_key_encode: getEmsc("GNUNET_CRYPTO_rsa_public_key_encode", + "number", + ["number", "number"]), + rsa_unblind: getEmsc("GNUNET_CRYPTO_rsa_unblind", + "number", + ["number", "number", "number"]), + hash_context_start: getEmsc("GNUNET_CRYPTO_hash_context_start", + "number", + []), + malloc: (size: number) => emscLib._malloc(size), +}; + + +/** + * Constants for signatures purposes, define what the signatures vouches for. + */ +export enum SignaturePurpose { + RESERVE_WITHDRAW = 1200, + WALLET_COIN_DEPOSIT = 1201, + MASTER_DENOMINATION_KEY_VALIDITY = 1025, + WALLET_COIN_MELT = 1202, + TEST = 4242, + MERCHANT_PAYMENT_OK = 1104, + MASTER_WIRE_FEES = 1028, + WALLET_COIN_PAYBACK = 1203, +} + + +/** + * Desired quality levels for random numbers. + */ +export enum RandomQuality { + WEAK = 0, + STRONG = 1, + NONCE = 2 +} + + +/** + * Object that is allocated in some arena. + */ +interface ArenaObject { + destroy(): void; +} + + +/** + * Context for cummulative hashing. + */ +export class HashContext implements ArenaObject { + private hashContextPtr: number | undefined; + + constructor() { + this.hashContextPtr = emscAlloc.hash_context_start(); + } + + /** + * Add data to be hashed. + */ + read(obj: PackedArenaObject): void { + if (!this.hashContextPtr) { + throw Error("assertion failed"); + } + emsc.hash_context_read(this.hashContextPtr, obj.nativePtr, obj.size()); + } + + /** + * Finish the hash computation. + */ + finish(h: HashCode) { + if (!this.hashContextPtr) { + throw Error("assertion failed"); + } + h.alloc(); + emsc.hash_context_finish(this.hashContextPtr, h.nativePtr); + } + + /** + * Abort hashing without computing the result. + */ + destroy(): void { + if (this.hashContextPtr) { + emsc.hash_context_abort(this.hashContextPtr); + } + this.hashContextPtr = undefined; + } +} + + +/** + * Arena object that points to an allocaed block of memory. + */ +abstract class MallocArenaObject implements ArenaObject { + protected _nativePtr: number | undefined = undefined; + + /** + * Is this a weak reference to the underlying memory? + */ + isWeak = false; + + destroy(): void { + if (this._nativePtr && !this.isWeak) { + emsc.free(this.nativePtr); + this._nativePtr = undefined; + } + } + + constructor(arena?: Arena) { + if (!arena) { + if (arenaStack.length == 0) { + throw Error("No arena available") + } + arena = arenaStack[arenaStack.length - 1]; + } + arena.put(this); + } + + alloc(size: number) { + if (this._nativePtr !== undefined) { + throw Error("Double allocation"); + } + this.nativePtr = emscAlloc.malloc(size); + } + + set nativePtr(v: number) { + if (v === undefined) { + throw Error("Native pointer must be a number or null"); + } + this._nativePtr = v; + } + + get nativePtr() { + // We want to allow latent allocation + // of native wrappers, but we never want to + // pass 'undefined' to emscripten. + if (this._nativePtr === undefined) { + throw Error("Native pointer not initialized"); + } + return this._nativePtr; + } +} + + +/** + * An arena stores objects that will be deallocated + * at the same time. + */ +interface Arena { + put(obj: ArenaObject): void; + destroy(): void; +} + + +/** + * Arena that must be manually destroyed. + */ +class SimpleArena implements Arena { + heap: Array<ArenaObject>; + + constructor() { + this.heap = []; + } + + put(obj: ArenaObject) { + this.heap.push(obj); + } + + destroy() { + for (let obj of this.heap) { + obj.destroy(); + } + this.heap = [] + } +} + + +/** + * Arena that destroys all its objects once control has returned to the message + * loop. + */ +class SyncArena extends SimpleArena { + private isScheduled: boolean; + + constructor() { + super(); + } + + pub(obj: MallocArenaObject) { + super.put(obj); + if (!this.isScheduled) { + this.schedule(); + } + this.heap.push(obj); + } + + private schedule() { + this.isScheduled = true; + Promise.resolve().then(() => { + this.isScheduled = false; + this.destroy(); + }); + } +} + +let arenaStack: Arena[] = []; +arenaStack.push(new SyncArena()); + + +/** + * Representation of monetary value in a given currency. + */ +export class Amount extends MallocArenaObject { + constructor(args?: AmountJson, arena?: Arena) { + super(arena); + if (args) { + this.nativePtr = emscAlloc.get_amount(args.value, + 0, + args.fraction, + args.currency); + } else { + this.nativePtr = emscAlloc.get_amount(0, 0, 0, ""); + } + } + + static getZero(currency: string, a?: Arena): Amount { + let am = new Amount(undefined, a); + let r = emsc.amount_get_zero(currency, am.nativePtr); + if (r != GNUNET_OK) { + throw Error("invalid currency"); + } + return am; + } + + + toNbo(a?: Arena): AmountNbo { + let x = new AmountNbo(a); + x.alloc(); + emsc.amount_hton(x.nativePtr, this.nativePtr); + return x; + } + + fromNbo(nbo: AmountNbo): void { + emsc.amount_ntoh(this.nativePtr, nbo.nativePtr); + } + + get value() { + return emsc.get_value(this.nativePtr); + } + + get fraction() { + return emsc.get_fraction(this.nativePtr); + } + + get currency(): String { + return emsc.get_currency(this.nativePtr); + } + + toJson(): AmountJson { + return { + value: emsc.get_value(this.nativePtr), + fraction: emsc.get_fraction(this.nativePtr), + currency: emsc.get_currency(this.nativePtr) + }; + } + + /** + * Add an amount to this amount. + */ + add(a: Amount) { + let res = emsc.amount_add(this.nativePtr, a.nativePtr, this.nativePtr); + if (res < 1) { + // Overflow + return false; + } + return true; + } + + /** + * Perform saturating subtraction on amounts. + */ + sub(a: Amount) { + // this = this - a + let res = emsc.amount_subtract(this.nativePtr, this.nativePtr, a.nativePtr); + if (res == 0) { + // Underflow + return false; + } + if (res > 0) { + return true; + } + throw Error("Incompatible currencies"); + } + + cmp(a: Amount) { + // If we don't check this, the c code aborts. + if (this.currency !== a.currency) { + throw Error(`incomparable currencies (${this.currency} and ${a.currency})`); + } + return emsc.amount_cmp(this.nativePtr, a.nativePtr); + } + + normalize() { + emsc.amount_normalize(this.nativePtr); + } +} + + +/** + * Count the UTF-8 characters in a JavaScript string. + */ +function countUtf8Bytes(str: string): number { + var s = str.length; + // JavaScript strings are UTF-16 arrays + for (let i = str.length - 1; i >= 0; i--) { + var code = str.charCodeAt(i); + if (code > 0x7f && code <= 0x7ff) { + // We need an extra byte in utf-8 here + s++; + } else if (code > 0x7ff && code <= 0xffff) { + // We need two extra bytes in utf-8 here + s += 2; + } + // Skip over the other surrogate + if (code >= 0xDC00 && code <= 0xDFFF) { + i--; + } + } + return s; +} + + +/** + * Managed reference to a contiguous block of memory in the Emscripten heap. + * Can be converted from / to a serialized representation. + * Should contain only data, not pointers. + */ +abstract class PackedArenaObject extends MallocArenaObject { + abstract size(): number; + + constructor(a?: Arena) { + super(a); + } + + randomize(qual: RandomQuality = RandomQuality.STRONG): void { + emsc.random_block(qual, this.nativePtr, this.size()); + } + + toCrock(): string { + var d = emscAlloc.data_to_string_alloc(this.nativePtr, this.size()); + var s = emscLib.Pointer_stringify(d); + emsc.free(d); + return s; + } + + toJson(): any { + // Per default, the json encoding of + // packed arena objects is just the crockford encoding. + // Subclasses typically want to override this. + return this.toCrock(); + } + + loadCrock(s: string) { + this.alloc(); + // We need to get the javascript string + // to the emscripten heap first. + let buf = ByteArray.fromStringWithNull(s); + let res = emsc.string_to_data(buf.nativePtr, + s.length, + this.nativePtr, + this.size()); + buf.destroy(); + if (res < 1) { + throw {error: "wrong encoding"}; + } + } + + alloc() { + // FIXME: should the client be allowed to call alloc multiple times? + if (!this._nativePtr) { + this.nativePtr = emscAlloc.malloc(this.size()); + } + } + + hash(): HashCode { + var x = new HashCode(); + x.alloc(); + emsc.hash(this.nativePtr, this.size(), x.nativePtr); + return x; + } + + hexdump() { + let bytes: string[] = []; + for (let i = 0; i < this.size(); i++) { + let b = emscLib.getValue(this.nativePtr + i, "i8"); + b = (b + 256) % 256; + bytes.push("0".concat(b.toString(16)).slice(-2)); + } + let lines: string[] = []; + for (let i = 0; i < bytes.length; i += 8) { + lines.push(bytes.slice(i, i + 8).join(",")); + } + return lines.join("\n"); + } +} + + +/** + * Amount, encoded for network transmission. + */ +export class AmountNbo extends PackedArenaObject { + size() { + return 24; + } + + toJson(): any { + let a = new SimpleArena(); + let am = new Amount(undefined, a); + am.fromNbo(this); + let json = am.toJson(); + a.destroy(); + return json; + } +} + + +/** + * Create a packed arena object from the base32 crockford encoding. + */ +function fromCrock<T extends PackedArenaObject>(s: string, ctor: Ctor<T>): T { + let x: T = new ctor(); + x.alloc(); + x.loadCrock(s); + return x; +} + + +/** + * Create a packed arena object from the base32 crockford encoding for objects + * that have a special decoding function. + */ +function fromCrockDecoded<T extends MallocArenaObject>(s: string, ctor: Ctor<T>, decodeFn: (p: number, s: number) => number): T { + let obj = new ctor(); + let buf = ByteArray.fromCrock(s); + obj.nativePtr = decodeFn(buf.nativePtr, buf.size()); + buf.destroy(); + return obj; +} + + +/** + * Encode an object using a special encoding function. + */ +function encode<T extends MallocArenaObject>(obj: T, encodeFn: any, arena?: Arena): ByteArray { + let ptr = emscAlloc.malloc(PTR_SIZE); + let len = encodeFn(obj.nativePtr, ptr); + let res = new ByteArray(len, undefined, arena); + res.nativePtr = emscLib.getValue(ptr, '*'); + emsc.free(ptr); + return res; +} + + +/** + * Private EdDSA key. + */ +export class EddsaPrivateKey extends PackedArenaObject { + static create(a?: Arena): EddsaPrivateKey { + let obj = new EddsaPrivateKey(a); + obj.nativePtr = emscAlloc.eddsa_key_create(); + return obj; + } + + size() { + return 32; + } + + getPublicKey(a?: Arena): EddsaPublicKey { + let obj = new EddsaPublicKey(a); + obj.nativePtr = emscAlloc.eddsa_public_key_from_private(this.nativePtr); + return obj; + } + + static fromCrock(s: string): EddsaPrivateKey { + return fromCrock(s, this); + } +} + + +export class EcdsaPrivateKey extends PackedArenaObject { + static create(a?: Arena): EcdsaPrivateKey { + let obj = new EcdsaPrivateKey(a); + obj.nativePtr = emscAlloc.ecdsa_key_create(); + return obj; + } + + size() { + return 32; + } + + getPublicKey(a?: Arena): EcdsaPublicKey { + let obj = new EcdsaPublicKey(a); + obj.nativePtr = emscAlloc.ecdsa_public_key_from_private(this.nativePtr); + return obj; + } + + static fromCrock(s: string): EcdsaPrivateKey { + return fromCrock(s, this); + } +} + + +export class EcdhePrivateKey extends PackedArenaObject { + static create(a?: Arena): EcdhePrivateKey { + let obj = new EcdhePrivateKey(a); + obj.nativePtr = emscAlloc.ecdhe_key_create(); + return obj; + } + + size() { + return 32; + } + + getPublicKey(a?: Arena): EcdhePublicKey { + let obj = new EcdhePublicKey(a); + obj.nativePtr = emscAlloc.ecdhe_public_key_from_private(this.nativePtr); + return obj; + } + + static fromCrock(s: string): EcdhePrivateKey { + return fromCrock(s, this); + } +} + + +/** + * Constructor for a given type. + */ +interface Ctor<T> { + new(): T +} + + +export class EddsaPublicKey extends PackedArenaObject { + size() { + return 32; + } + + static fromCrock(s: string): EddsaPublicKey { + return fromCrock(s, this); + } +} + +export class EcdsaPublicKey extends PackedArenaObject { + size() { + return 32; + } + + static fromCrock(s: string): EcdsaPublicKey { + return fromCrock(s, this); + } +} + + +export class EcdhePublicKey extends PackedArenaObject { + size() { + return 32; + } + + static fromCrock(s: string): EcdhePublicKey { + return fromCrock(s, this); + } +} + +export class RsaBlindingKeySecret extends PackedArenaObject { + size() { + return 32; + } + + /** + * Create a random blinding key secret. + */ + static create(a?: Arena): RsaBlindingKeySecret { + let o = new RsaBlindingKeySecret(a); + o.alloc(); + o.randomize(); + return o; + } + + static fromCrock(s: string): RsaBlindingKeySecret { + return fromCrock(s, this); + } +} + + +export class HashCode extends PackedArenaObject { + size() { + return 64; + } + + static fromCrock(s: string): HashCode { + return fromCrock(s, this); + } + + random(qual: RandomQuality = RandomQuality.STRONG) { + this.alloc(); + emsc.hash_create_random(qual, this.nativePtr); + } +} + + +export class ByteArray extends PackedArenaObject { + private allocatedSize: number; + + size() { + return this.allocatedSize; + } + + constructor(desiredSize: number, init?: number, a?: Arena) { + super(a); + if (init === undefined) { + this.nativePtr = emscAlloc.malloc(desiredSize); + } else { + this.nativePtr = init; + } + this.allocatedSize = desiredSize; + } + + static fromStringWithoutNull(s: string, a?: Arena): ByteArray { + // UTF-8 bytes, including 0-terminator + let terminatedByteLength = countUtf8Bytes(s) + 1; + let hstr = emscAlloc.malloc(terminatedByteLength); + emscLib.stringToUTF8(s, hstr, terminatedByteLength); + return new ByteArray(terminatedByteLength - 1, hstr, a); + } + + static fromStringWithNull(s: string, a?: Arena): ByteArray { + // UTF-8 bytes, including 0-terminator + let terminatedByteLength = countUtf8Bytes(s) + 1; + let hstr = emscAlloc.malloc(terminatedByteLength); + emscLib.stringToUTF8(s, hstr, terminatedByteLength); + return new ByteArray(terminatedByteLength, hstr, a); + } + + static fromCrock(s: string, a?: Arena): ByteArray { + // this one is a bit more complicated than the other fromCrock functions, + // since we don't have a fixed size + let byteLength = countUtf8Bytes(s); + let hstr = emscAlloc.malloc(byteLength + 1); + emscLib.stringToUTF8(s, hstr, byteLength + 1); + let decodedLen = Math.floor((byteLength * 5) / 8); + let ba = new ByteArray(decodedLen, undefined, a); + let res = emsc.string_to_data(hstr, byteLength, ba.nativePtr, decodedLen); + emsc.free(hstr); + if (res != GNUNET_OK) { + throw Error("decoding failed"); + } + return ba; + } +} + + +/** + * Data to sign, together with a header that includes a purpose id + * and size. + */ +export class EccSignaturePurpose extends PackedArenaObject { + size() { + return this.payloadSize + 8; + } + + payloadSize: number; + + constructor(purpose: SignaturePurpose, + payload: PackedArenaObject, + a?: Arena) { + super(a); + this.nativePtr = emscAlloc.purpose_create(purpose, + payload.nativePtr, + payload.size()); + this.payloadSize = payload.size(); + } +} + + +abstract class SignatureStruct { + abstract fieldTypes(): Array<any>; + + abstract purpose(): SignaturePurpose; + + private members: any = {}; + + constructor(x: { [name: string]: any }) { + for (let k in x) { + this.set(k, x[k]); + } + } + + toPurpose(a?: Arena): EccSignaturePurpose { + let totalSize = 0; + for (let f of this.fieldTypes()) { + let name = f[0]; + let member = this.members[name]; + if (!member) { + throw Error(`Member ${name} not set`); + } + totalSize += member.size(); + } + + let buf = emscAlloc.malloc(totalSize); + let ptr = buf; + for (let f of this.fieldTypes()) { + let name = f[0]; + let member = this.members[name]; + let size = member.size(); + emsc.memmove(ptr, member.nativePtr, size); + ptr += size; + } + let ba = new ByteArray(totalSize, buf, a); + return new EccSignaturePurpose(this.purpose(), ba); + } + + + toJson() { + let res: any = {}; + for (let f of this.fieldTypes()) { + let name = f[0]; + let member = this.members[name]; + if (!member) { + throw Error(`Member ${name} not set`); + } + res[name] = member.toJson(); + } + res["purpose"] = this.purpose(); + return res; + } + + protected set(name: string, value: PackedArenaObject) { + let typemap: any = {}; + for (let f of this.fieldTypes()) { + typemap[f[0]] = f[1]; + } + if (!(name in typemap)) { + throw Error(`Key ${name} not found`); + } + if (!(value instanceof typemap[name])) { + throw Error("Wrong type for ${name}"); + } + this.members[name] = value; + } +} + + +// It's redundant, but more type safe. +export interface WithdrawRequestPS_Args { + reserve_pub: EddsaPublicKey; + amount_with_fee: AmountNbo; + withdraw_fee: AmountNbo; + h_denomination_pub: HashCode; + h_coin_envelope: HashCode; +} + + +export class WithdrawRequestPS extends SignatureStruct { + constructor(w: WithdrawRequestPS_Args) { + super(w); + } + + purpose() { + return SignaturePurpose.RESERVE_WITHDRAW; + } + + fieldTypes() { + return [ + ["reserve_pub", EddsaPublicKey], + ["amount_with_fee", AmountNbo], + ["withdraw_fee", AmountNbo], + ["h_denomination_pub", HashCode], + ["h_coin_envelope", HashCode] + ]; + } +} + + +export interface PaybackRequestPS_args { + coin_pub: EddsaPublicKey; + h_denom_pub: HashCode; + coin_blind: RsaBlindingKeySecret; +} + + +export class PaybackRequestPS extends SignatureStruct { + constructor(w: PaybackRequestPS_args) { + super(w); + } + + purpose() { + return SignaturePurpose.WALLET_COIN_PAYBACK; + } + + fieldTypes() { + return [ + ["coin_pub", EddsaPublicKey], + ["h_denom_pub", HashCode], + ["coin_blind", RsaBlindingKeySecret], + ]; + } +} + + +interface RefreshMeltCoinAffirmationPS_Args { + session_hash: HashCode; + amount_with_fee: AmountNbo; + melt_fee: AmountNbo; + coin_pub: EddsaPublicKey; +} + +export class RefreshMeltCoinAffirmationPS extends SignatureStruct { + + constructor(w: RefreshMeltCoinAffirmationPS_Args) { + super(w); + } + + purpose() { + return SignaturePurpose.WALLET_COIN_MELT; + } + + fieldTypes() { + return [ + ["session_hash", HashCode], + ["amount_with_fee", AmountNbo], + ["melt_fee", AmountNbo], + ["coin_pub", EddsaPublicKey] + ]; + } +} + + +interface MasterWireFeePS_Args { + h_wire_method: HashCode; + start_date: AbsoluteTimeNbo; + end_date: AbsoluteTimeNbo; + wire_fee: AmountNbo; + closing_fee: AmountNbo; +} + +export class MasterWireFeePS extends SignatureStruct { + constructor(w: MasterWireFeePS_Args) { + super(w); + } + + purpose() { + return SignaturePurpose.MASTER_WIRE_FEES; + } + + fieldTypes() { + return [ + ["h_wire_method", HashCode], + ["start_date", AbsoluteTimeNbo], + ["end_date", AbsoluteTimeNbo], + ["wire_fee", AmountNbo], + ["closing_fee", AmountNbo], + ]; + } +} + + +export class AbsoluteTimeNbo extends PackedArenaObject { + static fromTalerString(s: string): AbsoluteTimeNbo { + let x = new AbsoluteTimeNbo(); + x.alloc(); + let r = /Date\(([0-9]+)\)/; + let m = r.exec(s); + if (!m || m.length != 2) { + throw Error(); + } + let n = parseInt(m[1]) * 1000000; + // XXX: This only works up to 54 bit numbers. + set64(x.nativePtr, n); + return x; + } + + static fromStampSeconds(stamp: number): AbsoluteTimeNbo { + let x = new AbsoluteTimeNbo(); + x.alloc(); + // XXX: This only works up to 54 bit numbers. + set64(x.nativePtr, stamp * 1000000); + return x; + } + + + size() { + return 8; + } +} + + +// XXX: This only works up to 54 bit numbers. +function set64(p: number, n: number) { + for (let i = 0; i < 8; ++i) { + emscLib.setValue(p + (7 - i), n & 0xFF, "i8"); + n = Math.floor(n / 256); + } +} + +// XXX: This only works up to 54 bit numbers. +function set32(p: number, n: number) { + for (let i = 0; i < 4; ++i) { + emscLib.setValue(p + (3 - i), n & 0xFF, "i8"); + n = Math.floor(n / 256); + } +} + + +export class UInt64 extends PackedArenaObject { + static fromNumber(n: number): UInt64 { + let x = new UInt64(); + x.alloc(); + set64(x.nativePtr, n); + return x; + } + + size() { + return 8; + } +} + + +export class UInt32 extends PackedArenaObject { + static fromNumber(n: number): UInt64 { + let x = new UInt32(); + x.alloc(); + set32(x.nativePtr, n); + return x; + } + + size() { + return 4; + } +} + + +// It's redundant, but more type safe. +export interface DepositRequestPS_Args { + h_contract: HashCode; + h_wire: HashCode; + timestamp: AbsoluteTimeNbo; + refund_deadline: AbsoluteTimeNbo; + amount_with_fee: AmountNbo; + deposit_fee: AmountNbo; + merchant: EddsaPublicKey; + coin_pub: EddsaPublicKey; +} + + +export class DepositRequestPS extends SignatureStruct { + constructor(w: DepositRequestPS_Args) { + super(w); + } + + purpose() { + return SignaturePurpose.WALLET_COIN_DEPOSIT; + } + + fieldTypes() { + return [ + ["h_contract", HashCode], + ["h_wire", HashCode], + ["timestamp", AbsoluteTimeNbo], + ["refund_deadline", AbsoluteTimeNbo], + ["amount_with_fee", AmountNbo], + ["deposit_fee", AmountNbo], + ["merchant", EddsaPublicKey], + ["coin_pub", EddsaPublicKey], + ]; + } +} + +export interface DenominationKeyValidityPS_args { + master: EddsaPublicKey; + start: AbsoluteTimeNbo; + expire_withdraw: AbsoluteTimeNbo; + expire_spend: AbsoluteTimeNbo; + expire_legal: AbsoluteTimeNbo; + value: AmountNbo; + fee_withdraw: AmountNbo; + fee_deposit: AmountNbo; + fee_refresh: AmountNbo; + fee_refund: AmountNbo; + denom_hash: HashCode; +} + +export class DenominationKeyValidityPS extends SignatureStruct { + constructor(w: DenominationKeyValidityPS_args) { + super(w); + } + + purpose() { + return SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY; + } + + fieldTypes() { + return [ + ["master", EddsaPublicKey], + ["start", AbsoluteTimeNbo], + ["expire_withdraw", AbsoluteTimeNbo], + ["expire_spend", AbsoluteTimeNbo], + ["expire_legal", AbsoluteTimeNbo], + ["value", AmountNbo], + ["fee_withdraw", AmountNbo], + ["fee_deposit", AmountNbo], + ["fee_refresh", AmountNbo], + ["fee_refund", AmountNbo], + ["denom_hash", HashCode] + ]; + } +} + +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 { + return fromCrockDecoded(s, this, emscAlloc.rsa_public_key_decode); + } + + toCrock() { + return this.encode().toCrock(); + } + + destroy() { + emsc.rsa_public_key_free(this.nativePtr); + this.nativePtr = 0; + } + + encode(arena?: Arena): ByteArray { + return encode(this, emscAlloc.rsa_public_key_encode); + } +} + + +export class EddsaSignature extends PackedArenaObject { + size() { + return 64; + } + static fromCrock(s: string): EddsaSignature { + return fromCrock(s, this); + } +} + + +export class RsaSignature extends MallocArenaObject { + static fromCrock(s: string, a?: Arena) { + return fromCrockDecoded(s, this, emscAlloc.rsa_signature_decode); + } + + encode(arena?: Arena): ByteArray { + return encode(this, emscAlloc.rsa_signature_encode); + } + + destroy() { + emsc.rsa_signature_free(this.nativePtr); + this.nativePtr = 0; + } +} + + +/** + * Blind a value so it can be blindly signed. + */ +export function rsaBlind(hashCode: HashCode, + blindingKey: RsaBlindingKeySecret, + pkey: RsaPublicKey, + arena?: Arena): ByteArray|null { + let buf_ptr_out = emscAlloc.malloc(PTR_SIZE); + let buf_size_out = emscAlloc.malloc(PTR_SIZE); + let res = emscAlloc.rsa_blind(hashCode.nativePtr, + blindingKey.nativePtr, + pkey.nativePtr, + buf_ptr_out, + buf_size_out); + let buf_ptr = emscLib.getValue(buf_ptr_out, '*'); + let buf_size = emscLib.getValue(buf_size_out, '*'); + emsc.free(buf_ptr_out); + emsc.free(buf_size_out); + if (res != GNUNET_OK) { + // malicious key + return null; + } + return new ByteArray(buf_size, buf_ptr, arena); +} + + +/** + * Sign data using EdDSA. + */ +export function eddsaSign(purpose: EccSignaturePurpose, + priv: EddsaPrivateKey, + a?: Arena): EddsaSignature { + let sig = new EddsaSignature(a); + sig.alloc(); + let res = emsc.eddsa_sign(priv.nativePtr, purpose.nativePtr, sig.nativePtr); + if (res < 1) { + throw Error("EdDSA signing failed"); + } + return sig; +} + + +/** + * Verify EdDSA-signed data. + */ +export function eddsaVerify(purposeNum: number, + verify: EccSignaturePurpose, + sig: EddsaSignature, + pub: EddsaPublicKey, + a?: Arena): boolean { + let r = emsc.eddsa_verify(purposeNum, + verify.nativePtr, + sig.nativePtr, + pub.nativePtr); + return r === GNUNET_OK; +} + + +/** + * Unblind a blindly signed value. + */ +export function rsaUnblind(sig: RsaSignature, + bk: RsaBlindingKeySecret, + pk: RsaPublicKey, + a?: Arena): RsaSignature { + let x = new RsaSignature(a); + x.nativePtr = emscAlloc.rsa_unblind(sig.nativePtr, + bk.nativePtr, + pk.nativePtr); + return x; +} + + +type TransferSecretP = HashCode; + +export interface FreshCoin { + priv: EddsaPrivateKey; + blindingKey: RsaBlindingKeySecret; +} + +/** + * Diffie-Hellman operation between an ECDHE private key + * and an EdDSA public key. + */ +export function ecdhEddsa(priv: EcdhePrivateKey, + pub: EddsaPublicKey): HashCode { + let h = new HashCode(); + h.alloc(); + let res = emsc.ecdh_eddsa(priv.nativePtr, pub.nativePtr, h.nativePtr); + if (res != GNUNET_OK) { + throw Error("ecdh_eddsa failed"); + } + return h; +} + + +/** + * Derive a fresh coin from the given seed. Used during refreshing. + */ +export function setupFreshCoin(secretSeed: TransferSecretP, + coinIndex: number): FreshCoin { + let priv = new EddsaPrivateKey(); + priv.isWeak = true; + let blindingKey = new RsaBlindingKeySecret(); + blindingKey.isWeak = true; + let buf = new ByteArray(priv.size() + blindingKey.size()); + + emsc.setup_fresh_coin(secretSeed.nativePtr, coinIndex, buf.nativePtr); + + priv.nativePtr = buf.nativePtr; + blindingKey.nativePtr = buf.nativePtr + priv.size(); + + return {priv, blindingKey}; +} diff --git a/src/crypto/emscLoader.d.ts b/src/crypto/emscLoader.d.ts new file mode 100644 index 000000000..e46ed7f13 --- /dev/null +++ b/src/crypto/emscLoader.d.ts @@ -0,0 +1,54 @@ +/* + 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, see <http://www.gnu.org/licenses/> + */ + + +declare function getLib(): EmscLib; + +export interface EmscFunGen { + (name: string, + ret: string, + args: string[]): ((...x: (number|string)[]) => any); + (name: string, + ret: "number", + args: string[]): ((...x: (number|string)[]) => number); + (name: string, + ret: "void", + args: string[]): ((...x: (number|string)[]) => void); + (name: string, + ret: "string", + args: string[]): ((...x: (number|string)[]) => string); +} + + +interface EmscLib { + cwrap: EmscFunGen; + + ccall(name: string, ret:"number"|"string", argTypes: any[], args: any[]): any + + stringToUTF8(s: string, addr: number, maxLength: number): void + + _free(ptr: number): void; + + _malloc(n: number): number; + + Pointer_stringify(p: number, len?: number): string; + + getValue(ptr: number, type: string, noSafe?: boolean): number; + + setValue(ptr: number, value: number, type: string, noSafe?: boolean): void; + + writeStringToMemory(s: string, buffer: number, dontAddNull?: boolean): void; +} diff --git a/src/crypto/emscLoader.js b/src/crypto/emscLoader.js new file mode 100644 index 000000000..723b8ae36 --- /dev/null +++ b/src/crypto/emscLoader.js @@ -0,0 +1,38 @@ +/* + This file is part of TALER + (C) 2017 Inria and 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, see <http://www.gnu.org/licenses/> + */ + + +// @ts-nocheck + +/** + * Load the taler emscripten lib. + * + * If in a WebWorker, importScripts is used. Inside a browser, + * the module must be globally available. + */ +export default function getLib() { + if (window.TalerEmscriptenLib) { + return TalerEmscriptenLib; + } + if (importScripts) { + importScripts('/src/emscripten/taler-emscripten-lib.js') + if (TalerEmscriptenLib) { + throw Error("can't import TalerEmscriptenLib"); + } + return TalerEmscriptenLib + } + throw Error("Can't find TalerEmscriptenLib."); +} |