check contract hash, fix unicode bug

This commit is contained in:
Florian Dold 2016-09-28 23:41:34 +02:00
parent 29909a27f5
commit 274204c21e
8 changed files with 198 additions and 51 deletions

View File

@ -76,8 +76,8 @@ namespace TalerNotify {
console.log("it's execute");
document.documentElement.style.visibility = "hidden";
taler.internalExecutePayment(resp.contractHash,
resp.payUrl,
resp.offerUrl);
resp.payUrl,
resp.offerUrl);
}
});
}
@ -163,38 +163,62 @@ namespace TalerNotify {
return;
}
const walletMsg = {
type: "check-repurchase",
detail: {
contract: offer.contract
},
if (!offer.H_contract) {
console.error("H_contract field missing");
return;
}
let walletHashContractMsg = {
type: "hash-contract",
detail: {contract: offer.contract}
};
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
if (resp.error) {
console.error("wallet backend error", resp);
chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => {
if (!resp.hash) {
console.log("error", resp);
throw Error("hashing failed");
}
if (resp.hash != offer.H_contract) {
console.error("merchant-supplied contract hash is wrong");
return;
}
if (resp.isRepurchase) {
console.log("doing repurchase");
console.assert(resp.existingFulfillmentUrl);
console.assert(resp.existingContractHash);
window.location.href = subst(resp.existingFulfillmentUrl,
resp.existingContractHash);
} else {
const uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
const params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
};
const target = uri.query(params).href();
if (msg.replace_navigation === true) {
document.location.replace(target);
} else {
document.location.href = target;
const walletMsg = {
type: "check-repurchase",
detail: {
contract: offer.contract
},
};
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
if (resp.error) {
console.error("wallet backend error", resp);
return;
}
}
if (resp.isRepurchase) {
console.log("doing repurchase");
console.assert(resp.existingFulfillmentUrl);
console.assert(resp.existingContractHash);
window.location.href = subst(resp.existingFulfillmentUrl,
resp.existingContractHash);
} else {
const uri = URI(chrome.extension.getURL(
"pages/confirm-contract.html"));
const params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
};
const target = uri.query(params).href();
if (msg.replace_navigation === true) {
document.location.replace(target);
} else {
document.location.href = target;
}
}
});
});
});

View File

