diff options
author | Anton Luka Šijanec <anton@sijanec.eu> | 2023-06-20 01:51:29 +0200 |
---|---|---|
committer | Anton Luka Šijanec <anton@sijanec.eu> | 2023-06-20 01:51:29 +0200 |
commit | 530b0fe326d8a9623e27e081b51512cdf1d5b5d7 (patch) | |
tree | dfe2b901bf10f9acd416c22f61b475b379e52b44 | |
parent | download changes to website update (diff) | |
download | r-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 |
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
m--------- | prog/ž/QR-Code-generator | 0 | ||||
-rw-r--r-- | prog/ž/app.html | 473 | ||||
-rw-r--r-- | prog/ž/composer.json | 13 | ||||
-rw-r--r-- | prog/ž/gen.html | 60 | ||||
-rw-r--r-- | prog/ž/index.php | 311 | ||||
-rw-r--r-- | prog/ž/package.json | 5 | ||||
-rwxr-xr-x | prog/ž/test.php | 72 |
9 files changed, 942 insertions, 0 deletions
@@ -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("<", "<"); + 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; |