summaryrefslogblamecommitdiffstats
path: root/prog/ž/app.html
blob: 544db56fe78e1c8657704e43322b348b55ebe7ca (plain) (tree)








































































































































































































































                                                                                                                                                                                   

                                                                                                                                                         













































































































































































































































                                                                                                                                                                                                                                                                                                                                                                                          
<!DOCTYPE html>
<style>
img {
	display: none
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<label for=sec1>sec1 pubkey or directory entry: </label><input onchange=paypossible() placeholder="02ab83cc.../adrian" id=sec1>
<label for=amount>amount: </label><input onchange=paypossible() type=number min=0 max=4294967296 id=amount>
<br><label for=comment>comment:</label>
<br><textarea maxlength=256 style=width:100%;height:3cm placeholder="2023-06-18 21:06:40
kava v motorinu" id=comment></textarea>
<button onclick=paynow() disabled id=pay>pay</button>
<br><label for=directory>directory:</label>
<br><textarea id=directory style=width:100%;height:3cm placeholder="02ab82cd... anton
0384cc98... adrian
03ccdd99... oliver" onchange=savedir()></textarea>
<br><label for=computer>computer:</label> <input onchange=comp() id=computer placeholder="https://denar.sijanec.eu/api.php?m=" />
<button onclick=upload_transactions()>upload transactions</button>
<button onclick=localStorage.removeItem("last_sync_hash")>remove last sync hash</button>
<div id=log></div>
<label for=jwk>private jwk:</label>
<input onchange=login() placeholder='{"alg":"ES384","crv":"P-384","d":"' id=jwk />
<a href=gen.html>gen.html</a>
<br>my pubkey: <span id=sec1me></span>
<div id=reader style=width:100% ><button onclick=cam() >open camera</button></div>
<input type=checkbox id=allbal onchange=chkbox() checked disabled /><label for=allbal>show all <span id=balcnt></span> balances</label>
<div id=balances></div>
<input type=checkbox id=alltx onchange=chkbox() checked disabled /><label for=alltx>show all <span id=txscnt></span> txs</label>
<div id=txs></div>
<script>
async function try_import_tx (a) {
	if (a.length != tx_len)
		return false;
	let tx = await parse_tx(a);
	if (!tx)
		return false;
	if (!localStorage.getItem("transactions")) {
		localStorage.setItem("transactions", a2hex(a));
		return true;
	}
	let transactions = hex2a(localStorage.getItem("transactions"));
	for (let j = 0; j < transactions.length/tx_len; j++) {
		let oldtx = await parse_tx(transactions.slice(tx_len*j, tx_len*j+tx_len));
		if (a2hex(oldtx.hash) == a2hex(tx.hash))
			return false;
	}
	let new_transactions = new Uint8Array(transactions.length + tx_len);
	new_transactions.set(transactions);
	new_transactions.set(a, transactions.length);
	transactions = new_transactions;
	localStorage.setItem("transactions", a2hex(transactions));
	return true;
}
function onscanfail (error) {
	return;
}
function onscan (text, result) {
	if (typeof(text) == "string" && text.startsWith("U")) {
		text = text.slice(-x.length+1);
		let a = new Uint8Array(text.length);
		for (let i = 0; i < text.length; i++)
			a[i] = text.charCodeAt(i)-32;
		return onscan(a, result);
	}
	if (typeof(text) == "string" && text.startsWith("base64:")) // XXX untested
		return onscan(Uint8Array.from(atob(text.split(":")[1]), c => c.charCodeAt(0)), result);
	if (typeof(text) == "string" && text.startsWith('{') && text.includes("ES384") && text.includes("sign")) {
		jwk.value = text;
		localStorage.setItem("jwk", jwk.value);
		return;
	}
	let a = new Uint8Array(text.length);
	window.scanned = text;
	for (let i = 0; i < text.length; i++)
		a[i] = text.charCodeAt(i);
	console.log("onscan: " + a2hex(a));
	if (a[0] == 0) {
		alert("standard private key qr codes are unsupported, use a jwk privkey qr code");
		return;
	}
	if (a[0] == 1) {
		let p = new Uint8Array(49);
		for (let i = 0; i < 49; i++)
			p[i] = a[1+i];
		let t = new Uint8Array(a.length-50);
		for (let i = 0; i < a.length-50; i++)
			t[i] = a[50+i];
		directory.value += a2hex(p) + " " + new TextDecoder().decode(t).split("\n")[0];
		return;
	}
	if (a[0] == 2) {
		let t = new Uint8Array(tx_len);
		for (let i = 0; i < tx_len; i++)
			t[i] = a[1+i];
		if (!parse_tx(t)) {
			alert("transaction couldn't be parsed or signature check failed");
			return;
		}
		try_import_tx(t);
		return;
	}
	if (a[0] == 3) {
		let r = new Uint8Array(49);
		for (let i = 0; i < 49; i++)
			r[i] = a[1+i];
		sec1.value = a2hex(p);
		amount.value = a[49+1]*256*256*256 + a[49+2]*256*256 + a[49+3]*256 + a[49+4];
		let t = new Uint8Array(a.length-54);
		for (let i = 0; i < a.length-54; i++)
			t[i] = a[54+i];
		comment.value = new TextDecoder().decode(t);
		return;
	}
}
function cam () {
	let qr = new Html5QrcodeScanner("reader", {fps: 10, qrbox: {width: 350, height: 350}}, false);
	qr.render(onscan, onscanfail);
}
const hexchars = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
function a2hex (a) {
        let r = "";
        for (let i = 0; i < a.length; i++) {
                r += hexchars[a[i] >> 4];
                r += hexchars[a[i] % 16];
        }
        return r;
}
function singlehex (h) {
	h = h.toLowerCase();
	if (!hexchars.includes(h))
		return null;
	if (h.charCodeAt(0) >= "a".charCodeAt(0))
		return 10 + h.charCodeAt(0) - "a".charCodeAt(0);
	return h.charCodeAt(0) - "0".charCodeAt(0);
}
function hex2a (s) {
	if (!s)
		return null;
	for (let i = 0; i < s.length; i++)
		if (singlehex(s[i]) === null) {	
			return null;
		}
	let a = new Uint8Array(s.length/2);
	for (let i = 0; i < s.length/2; i++)
		a[i] = singlehex(s[2*i])*16 + singlehex(s[2*i+1]);
	return a;
}
function xytosec1 (x, y) {
	let r = new Uint8Array(49);
	if (y[47] % 2)
		r[0] = 3;
	else
		r[1] = 2;
	for (let i = 0; i < 48; i++)
		r[1+i] = x[i];
	return r;
}
function sec1s_get (k) {
	return hex2a(localStorage.getItem("sec1s_" + k));
}
function sec1s_set (k, v) {
	console.log("sec1s_set: " + k);
	if (k.slice(2, 2+48*2) == a2hex(v).slice(2, 2+48*2))
		return localStorage.setItem("sec1s_" + k, a2hex(v));
}
var sec1s = {}; /// storage for Y derived from X by server
function pubkey_from_sec1uncompressed (a) {
	if (a.length != 48*2+1)
		return null;
	return crypto.subtle.importKey("raw", a, {name: "ECDSA", namedCurve: "P-384"}, true, ["verify"]);
}
async function pubkey_from_sec1 (a) {
	if (!a)
		return null;
	if (a[0] == 4)
		return await pubkey_from_sec1uncompressed(a);
	if (sec1s_get(a2hex(a)))
		return await pubkey_from_sec1uncompressed(sec1s_get(a2hex(a)));
	sec1s_set(a2hex(a), new Uint8Array(await (await fetch(computer.value + "sec1decompress", {method: "POST", body: a})).arrayBuffer()));
	return await pubkey_from_sec1uncompressed(sec1s_get(a2hex(a)));
}
const tx_len = 49*2+4+256+32+48*2;
async function parse_tx (a /* uint8array */) { // also verifies
	if (a.length < 49*2+4+256+32+48*2)
		return null;
	let r = {sender: null, recipient: null, amount: null, comment: null, nonce: null, r: null, s: null, hash: null, commentstr: null, senderkey: null, raw: a};
	r.sender = a.slice(0, 49); 
	r.recipient = a.slice(49, 49+49);
	r.amount = a[49*2]*256*256*256 + a[49*2+1]*256*256 + a[49*2+2]*256 + a[49*2+3];
	r.comment = a.slice(49*2+4, 49*2+4+256);
	r.nonce = a.slice(49*2+4+256, 49*2+4+256+32);
	r.r = a.slice(49*2+4+256+32, 49*2+4+256+32+48);
	r.s = a.slice(49*2+4+256+32+48);
	r.commentstr = new TextDecoder().decode(r.comment);
	r.senderkey = await pubkey_from_sec1(r.sender);
	r.hash = new Uint8Array(await crypto.subtle.digest("SHA-256", a));
	if (await crypto.subtle.verify({name: "ECDSA", hash: "SHA-384"}, r.senderkey, a.slice(-48*2), a.slice(0, tx_len-48*2)))
		return r;
	return false;
}
function upload_transactions () {
	return fetch(computer.value + "transactions", {method: "POST", body: hex2a(localStorage.getItem("transactions"))});
	/* let local_txs = hex2a(localStorage.getItem("transactions"));
	let server_txs = await (await fetch(computer.value + "transactions")).arrayBuffer();
	if (server_txs.length % tx_len) {
		log.innerText += "server transactions response length modulo tx_len isn't zero!";
		return;
	}
	let localtxs = [];
	for (let i = 0; i < local_txs.length/tx_len; i++)
		localtxs.push(local_txs.slice(tx_len*i, tx_len*i+tx_len));
	for (let i = 0; i < server_txs.length/tx_len; i++) {
		let remote_tx = local_txs.slice(tx_len*i, tx_len*i+tx_len);
		while (localtxs.indexOf(remote_tx) !== -1)
			localtxs.splice(localtxs.indexOf(remote_tx), 1);
	}
	localtxs.forEach(tx => {
		fetch(computer.value + "transaction", {method: "POST", body: tx});
	}); */
}
async function sync_transactions () {
	let transactions = hex2a(localStorage.getItem("transactions"));
	if (!transactions)
		transactions = new Uint8Array(0);
	let server_transactions = new Uint8Array(await (await fetch(computer.value + "transactions", {headers: {"After": localStorage.getItem("last_sync_hash")}})).arrayBuffer());
	if (server_transactions.length % tx_len)
		return;
	let count = 0;
	aLoop:
	for (let i = 0; i < server_transactions.length/tx_len; i++)
		if (await try_import_tx(server_transactions.slice(tx_len*i, tx_len*i+tx_len)))
			count++;
	if (server_transactions.length)
		localStorage.setItem("last_sync_hash", a2hex(new Uint8Array(await crypto.subtle.digest("SHA-256", server_transactions.slice(-tx_len)))));
	if (count)
		rendertxsbal();
	return count;
}

async function paynow () {
	let sender = await sec1_from_pubkey(await pubkey_from_string("me"));
	let rcpt = await sec1_from_pubkey(window.recipient);
	let amount32 = new Uint8Array(4);
	amount32[3] = amount.value % 256;
	amount32[2] = (amount.value >> 8) % 256;
	amount32[1] = (amount.value >> 16) % 256;
	amount32[0] = (amount.value >> 24) % 256;
	amount.value = "";
	let comm = new TextEncoder().encode(comment.value);
	let comm256 = new Uint8Array(256);
	comm256[255] = 69; // user agent (nonstandard ofc)
	for (let i = 0; i < 256; i++)
		comm256[i] = comm[i];
	nonce = crypto.getRandomValues(new Uint8Array(32));
	let tx_unsigned = new Uint8Array(tx_len-2*48);
	tx_unsigned.set(sender);
	tx_unsigned.set(rcpt, sender.length);
	tx_unsigned.set(amount32, sender.length+rcpt.length);
	tx_unsigned.set(comm256, sender.length+rcpt.length+amount32.length);
	tx_unsigned.set(nonce, sender.length+rcpt.length+amount32.length+comm256.length);
	let signature = new Uint8Array(await crypto.subtle.sign({name: "ECDSA", hash: "SHA-384"}, key, tx_unsigned));
	let tx_signed = new Uint8Array(tx_unsigned.length+signature.length);
	tx_signed.set(tx_unsigned);
	tx_signed.set(signature, tx_unsigned.length);
	let transactions = hex2a(localStorage.getItem("transactions"));
	let new_transactions = new Uint8Array((transactions ? transactions.length : 0) + tx_signed.length);
	if (transactions)
		new_transactions.set(transactions);
	new_transactions.set(tx_signed, transactions ? transactions.length : 0);
	localStorage.setItem("transactions", a2hex(new_transactions));
	rendertxsbal();
	upload_transactions();
}
function comp () {
	if (!computer.value)
		return;
	if (localStorage.getItem("computer") != computer.value) {
		localStorage.setItem("computer", computer.value);
		localStorage.removeItem("last_sync_hash");
		sync_transactions();
	}
	paypossible();
}
function sec1_compress (k) {
	let r = new Uint8Array(49);
	r[0] = 2;
	if (k[48*2] % 2)
		r[0] = 3;
	for (let i = 0; i < 48; i++)
		r[1+i] = k[1+i];
	return r;
}
async function sec1_from_pubkey (k) {
	return sec1_compress(new Uint8Array(await crypto.subtle.exportKey("raw", k)));
}
async function pubkey_from_string (s) {
	if (s == "me")
		return await pubkey_from_sec1(hex2a(sec1me.innerText));
	if (await pubkey_from_sec1(hex2a(s))) {
		return await pubkey_from_sec1(hex2a(s));
	}
	let d = directory.value.split("\n");
	for (let i = 0; i < d.length; i++) {
		if (d[i].includes(s)) {
			return await pubkey_from_sec1(hex2a(d[i].split(" ")[0]));
		}
	}
	return false;
}
async function paypossible () {
	if (amount.value == "") {
		console.log("paypossible: empty amount field");
		pay.disabled = true;
		return;
	}
	if (!(Number(amount.value) <= 4294967296 && Number(amount.value) >= 0)) {
		console.log("paypossible: amount invalid");
		pay.disabled = true;
		return;
	}
	if (!key.usages.includes("sign")) {
		console.log("paypossible: bad privkey");
		pay.disabled = true;
		return;
	}
	window.recipient = await pubkey_from_string(sec1.value);
	if (recipient == false) {
		console.log("paypossible: recipient pubkey bad");
		pay.disabled = true;
		return;
	}
	pay.disabled = false;
}
function resolvepubkey (a) {
	if (a2hex(a) == sec1me.innerText)
		return "me";
	let d = directory.value.split("\n");
	for (let i = 0; i < d.length; i++)
		if (d[i].includes(a2hex(a).slice(-48*2)))
			return d[i].split(" ").slice(1).join(" ");
	return a2hex(a);
}
function draw_canvas (qr, scale, border, light, dark, canvas) {
	canvas.width = canvas.height = (qr.size + border * 2) * scale;
	let ctx = canvas.getContext("2d");
	for (let y = -border; y < qr.size + border; y++) {
		for (let x = -border; x < qr.size + border; x++) {
			ctx.fillStyle = qr.getModule(x, y) ? dark : light;
			ctx.fillRect((x + border) * scale, (y + border) * scale, scale, scale);
		}
	}
}
function txqr (seq, h) {
	let a = hex2a(h);
	let d = new Uint8Array(a.length+1);
	d.set(a, 1)
	d[0] = 2;
	// draw_canvas(qrcodegen.QrCode.encodeBinary(d, qrcodegen.QrCode.Ecc.LOW), 5, 4, "#FFF", "#000", document.getElementById("cnv" + seq));
	let s = "U";
	for (let i = 0; i < d.length; i++)
		s += String.fromCharCode(32+d[i]);
	draw_canvas(qrcodegen.QrCode.encodeText(s, qrcodegen.QrCode.Ecc.LOW), 5, 4, "#FFF", "#000", document.getElementById("cnv" + seq));
	document.getElementById("cnv" + seq).hidden = false;
	document.getElementById("btn" + seq).hidden = true;
	document.getElementById("br" + seq).hidden = false;
}
async function rendertxsbal () {
	let transactions = hex2a(localStorage.getItem("transactions") ?? "");
	if (!transactions) {
		txs.innerHTML = "no transactions";
		balances.innerHTML = "no transactions";
		return;
	}
	let trans = [];
	let balan = {};
	for (let j = 0; j < transactions.length/tx_len; j++) {
		let tx = await parse_tx(transactions.slice(tx_len*j, tx_len*j+tx_len));
		trans.push(tx);
		if (!Object.keys(balan).includes(resolvepubkey(tx.sender)))
			balan[resolvepubkey(tx.sender)] = 0;
		if (!Object.keys(balan).includes(resolvepubkey(tx.recipient)))
			balan[resolvepubkey(tx.recipient)] = 0;
		balan[resolvepubkey(tx.recipient)] += tx.amount;
		balan[resolvepubkey(tx.sender)] -= tx.amount;
	}
	txscnt.innerText = trans.length;
	txs.innerHTML = "";
	let tx = null;
	let seq = 0;
	while (tx = trans.pop())
		if (alltx.checked || resolvepubkey(tx.sender) == "me" || resolvepubkey(tx.recipient) == "me") {
			txs.innerHTML += "<hr><canvas hidden='' id=cnv" + seq + "></canvas> <button id=btn" + seq + " onclick=txqr(" + seq + ",'" + a2hex(tx.raw) + "')>qr</button><br id=br" + seq + " hidden>sender: " + resolvepubkey(tx.sender) + "<br>recipient: " + resolvepubkey(tx.recipient) + "<br>amount: " + tx.amount + "<br>comment: " + tx.commentstr.replace("<", "&lt;");
			seq++;
		}
	balances.innerHTML = "";
	Object.keys(balan).forEach(c => {
		if (allbal.checked || c == "me")
			balances.innerHTML += c + ": " + balan[c] + "<br>"
	});
	balcnt.innerText = Object.keys(balan).length;
}
async function chkbox () {
	if (allbal.checked)
		localStorage.setItem("allbal", "true");
	else
		localStorage.setItem("allbal", "false");
	if (alltx.checked)
		localStorage.setItem("alltx", "true");
	else
		localStorage.setItem("alltx", "false");
	rendertxsbal();
}
async function login () {
	localStorage.setItem("jwk", jwk.value);
	window.key = await crypto.subtle.importKey("jwk", JSON.parse(jwk.value), {name: "ECDSA", namedCurve: "P-384"}, true, ["sign"]);
	if (key.usages.includes("sign")) {
		allbal.disabled = false;
		alltx.disabled = false;
		if (localStorage.getItem("alltx") == "true")
			alltx.checked = true;
		else
			alltx.checked = false;
		if (localStorage.getItem("allbal") == "true")
			allbal.checked = true;
		else
			allbal.checked = false;
		chkbox();
		let mysec1 = new Uint8Array(49);
		if (Uint8Array.from(atob(JSON.parse(jwk.value).y.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0))[47] % 2)
			mysec1[0] = 3;
		else
			mysec1[0] = 2;
		for (let i = 0; i < 48; i++)
			mysec1[1+i] = Uint8Array.from(atob(JSON.parse(jwk.value).x.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0))[i];
		sec1me.innerText = a2hex(mysec1);
	} else {
		console.log("login: privkey not ok");
		sec1me.innerText = "";
		allbal.disabled = true;
		allbal.checked = true;
		alltx.checked = true;
		alltx.disabled = true;
	}
	paypossible();
}
function savedir () {
	localStorage.setItem("directory", directory.value);
}
async function main () {
	directory.value = localStorage.getItem("directory");
	computer.value = localStorage.getItem("computer");
	jwk.value = localStorage.getItem("jwk");
	if (jwk.value != "")
		await login();
	await comp();
	let push = false;
	while (true) {
		sync_transactions();
		if (location.protocol == "file:" || !push)
			await new Promise(r => setTimeout(r, 2000));
		else {
			let resp = await fetch(computer.value + "push", {method: "POST", body: hex2a(localStorage.getItem("last_sync_hash"))})
			if (!resp.ok)
				await new Promise(r => setTimeout(r, 2000));
		}
	}
	rendertxsbal();
}
main();
</script>
<script src=QR-Code-generator/typescript-javascript/qrcodegen.js></script>
<script src=node_modules/html5-qrcode/html5-qrcode.min.js></script>