diff options
Diffstat (limited to 'extension/lib/wallet')
-rw-r--r-- | extension/lib/wallet/emscriptif.ts | 70 | ||||
-rw-r--r-- | extension/lib/wallet/types.ts | 52 | ||||
-rw-r--r-- | extension/lib/wallet/wallet.ts | 224 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.ts | 35 |
4 files changed, 339 insertions, 42 deletions
diff --git a/extension/lib/wallet/emscriptif.ts b/extension/lib/wallet/emscriptif.ts index b11d845f0..690800e2f 100644 --- a/extension/lib/wallet/emscriptif.ts +++ b/extension/lib/wallet/emscriptif.ts @@ -89,6 +89,9 @@ var emsc = { 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']), @@ -144,9 +147,10 @@ var emscAlloc = { }; -enum SignaturePurpose { +export enum SignaturePurpose { RESERVE_WITHDRAW = 1200, WALLET_COIN_DEPOSIT = 1201, + MASTER_DENOMINATION_KEY_VALIDITY = 1025, } enum RandomQuality { @@ -336,11 +340,11 @@ export class Amount extends ArenaObject { return emsc.get_fraction(this.nativePtr); } - get currency() { + get currency(): String { return emsc.get_currency(this.nativePtr); } - toJson() { + toJson(): AmountJson { return { value: emsc.get_value(this.nativePtr), fraction: emsc.get_fraction(this.nativePtr), @@ -351,7 +355,7 @@ export class Amount extends ArenaObject { /** * Add an amount to this amount. */ - add(a) { + add(a: Amount) { let res = emsc.amount_add(this.nativePtr, a.nativePtr, this.nativePtr); if (res < 1) { // Overflow @@ -363,7 +367,7 @@ export class Amount extends ArenaObject { /** * Perform saturating subtraction on amounts. */ - sub(a) { + sub(a: Amount) { // this = this - a let res = emsc.amount_subtract(this.nativePtr, this.nativePtr, a.nativePtr); if (res == 0) { @@ -376,7 +380,7 @@ export class Amount extends ArenaObject { throw Error("Incompatible currencies"); } - cmp(a) { + cmp(a: Amount) { return emsc.amount_cmp(this.nativePtr, a.nativePtr); } @@ -848,6 +852,44 @@ export class DepositRequestPS extends SignatureStruct { } } +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; + 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], + ["denom_hash", HashCode] + ]; + } +} + interface Encodeable { encode(arena?: Arena): ByteArray; @@ -932,6 +974,22 @@ export function eddsaSign(purpose: EccSignaturePurpose, } +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); + if (r === GNUNET_OK) { + return true; + } + return false; +} + + export function rsaUnblind(sig: RsaSignature, bk: RsaBlindingKey, pk: RsaPublicKey, diff --git a/extension/lib/wallet/types.ts b/extension/lib/wallet/types.ts index 197aed938..fd4ca8b01 100644 --- a/extension/lib/wallet/types.ts +++ b/extension/lib/wallet/types.ts @@ -53,4 +53,56 @@ export class CreateReserveResponse { reservePub: string; static checked: (obj: any) => CreateReserveResponse; +} + + +@Checkable.Class +export class Denomination { + @Checkable.Value(AmountJson) + value: AmountJson; + + @Checkable.String + denom_pub: string; + + @Checkable.Value(AmountJson) + fee_withdraw: AmountJson; + + @Checkable.Value(AmountJson) + fee_deposit: AmountJson; + + @Checkable.Value(AmountJson) + fee_refresh: AmountJson; + + @Checkable.String + stamp_start: string; + + @Checkable.String + stamp_expire_withdraw: string; + + @Checkable.String + stamp_expire_legal: string; + + @Checkable.String + stamp_expire_deposit: string; + + @Checkable.String + master_sig: string; + + @Checkable.Optional(Checkable.String) + pub_hash: string; + + static checked: (obj: any) => Denomination; +} + + +export interface IMintInfo { + baseUrl: string; + masterPublicKey: string; + denoms: Denomination[]; +} + +export interface ReserveCreationInfo { + mintInfo: IMintInfo; + selectedDenoms: Denomination[]; + withdrawFee: AmountJson; }
\ No newline at end of file diff --git a/extension/lib/wallet/wallet.ts b/extension/lib/wallet/wallet.ts index 608876abf..eca937ff1 100644 --- a/extension/lib/wallet/wallet.ts +++ b/extension/lib/wallet/wallet.ts @@ -22,37 +22,49 @@ */ import * as native from "./emscriptif"; -import {AmountJson, CreateReserveResponse} from "./types"; +import {AmountJson, CreateReserveResponse, IMintInfo, Denomination} from "./types"; import {HttpResponse, RequestException} from "./http"; import {Query} from "./query"; import {Checkable} from "./checkable"; import {canonicalizeBaseUrl} from "./helpers"; +import {ReserveCreationInfo} from "./types"; "use strict"; -export interface Mint { - baseUrl: string; - keys: Keys -} - export interface CoinWithDenom { coin: Coin; denom: Denomination; } -export interface Keys { + +@Checkable.Class +export class KeysJson { + @Checkable.List(Checkable.Value(Denomination)) denoms: Denomination[]; -} -export interface Denomination { - value: AmountJson; - denom_pub: string; - fee_withdraw: AmountJson; - fee_deposit: AmountJson; - stamp_expire_withdraw: string; + @Checkable.String + master_public_key: string; + + @Checkable.Any + auditors: any[]; + + @Checkable.String + list_issue_date: string; + + @Checkable.Any + signkeys: any; + + @Checkable.String + eddsa_pub: string; + + @Checkable.String + eddsa_sig: string; + + static checked: (obj: any) => KeysJson; } + export interface PreCoin { coinPub: string; coinPriv: string; @@ -75,6 +87,115 @@ 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; + denoms: Denomination[]; + + constructor(obj: {baseUrl: string} & any) { + this.baseUrl = obj.baseUrl; + + if (obj.denoms) { + this.denoms = Array.from(<Denomination[]>obj.denoms); + } else { + this.denoms = []; + } + + if (typeof obj.masterPublicKey === "string") { + this.masterPublicKey = obj.masterPublicKey; + } + } + + static fresh(baseUrl: string): MintInfo { + return new MintInfo({baseUrl}); + } + + /** + * Merge new key information into the mint info. + * If the new key information is invalid (missing fields, + * invalid signatures), an exception is thrown, but the + * 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; + } + + 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"); + } + // TODO: check if info still matches + found = true; + break; + } + } + + 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); + } + } +} + + @Checkable.Class export class CreateReserveRequest { /** @@ -107,14 +228,14 @@ export class ConfirmReserveRequest { @Checkable.Class -export class MintInfo { +export class MintHandle { @Checkable.String master_pub: string; @Checkable.String url: string; - static checked: (obj: any) => MintInfo; + static checked: (obj: any) => MintHandle; } @@ -144,8 +265,8 @@ export class Contract { @Checkable.String merchant_pub: string; - @Checkable.List(Checkable.Value(MintInfo)) - mints: MintInfo[]; + @Checkable.List(Checkable.Value(MintHandle)) + mints: MintHandle[]; @Checkable.List(Checkable.AnyObject) products: any[]; @@ -274,6 +395,10 @@ 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. @@ -441,7 +566,7 @@ export class Wallet { */ private getPossibleMintCoins(paymentAmount: AmountJson, depositFeeLimit: AmountJson, - allowedMints: MintInfo[]): Promise<MintCoins> { + allowedMints: MintHandle[]): Promise<MintCoins> { // Mapping from mint base URL to list of coins together with their // denomination let m: MintCoins = {}; @@ -639,7 +764,8 @@ export class Wallet { return Query(this.db).put("history", depleted).finish(); }) .catch((e) => { - console.error("Failed to deplete reserve", e.stack); + console.error("Failed to deplete reserve"); + console.error(e); }); } @@ -791,8 +917,8 @@ export class Wallet { /** * Withdraw coins from a reserve until it is empty. */ - private depleteReserve(reserve, mint: Mint): Promise<void> { - let denomsAvailable: Denomination[] = copy(mint.keys.denoms); + private depleteReserve(reserve, mint: MintInfo): Promise<void> { + let denomsAvailable: Denomination[] = copy(mint.denoms); let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, denomsAvailable); @@ -811,7 +937,7 @@ export class Wallet { * Update the information about a reserve that is stored in the wallet * by quering the reserve's mint. */ - private updateReserve(reservePub: string, mint: Mint): Promise<Reserve> { + private updateReserve(reservePub: string, mint: MintInfo): Promise<Reserve> { return Query(this.db) .get("reserves", reservePub) .then((reserve) => { @@ -846,26 +972,58 @@ export class Wallet { } + getReserveCreationInfo(baseUrl: string, + amount: AmountJson): Promise<ReserveCreationInfo> { + return this.updateMintFromUrl(baseUrl) + .then((mintInfo: IMintInfo) => { + let selectedDenoms = getWithdrawDenomList(amount, + mintInfo.denoms); + + let acc = native.Amount.getZero(amount.currency); + for (let d of selectedDenoms) { + acc.add(new native.Amount(d.fee_withdraw)); + } + let ret: ReserveCreationInfo = { + mintInfo, + selectedDenoms, + withdrawFee: acc.toJson(), + }; + return ret; + }); + } + + /** * 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. */ - private updateMintFromUrl(baseUrl): Promise<Mint> { + updateMintFromUrl(baseUrl): Promise<MintInfo> { + baseUrl = canonicalizeBaseUrl(baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl); return this.http.get(reqUrl).then((resp) => { if (resp.status != 200) { throw Error("/keys request failed"); } - let mintKeysJson = JSON.parse(resp.responseText); - if (!mintKeysJson) { - throw new RequestException({url: reqUrl, hint: "keys invalid"}); - } - let mint: Mint = { - baseUrl: baseUrl, - keys: mintKeysJson - }; - return Query(this.db).put("mints", mint).finish().then(() => mint); + let mintKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); + + return Query(this.db).get("mints", baseUrl).then((r) => { + let mint; + + console.log("got mints result"); + console.dir(r); + + if (!r) { + mint = MintInfo.fresh(baseUrl); + console.log("making fresh mint"); + } else { + mint = new MintInfo(r); + console.log("using old mint"); + } + + mint.mergeKeys(mintKeysJson); + return Query(this.db).put("mints", mint).finish().then(() => mint); + }); }); } diff --git a/extension/lib/wallet/wxmessaging.ts b/extension/lib/wallet/wxmessaging.ts index 934984722..1267167d2 100644 --- a/extension/lib/wallet/wxmessaging.ts +++ b/extension/lib/wallet/wxmessaging.ts @@ -19,6 +19,7 @@ import {Wallet, Offer, Badge, ConfirmReserveRequest, CreateReserveRequest} from import {deleteDb, exportDb, openTalerDb} from "./db"; import {BrowserHttpLib} from "./http"; import {Checkable} from "./checkable"; +import {AmountJson} from "./types"; "use strict"; @@ -77,7 +78,11 @@ function makeHandlers(db: IDBDatabase, } catch (e) { if (e instanceof Checkable.SchemaError) { console.error("schema error:", e.message); - return Promise.resolve({error: "invalid contract", hint: e.message, detail: detail}); + return Promise.resolve({ + error: "invalid contract", + hint: e.message, + detail: detail + }); } else { throw e; } @@ -88,6 +93,19 @@ function makeHandlers(db: IDBDatabase, ["execute-payment"]: function(detail) { return wallet.executePayment(detail.H_contract); }, + ["mint-info"]: function(detail) { + if (!detail.baseUrl) { + return Promise.resolve({error: "bad url"}); + } + return wallet.updateMintFromUrl(detail.baseUrl); + }, + ["reserve-creation-info"]: function(detail) { + if (!detail.baseUrl || typeof detail.baseUrl !== "string") { + return Promise.resolve({error: "bad url"}); + } + let amount = AmountJson.checked(detail.amount); + return wallet.getReserveCreationInfo(detail.baseUrl, amount); + }, ["get-history"]: function(detail) { // TODO: limit history length return wallet.getHistory(); @@ -119,7 +137,7 @@ function dispatch(handlers, req, sendResponse) { }) }) .catch((e) => { - console.log("exception during wallet handler'"); + console.log("exception during wallet handler"); console.error(e.stack); sendResponse({ error: "exception", @@ -155,7 +173,18 @@ export function wxMain() { let wallet = new Wallet(db, http, badge); let handlers = makeHandlers(db, wallet); chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - return dispatch(handlers, req, sendResponse) + try { + return dispatch(handlers, req, sendResponse) + } catch (e) { + console.log("exception during wallet handler (dispatch)"); + console.error(e.stack); + sendResponse({ + error: "exception", + hint: e.message, + stack: e.stack.toString() + }); + return false; + } }); }) .catch((e) => { |