@ -33,6 +33,8 @@ export interface EmscFunGen {
export declare namespace Module {
var cwrap: EmscFunGen;
function stringToUTF8(s: string, addr: number, maxLength: number): void
function _free(ptr: number): void;
function _malloc(n: number): number;

View File

@ -176,6 +176,10 @@ export class CryptoApi {
return this.doRpc("createPreCoin", 1, denom, reserve);
}
hashString(str: string): Promise<string> {
return this.doRpc("hashString", 1, str);
}
hashRsaPub(rsaPub: string): Promise<string> {
return this.doRpc("hashRsaPub", 2, rsaPub);
}

View File

@ -139,6 +139,11 @@ namespace RpcFunctions {
}
export function hashString(str: string): string {
const b = native.ByteArray.fromString(str);
return b.hash().toCrock();
}
export function hashRsaPub(rsaPub: string): string {
return native.RsaPublicKey.fromCrock(rsaPub)

View File

@ -36,8 +36,9 @@ const GNUNET_SYSERR = -1;
let Module = EmscWrapper.Module;
let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(null,
args);
let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(
null,
args);
var emsc = {
free: (ptr: number) => Module._free(ptr),
@ -395,6 +396,30 @@ export class Amount extends ArenaObject {
}
/**
* Count the UTF-8 characters in a JavaScript string.
*/
function countBytes(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.
* Should contain only data, not pointers.
@ -632,17 +657,20 @@ export class ByteArray extends PackedArenaObject {
}
static fromString(s: string, a?: Arena): ByteArray {
let hstr = emscAlloc.malloc(s.length + 1);
Module.writeStringToMemory(s, hstr);
return new ByteArray(s.length, hstr, a);
// UTF-8 bytes, including 0-terminator
let terminatedByteLength = countBytes(s) + 1;
let hstr = emscAlloc.malloc(terminatedByteLength);
Module.stringToUTF8(s, hstr, terminatedByteLength);
return new ByteArray(terminatedByteLength, hstr, a);
}
static fromCrock(s: string, a?: Arena): ByteArray {
let hstr = emscAlloc.malloc(s.length + 1);
Module.writeStringToMemory(s, hstr);
let decodedLen = Math.floor((s.length * 5) / 8);
let byteLength = countBytes(s) + 1;
let hstr = emscAlloc.malloc(byteLength);
Module.stringToUTF8(s, hstr, byteLength);
let decodedLen = Math.floor((byteLength * 5) / 8);
let ba = new ByteArray(decodedLen, undefined, a);
let res = emsc.string_to_data(hstr, s.length, ba.nativePtr, decodedLen);
let res = emsc.string_to_data(hstr, byteLength, ba.nativePtr, decodedLen);
emsc.free(hstr);
if (res != GNUNET_OK) {
throw Error("decoding failed");

View File

@ -45,12 +45,22 @@ import {ExchangeHandle} from "./types";
"use strict";
export interface CoinWithDenom {
coin: Coin;
denom: Denomination;
}
interface ReserveRecord {
reserve_pub: string;
reserve_priv: string,
exchange_base_url: string,
created: number,
last_query: number|null,
current_amount: null,
requested_amount: AmountJson,
confirmed: boolean,
}
@Checkable.Class
export class KeysJson {
@ -124,6 +134,13 @@ export class Offer {
static checked: (obj: any) => Offer;
}
export interface HistoryRecord {
type: string;
timestamp: number;
subjectId?: string;
detail: any;
}
interface ExchangeCoins {
[exchangeUrl: string]: CoinWithDenom[];
@ -145,6 +162,32 @@ export interface Badge {
stopBusy(): void;
}
export function canonicalJson(obj: any): string {
// Check for cycles, etc.
JSON.stringify(obj);
if (typeof obj == "string" || typeof obj == "number" || obj === null) {
return JSON.stringify(obj)
}
if (Array.isArray(obj)) {
let objs: string[] = obj.map((e) => canonicalJson(e));
return `[${objs.join(',')}]`;
}
let keys: string[] = [];
for (let key in obj) {
keys.push(key);
}
keys.sort();
let s = "{";
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
if (i != keys.length - 1) {
s += ",";
}
}
return s + "}";
}
function deepEquals(x: any, y: any): boolean {
if (x === y) {
@ -467,6 +510,7 @@ export class Wallet {
let historyEntry = {
type: "pay",
timestamp: (new Date).getTime(),
subjectId: `contract-${offer.H_contract}`,
detail: {
merchantName: offer.contract.merchant.name,
amount: offer.contract.amount,
@ -485,6 +529,11 @@ export class Wallet {
}
async putHistory(historyEntry: HistoryRecord): Promise<void> {
await Query(this.db).put("history", historyEntry).finish();
}
/**
* Add a contract to the wallet and sign coins,
* but do not send them yet.
@ -574,7 +623,7 @@ export class Wallet {
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
*/
private async processReserve(reserveRecord: any,
private async processReserve(reserveRecord: ReserveRecord,
retryDelayMs: number = 250): Promise<void> {
const opId = "reserve-" + reserveRecord.reserve_pub;
this.startOperation(opId);
@ -586,9 +635,11 @@ export class Wallet {
await this.depleteReserve(reserve, exchange);
let depleted = {
type: "depleted-reserve",
subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
timestamp: (new Date).getTime(),
detail: {
reservePub: reserveRecord.reserve_pub,
currentAmount: reserveRecord.current_amount,
}
};
await Query(this.db).put("history", depleted).finish();
@ -630,7 +681,7 @@ export class Wallet {
const now = (new Date).getTime();
const canonExchange = canonicalizeBaseUrl(req.exchange);
const reserveRecord = {
const reserveRecord: ReserveRecord = {
reserve_pub: keypair.pub,
reserve_priv: keypair.priv,
exchange_base_url: canonExchange,
@ -644,6 +695,7 @@ export class Wallet {
const historyEntry = {
type: "create-reserve",
timestamp: now,
subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
detail: {
requestedAmount: req.amount,
reservePub: reserveRecord.reserve_pub,
@ -674,26 +726,28 @@ export class Wallet {
*/
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
const now = (new Date).getTime();
let reserve: ReserveRecord = await Query(this.db)
.get("reserves", req.reservePub);
const historyEntry = {
type: "confirm-reserve",
timestamp: now,
subjectId: `reserve-progress-${reserve.reserve_pub}`,
detail: {
reservePub: req.reservePub,
requestedAmount: reserve.requested_amount,
}
};
let r = await Query(this.db)
.get("reserves", req.reservePub);
if (!r) {
if (!reserve) {
console.error("Unable to confirm reserve, not found in DB");
return;
}
r.confirmed = true;
reserve.confirmed = true;
await Query(this.db)
.put("reserves", r)
.put("reserves", reserve)
.put("history", historyEntry)
.finish();
this.processReserve(r);
this.processReserve(reserve);
}
@ -801,8 +855,10 @@ export class Wallet {
let historyEntry = {
type: "reserve-update",
timestamp: (new Date).getTime(),
subjectId: `reserve-progress-${reserve.reserve_pub}`,
detail: {
reservePub,
requestedAmount: reserve.requested_amount,
oldAmount,
newAmount
}
@ -1040,6 +1096,10 @@ export class Wallet {
return {history};
}
async hashContract(contract: any): Promise<string> {
return this.cryptoApi.hashString(canonicalJson(contract));
}
/**
* Check if there's an equivalent contract we've already purchased.
*/

View File

@ -151,6 +151,20 @@ function makeHandlers(db: IDBDatabase,
}
return wallet.updateExchangeFromUrl(detail.baseUrl);
},
["hash-contract"]: function(detail) {
if (!detail.contract) {
return Promise.resolve({error: "contract missing"});
}
return wallet.hashContract(detail.contract).then((hash) => {
return {hash};
});
},
["put-history-entry"]: function(detail: any) {
if (!detail.historyEntry) {
return Promise.resolve({error: "historyEntry missing"});
}
return wallet.putHistory(detail.historyEntry);
},
["reserve-creation-info"]: function(detail, sender) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({error: "bad url"});

View File

@ -30,7 +30,7 @@
import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
import {Wallet} from "../lib/wallet/wallet";
import {HistoryRecord} from "../lib/wallet/wallet";
import {AmountJson} from "../lib/wallet/types";
declare var m: any;
@ -173,7 +173,7 @@ function retryPayment(url: string, contractHash: string) {
}
function formatHistoryItem(historyItem: any) {
function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail;
const t = historyItem.timestamp;
console.log("hist item", historyItem);
@ -215,7 +215,7 @@ namespace WalletHistory {
}
class Controller {
myHistory: any;
myHistory: any[];
gotError = false;
constructor() {
@ -241,14 +241,24 @@ namespace WalletHistory {
}
export function view(ctrl: Controller) {
let history = ctrl.myHistory;
let history: HistoryRecord[] = ctrl.myHistory;
if (ctrl.gotError) {
return i18n`Error: could not retrieve event history`;
}
if (!history) {
throw Error("Could not retrieve history");
}
let listing = _.map(history, formatHistoryItem);
let subjectMemo: {[s: string]: boolean} = {};
let listing: any[] = [];
for (let record of history.reverse()) {
//if (record.subjectId && subjectMemo[record.subjectId]) {
// return;
//}
subjectMemo[record.subjectId as string] = true;
listing.push(formatHistoryItem(record));
}
if (listing.length > 0) {
return m("div.container", listing);
}