summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAnton Luka Šijanec <anton@sijanec.eu>2023-06-20 01:51:29 +0200
committerAnton Luka Šijanec <anton@sijanec.eu>2023-06-20 01:51:29 +0200
commit530b0fe326d8a9623e27e081b51512cdf1d5b5d7 (patch)
treedfe2b901bf10f9acd416c22f61b475b379e52b44
parentdownload changes to website update (diff)
downloadr-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar.gz
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar.bz2
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar.lz
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar.xz
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.tar.zst
r-530b0fe326d8a9623e27e081b51512cdf1d5b5d7.zip
-rw-r--r--.gitignore5
-rw-r--r--.gitmodules3
m---------prog/ž/QR-Code-generator0
-rw-r--r--prog/ž/app.html473
-rw-r--r--prog/ž/composer.json13
-rw-r--r--prog/ž/gen.html60
-rw-r--r--prog/ž/index.php311
-rw-r--r--prog/ž/package.json5
-rwxr-xr-xprog/ž/test.php72
9 files changed, 942 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 88d1567..23baf12 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,8 @@ core
*.out
.gdb_history
__pycache__/
+db
+composer.lock
+package-lock.json
+vendor/
+node_modules/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..edd56a2
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "prog/ž/QR-Code-generator"]
+ path = prog/ž/QR-Code-generator
+ url = https://github.com/nayuki/QR-Code-generator
diff --git a/prog/ž/QR-Code-generator b/prog/ž/QR-Code-generator
new file mode 160000
+Subproject 22fac31bdf81da68730c177c0e931c93234d2a3
diff --git a/prog/ž/app.html b/prog/ž/app.html
new file mode 100644
index 0000000..f5601e1
--- /dev/null
+++ b/prog/ž/app.html
@@ -0,0 +1,473 @@
+<!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++;
+ window.lsh = a2hex(new Uint8Array(await crypto.subtle.digest("SHA-256", server_transactions.slice(-tx_len))));
+ localStorage.setItem("last_sync_hash", lsh);
+ 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>
diff --git a/prog/ž/composer.json b/prog/ž/composer.json
new file mode 100644
index 0000000..63b5b5e
--- /dev/null
+++ b/prog/ž/composer.json
@@ -0,0 +1,13 @@
+{
+ "name": "sijanec/zcaron",
+ "type": "project",
+ "require": {
+ "mdanter/ecc": "^1.0"
+ },
+ "authors": [
+ {
+ "name": "Anton Luka Šijanec",
+ "email": "anton@sijanec.eu"
+ }
+ ]
+}
diff --git a/prog/ž/gen.html b/prog/ž/gen.html
new file mode 100644
index 0000000..47fe749
--- /dev/null
+++ b/prog/ž/gen.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<h1>private</h1>
+<canvas id=privkey></canvas>
+<div id=privd></div>
+<h1>public</h1>
+<canvas id=pubkey></canvas>
+<div id=pubsec1></div>
+<script src=QR-Code-generator/typescript-javascript/qrcodegen.js></script>
+<script>
+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 a2hex (a) {
+ let r = "";
+ const hexchars = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
+ for (let i = 0; i < a.length; i++) {
+ r += hexchars[a[i] >> 4];
+ r += hexchars[a[i] % 16];
+ }
+ return r;
+}
+async function main () {
+ while (!pubsec1.innerText.startsWith("02aa") && !pubsec1.innerText.startsWith("03aa")) {
+ window.keyobj = await crypto.subtle.generateKey(
+ {
+ name: "ECDSA",
+ namedCurve: "P-384",
+ },
+ true,
+ ["sign", "verify"]
+ );
+ let jwk = await crypto.subtle.exportKey("jwk", keyobj.privateKey);
+ let d = jwk.d.replace(/-/g, "+").replace(/_/g, "/");
+ let x = (await crypto.subtle.exportKey("jwk", keyobj.publicKey)).x.replace(/-/g, "+").replace(/_/g, "/");
+ let y = (await crypto.subtle.exportKey("jwk", keyobj.publicKey)).y.replace(/-/g, "+").replace(/_/g, "/");
+ d = Uint8Array.from(atob(d), c => c.charCodeAt(0));
+ privd.innerText = a2hex(d) + "(big endian d) jwk: " + JSON.stringify(jwk);
+ draw_canvas(qrcodegen.QrCode.encodeText(JSON.stringify(jwk), qrcodegen.QrCode.Ecc.LOW), 5, 4, "#FFF", "#000", privkey);
+ x = Uint8Array.from(atob(x), c => c.charCodeAt(0));
+ y = Uint8Array.from(atob(y), c => c.charCodeAt(0));
+ var sec1 = new Uint8Array(49);
+ if (y[47] % 2)
+ sec1[0] = 3;
+ else
+ sec1[0] = 2;
+ for (let i = 0; i < 48; i++)
+ sec1[1+i] = x[i];
+ pubsec1.innerText = a2hex(sec1) + " (sec-1)";
+ draw_canvas(qrcodegen.QrCode.encodeBinary(sec1, qrcodegen.QrCode.Ecc.LOW), 10, 4, "#FFF", "#000", pubkey);
+ }
+}
+main();
+</script>
diff --git a/prog/ž/index.php b/prog/ž/index.php
new file mode 100644
index 0000000..5b46ce1
--- /dev/null
+++ b/prog/ž/index.php
@@ -0,0 +1,311 @@
+<?php
+require_once "vendor/autoload.php";
+use Mdanter\Ecc\Crypto\Signature\SignHasher;
+use Mdanter\Ecc\Crypto\Key\PublicKey;
+use Mdanter\Ecc\Primitives\Point;
+use Mdanter\Ecc\EccFactory;
+use Mdanter\Ecc\Crypto\Signature\Signer;
+use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
+use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
+use Mdanter\Ecc\Math;
+use Mdanter\Ecc\Primitives\CurveFp;
+use Mdanter\Ecc\Crypto\Signature;
+use Mdanter\Ecc\Math\GmpMath;
+$adapter = EccFactory::getAdapter();
+$curve = EccFactory::getNistCurves()->curve384();
+$generator = EccFactory::getNistCurves()->generator384();
+$useDerandomizedSignatures = true;
+$algorithm = 'sha384';
+$math = new GmpMath();
+function sec1parse ($in) {
+ switch ($in[0]) {
+ case "\x02":
+ $isOdd = false;
+ break;
+ case "\x03":
+ $isOdd = true;
+ break;
+ default:
+ return null;
+ }
+ global $math;
+ global $curve;
+ $x = $math->stringToInt(substr($in, 1, 48));
+ $y = $curve->recoverYfromX($isOdd, $x);
+ global $adapter;
+ global $generator;
+ return new PublicKey($adapter, $generator, new Point($adapter, $curve, $x, $y));
+}
+class Transaction {
+ public $sender;
+ public $recipient;
+ public $amount;
+ public $comment;
+ public $nonce;
+ public $r;
+ public $s;
+ public function parse ($in) {
+ $this->sender = substr($in, 0, 49);
+ $this->recipient = substr($in, 49, 49);
+ $amount = substr($in, 49*2, 4);
+ $this->amount = unpack("N", $amount)[1];
+ $this->comment = substr($in, 49*2+4, 256);
+ $this->nonce = substr($in, 49*2+4+256, 32);
+ $this->r = substr($in, 49*2+4+256+32, 48);
+ $this->s = substr($in, 49*2+4+256+32+48, 48);
+ }
+ public function serialize ($without_signature = false) {
+ return str_pad($this->sender, 49, "\0") . str_pad($this->recipient, 49, "\0") . pack("N", $this->amount) . str_pad($this->comment, 256, "\0") . str_pad($this->nonce, 32, "\0") . ($without_signature ? "" : (str_pad($this->r, 48, "\0") . str_pad($this->s, 48, "\0")));
+ }
+ public function verify () {
+ global $adapter;
+ global $generator;
+ global $algorithm;
+ global $math;
+ $signer = new Signer($adapter);
+ $publickey = sec1parse($this->sender);
+ $hasher = new SignHasher($algorithm, $adapter);
+ $hash = $hasher->makeHash($this->serialize(true), $generator);
+ return $signer->verify($publickey, new \Mdanter\Ecc\Crypto\Signature\Signature($math->stringToInt($this->r), $math->stringToInt($this->s)), $hash);
+ }
+ public function hash () {
+ return hash("sha256", $this->serialize(), true);
+ }
+}
+function tx_from_row($row) {
+ $tx = new Transaction();
+ $tx->sender = $row["sender"];
+ $tx->recipient = $row["recipient"];
+ $tx->amount = $row["amount"];
+ $tx->comment = $row["comment"];
+ $tx->nonce = $row["nonce"];
+ $tx->r = $row["r"];
+ $tx->s = $row["s"];
+ return $tx;
+}
+function last_tx ($db) {
+ foreach ($db->query("select * from transactions order by id desc limit 1") as $row);
+ if ($row)
+ return tx_from_row($row);
+ return;
+}
+if (!empty($_REQUEST["src"])) {
+ header("Content-Type: text/plain");
+ die(file_get_contents($_SERVER["SCRIPT_FILENAME"]));
+}
+if ($_SERVER["REQUEST_METHOD"] == "OPTIONS") {
+ http_response_code(204);
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header("Access-Control-Allow-Headers: *");
+ header("Access-Control-Max-Age: 86400");
+ die();
+}
+define("TEXT", "text/plain");
+function response ($code, $body="", $type="application/octet-stream") {
+ http_response_code($code);
+ header("Content-Type: " . $type);
+ header("Access-Control-Allow-Origin: *");
+ header("Access-Control-Allow-Methods: *");
+ header("Access-Control-Allow-Headers: *");
+ header("Access-Control-Max-Age: 86400");
+ echo $body;
+}
+if (($ret = @file_get_contents("error_status.txt")) !== false) {
+ response(500, $ret, TEXT);
+ die();
+}
+function computers_post_handler ($in, $db, $forcepost=false) {
+ $numcomp = sizeof($db->query("select url from computers"));
+ if (strlen($in) % 256) {
+ return [413, "content length should've been divisible by 256", TEXT];
+ }
+ $in = str_split($in, 256);
+ $stmt = $db->prepare("insert or ignore into computers (url) values (:url)");
+ foreach ($in as $url) {
+ $stmt->bindParam(":url", $url, PDO::PARAM_LOB);
+ $stmt->execute();
+ }
+ $stmt = null;
+ $computers = [];
+ foreach ($db->query("select url from computers") as $url)
+ $computers[] = $url;
+ if ($numcomp != sizeof($computers) || $forcepost) {
+ foreach ($computers as $url) // this would be better with curl parallel/multi
+ file_get_contents(explode("\0", $url)[0] . "computers", false, stream_context_create(["http" => ["method" => "POST", "content" => implode("", $computers), "timeout" => 1]]));
+ return [201];
+ } else {
+ return [202];
+ }
+}
+function transactions_post_handler ($in, $db) {
+ $tx = new Transaction();
+ $txlen = strlen($tx->serialize());
+ if (strlen($in) % $txlen) {
+ return [469, "body length should've been divisible by $txlen", TEXT];
+ }
+ $in = str_split($in, $txlen);
+ foreach ($in as $txstr) {
+ $tx->parse($txstr);
+ if (!$tx->verify())
+ continue;
+ $stmt = $db->prepare("select * from transactions where hash=:hash");
+ $txhash = $tx->hash();
+ $stmt->bindParam(":hash", $txhash, PDO::PARAM_LOB);
+ $stmt->execute();
+ if ($stmt->rowCount())
+ continue;
+ $stmt = null;
+ $stmt = $db->prepare("insert or ignore into transactions (sender, recipient, amount, comment, nonce, r, s, hash) values (:sender, :recipient, :amount, :comment, :nonce, :r, :s, :hash)");
+ $stmt->bindParam(":sender", $tx->sender, PDO::PARAM_LOB);
+ $stmt->bindParam(":recipient", $tx->recipient, PDO::PARAM_LOB);
+ $stmt->bindParam(":amount", $tx->amount, PDO::PARAM_LOB);
+ $stmt->bindParam(":comment", $tx->comment, PDO::PARAM_LOB);
+ $stmt->bindParam(":nonce", $tx->nonce, PDO::PARAM_LOB);
+ $stmt->bindParam(":r", $tx->r, PDO::PARAM_LOB);
+ $stmt->bindParam(":s", $tx->s, PDO::PARAM_LOB);
+ $stmt->bindParam(":hash", $txhash, PDO::PARAM_LOB);
+ $stmt->execute();
+ $stmt = null;
+ $computers = [];
+ foreach ($db->query("select url from computers") as $url)
+ $computers[] = $url;
+ foreach ($computers as $url)
+ file_get_contents(explode("\0", $url)[0] . "transaction", false, stream_context_create(["http" => ["method" => "POST", "content" => $in, "timeout" => 1]]));
+ }
+ return [200];
+}
+function transactions_get_handler ($db, $after) {
+ $response = "";
+ $ret = $db->query("select * from transactions order by id");
+ $hash = null;
+ $stmt = $db->prepare("select * from transactions where hash=:hash");
+ $stmt->bindParam(":hash", $after, PDO::PARAM_LOB);
+ $stmt->execute();
+ if ($stmt->fetch())
+ $hash = $after;
+ $stmt = null;
+ foreach ($ret as $row)
+ if ($hash) {
+ if ($hash == tx_from_row($row)->hash())
+ $hash = null;
+ } else
+ $response .= tx_from_row($row)->serialize();
+ if ($response == "")
+ return [204];
+ return [200, $response];
+}
+function sync_checkpoint_computer ($db, $url) {
+ $stmt = $db->prepare("select last_hash from computers where url=:url");
+ $stmt->bindParam(":url", $url, PDO::PARAM_LOB);
+ $stmt->execute();
+ return $stmt->fetchColumn(0);
+}
+# create table computers (url TEXT NOT NULL UNIQUE CHECK(length(url) == 256), last_hash TEXT NOT NULL UNIQUE CHECK(length(last_hash) == 32), date default CURRENT_TIMESTAMP);
+# create table transactions (id integer primary key autoincrement, sender TEXT NOT NULL CHECK(length(sender) == 49), recipient TEXT NOT NULL CHECK(length(recipient) == 49), amount INTEGER NOT NULL CHECK(amount >= 0), comment TEXT NOT NULL CHECK(length(comment) == 256), nonce TEXT NOT NULL CHECK(length(nonce) == 32), r TEXT NOT NULL CHECK(length(r) == 48), s TEXT NOT NULL CHECK(length(s) == 48), hash TEXT NOT NULL UNIQUE CHECK(length(hash) == 32), date default CURRENT_TIMESTAMP);
+$db = new PDO("sqlite:db", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
+if (!$db)
+ response(503, "db: " . $e->getMessage(), TEXT);
+switch ($_REQUEST["e"] . "-" . $_SERVER["REQUEST_METHOD"]) {
+ case "sec1decompress-GET":
+ $x = $math->intToString(sec1parse(hex2bin($_REQUEST["s"]))->getPoint()->getX());
+ $y = $math->intToString(sec1parse(hex2bin($_REQUEST["s"]))->getPoint()->getY());
+ response(200, "\x04$x$y");
+ break;
+ case "sec1decompress-POST":
+ $in = file_get_contents("php://input");
+ global $math;
+ $x = $math->intToString(sec1parse($in)->getPoint()->getX());
+ $y = $math->intToString(sec1parse($in)->getPoint()->getY());
+ response(200, "\x04$x$y");
+ break;
+ case "push-POST":
+ $in = file_get_contents("php://input");
+ for ($i = 0; $i < 60; $i++) {
+ $resp = transactions_get_handler($db, $in);
+ if ($resp[0] == 200) {
+ response(...$resp);
+ break;
+ }
+ usleep(250000);
+ }
+ if ($resp[0] != 200)
+ response(204);
+ break;
+ case "jutro-GET":
+ $computers = [];
+ foreach ($db->query("select url from computers") as $url)
+ $computers[] = $url;
+ $send = "";
+ foreach ($computers as $url) {
+ $recvd = file_get_contents(explode("\0", $url)[0] . "computers");
+ if (strlen($recvd) % 256) {
+ error_log("server $url returned non mod256 computers get response length", 3, "log");
+ continue;
+ }
+ $send .= $recvd;
+ }
+ computers_post_handler($send, $db, true);
+ $computers = [];
+ foreach ($db->query("select url from computers") as $url)
+ $computers[] = $url;
+ foreach ($computers as $url) {
+ $transactions = file_get_contents(explode("\0", $url[0])[0] . "transactions", false, stream_context_create(["http" => ["header" => "After: " . bin2hex(sync_checkpoint_computer($db, $url)) . "\r\n", "timeout" => 1]]));
+ $tx = new Transaction();
+ if (strlen($transactions) % strlen($tx->serialize())) {
+ error_log("server $url returned not correct mod for transactions response length", 3, "log");
+ continue;
+ }
+ foreach (str_split($transactions, strlen($tx->serialize())) as $transaction) {
+ $tx->parse($transaction);
+ $txhash = $tx->hash;
+ $stmt = $db->prepare("update computers set last_hash=:last_hash where url=:url");
+ $stmt->bindParam(":last_hash", $txhash, PDO::PARAM_LOB);
+ $stmt->bindParam(":url", $url, PDO::PARAM_LOB);
+ transactions_post_handler($transaction);
+ }
+ }
+ break;
+ case "computers-GET":
+ $ret = $db->query("select url from computers");
+ response(200);
+ foreach ($ret as $row)
+ echo $row[0];
+ break;
+ case "computers-POST":
+ $in = file_get_contents("php://input");
+ response(...computers_post_handler($in, $db));
+ break;
+ case "transactions-POST":
+ $in = file_get_contents("php://input");
+ response(...transactions_post_handler($in, $db));
+ break;
+ case "transactions-GET":
+ response(...transactions_get_handler($db, hex2bin($_SERVER["HTTP_AFTER"])));
+ break;
+ case "state-GET":
+ $ret = $db->query("select * from transactions order by id");
+ $out = "";
+ $balances = [];
+ foreach ($ret as $row) {
+ $tx = tx_from_row($row);
+ if (!$tx->verify()) {
+ $message = "transaction with internal id {$row["id"]} has an invalid signature.";
+ file_put_contents("error_status.txt", $message);
+ response(500, $message);
+ break 2;
+ }
+ @$balances[$tx->sender] -= $tx->amount;
+ @$balances[$tx->recipient] += $tx->amount;
+ }
+ response(200);
+ foreach ($balances as $key => $value) // do not trust balances provided by this API, since they
+ $packed = pack("q", $value); // are cast to machine dependent int by php
+ if (pack("Q", 123) === pack("P", 123)) // machine is little endian
+ $packed = strrev($packed);
+ echo $key . $packed;
+ break;
+ default:
+ response(400, "unknown endpoint or method not allowed", TEXT);
+ break;
+}
diff --git a/prog/ž/package.json b/prog/ž/package.json
new file mode 100644
index 0000000..8cfcfb2
--- /dev/null
+++ b/prog/ž/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "html5-qrcode": "^2.3.8"
+ }
+}
diff --git a/prog/ž/test.php b/prog/ž/test.php
new file mode 100755
index 0000000..dc3ab50
--- /dev/null
+++ b/prog/ž/test.php
@@ -0,0 +1,72 @@
+#!/usr/bin/php
+<?php
+
+require "vendor/autoload.php";
+
+use Mdanter\Ecc\Crypto\Signature\SignHasher;
+use Mdanter\Ecc\Crypto\Key\PublicKey;
+use Mdanter\Ecc\Primitives\Point;
+use Mdanter\Ecc\EccFactory;
+use Mdanter\Ecc\Crypto\Signature\Signer;
+use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
+use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
+use Mdanter\Ecc\Math;
+use Mdanter\Ecc\Math\GmpMath;
+
+// ECDSA domain is defined by curve/generator/hash algorithm,
+// which a verifier must be aware of.
+
+$adapter = EccFactory::getAdapter();
+$generator = EccFactory::getNistCurves()->generator384();
+$useDerandomizedSignatures = true;
+$algorithm = 'sha384';
+$derSerializer = new DerPrivateKeySerializer($adapter);
+
+## generate der key
+$private = $generator->createPrivateKey();
+echo "privkey: " . $private->getSecret() . PHP_EOL;
+$der = $derSerializer->serialize($private);
+$math = new GmpMath();
+// echo bin2hex($math->intToString($private->getSecret())) . PHP_EOL;
+// echo bin2hex($der) . PHP_EOL;
+
+## You'll be restoring from a key, as opposed to generating one.
+$key = $derSerializer->parse($der);
+
+$document = 'I am writing today...';
+
+$hasher = new SignHasher($algorithm, $adapter);
+$hash = $hasher->makeHash($document, $generator);
+
+echo "message: $document" . PHP_EOL;
+echo "hash: $hash" . PHP_EOL;
+
+# Derandomized signatures are not necessary, but is avoids
+# the risk of a low entropy RNG, causing accidental reuse
+# of a k value for a different message, which leaks the
+# private key.
+if ($useDerandomizedSignatures) {
+ $random = \Mdanter\Ecc\Random\RandomGeneratorFactory::getHmacRandomGenerator($key, $hash, $algorithm);
+} else {
+ $random = \Mdanter\Ecc\Random\RandomGeneratorFactory::getRandomGenerator();
+}
+$randomK = $random->generate($generator->getOrder());
+
+$signer = new Signer($adapter);
+$signature = $signer->sign($key, $hash, $randomK);
+
+# $serializer = new DerSignatureSerializer();
+# $serializedSig = $serializer->serialize($signature);
+# echo base64_encode($serializedSig) . PHP_EOL;
+
+echo "signature: r=" . $signature->getR() . " s=" . $signature->getS() . PHP_EOL;
+
+$pubkey = $key->getPublicKey();
+$x = $pubkey->getPoint()->getX();
+$y = $pubkey->getPoint()->getY();
+
+echo "public key: x=" . $x . " y=" . $y . PHP_EOL;
+
+$publickey = new PublicKey($adapter, $generator, new Point($adapter, EccFactory::getNistCurves()->curve384(), $x, $y));
+
+echo "signature check " . ($signer->verify($publickey, $signature, $hash) ? "passed" : "failed") . PHP_EOL;