Support ECIES encryption for payment destination

This commit is contained in:
Anton Kovalenko 2022-01-07 17:51:29 +03:00
parent d51e27689f
commit 5e69a9bedf
4 changed files with 119 additions and 23 deletions

40
package-lock.json generated

@ -1,15 +1,18 @@
{ {
"name": "svelte-app", "name": "lnurl-pay.me",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "svelte-app", "name": "lnurl-pay.me",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@imask/svelte": "^6.1.0", "@imask/svelte": "^6.1.0",
"@noble/hashes": "^0.5.9",
"@noble/secp256k1": "^1.4.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"jscrypto": "^1.0.2",
"kjua": "^0.9.0", "kjua": "^0.9.0",
"sirv-cli": "^1.0.0", "sirv-cli": "^1.0.0",
"sveltestrap": "^5.4.0", "sveltestrap": "^5.4.0",
@ -74,6 +77,16 @@
"svelte": ">=3.0.0" "svelte": ">=3.0.0"
} }
}, },
"node_modules/@noble/hashes": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-0.5.9.tgz",
"integrity": "sha512-7lN1Qh6d8DUGmfN36XRsbN/WcGIPNtTGhkw26vWId/DlCIGsYJJootTtPGghTLcn/AaXPx2Q0b3cacrwXa7OVw=="
},
"node_modules/@noble/secp256k1": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.4.0.tgz",
"integrity": "sha512-cYpUbQ2uitPgf5QuQnpi8Nu+ZmQjSDunFKw6vvxaOSkbMUhCf4K723WLUuuK1K/sf6H/dvqKbmEAeop5i3qTJg=="
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.15", "version": "1.0.0-next.15",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
@ -629,6 +642,14 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true "dev": true
}, },
"node_modules/jscrypto": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/jscrypto/-/jscrypto-1.0.2.tgz",
"integrity": "sha512-r+oNJLGTv1nkNMBBq3c70xYrFDgJOYVgs2OHijz5Ht+0KJ0yObD0oYxC9mN72KLzVfXw+osspg6t27IZvuTUxw==",
"bin": {
"jscrypto": "bin/cli.js"
}
},
"node_modules/kjua": { "node_modules/kjua": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz", "resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz",
@ -1220,6 +1241,16 @@
"imask": "^6.1.0" "imask": "^6.1.0"
} }
}, },
"@noble/hashes": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-0.5.9.tgz",
"integrity": "sha512-7lN1Qh6d8DUGmfN36XRsbN/WcGIPNtTGhkw26vWId/DlCIGsYJJootTtPGghTLcn/AaXPx2Q0b3cacrwXa7OVw=="
},
"@noble/secp256k1": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.4.0.tgz",
"integrity": "sha512-cYpUbQ2uitPgf5QuQnpi8Nu+ZmQjSDunFKw6vvxaOSkbMUhCf4K723WLUuuK1K/sf6H/dvqKbmEAeop5i3qTJg=="
},
"@polka/url": { "@polka/url": {
"version": "1.0.0-next.15", "version": "1.0.0-next.15",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
@ -1660,6 +1691,11 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true "dev": true
}, },
"jscrypto": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/jscrypto/-/jscrypto-1.0.2.tgz",
"integrity": "sha512-r+oNJLGTv1nkNMBBq3c70xYrFDgJOYVgs2OHijz5Ht+0KJ0yObD0oYxC9mN72KLzVfXw+osspg6t27IZvuTUxw=="
},
"kjua": { "kjua": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz", "resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz",

@ -21,7 +21,10 @@
}, },
"dependencies": { "dependencies": {
"@imask/svelte": "^6.1.0", "@imask/svelte": "^6.1.0",
"@noble/hashes": "^0.5.9",
"@noble/secp256k1": "^1.4.0",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"jscrypto": "^1.0.2",
"kjua": "^0.9.0", "kjua": "^0.9.0",
"sirv-cli": "^1.0.0", "sirv-cli": "^1.0.0",
"sveltestrap": "^5.4.0", "sveltestrap": "^5.4.0",

@ -8,6 +8,7 @@
import InputMask from './InputMask.svelte'; import InputMask from './InputMask.svelte';
import payways from './payways.js'; import payways from './payways.js';
import QR from './QR.svelte'; import QR from './QR.svelte';
import CTC from './CTC.svelte'; import CTC from './CTC.svelte';
@ -17,7 +18,8 @@
import UTF8 from 'utf-8' import UTF8 from 'utf-8'
import PayFlow from './PayFlow.svelte'; import PayFlow from './PayFlow.svelte';
import { ecEncrypt } from './ecies.js';
let payway = payways[0]; let payway = payways[0];
setTimeout(()=>{ setTimeout(()=>{
@ -28,8 +30,12 @@
let inputId; let inputId;
let amountMask; let amountMask;
let realAmount; let realAmount;
let extAccount;
let briefAccount;
let encrypt = false;
$: inputId = payway.iid||payway.id; $: inputId = payway.iid||payway.id;
$: accountReady = accountComplete && accounts[inputId]
$: amountMask = { $: amountMask = {
mask: Number, scale:2, mask: Number, scale:2,
min: payway.min, max:payway.max, min: payway.min, max:payway.max,
@ -39,6 +45,10 @@
$: realAmount = amounts[payway.id] && ( $: realAmount = amounts[payway.id] && (
Math.max(Math.min(amounts[payway.id], payway.max),payway.min)) Math.max(Math.min(amounts[payway.id], payway.max),payway.min))
$: extAccount = accountReady ? (encrypt? "0g" + ecEncrypt(accounts[inputId].padEnd(8)): accounts[inputId]):"";
$: briefAccount = extAccount ? (encrypt ? extAccount.slice(0,28)+"…" : extAccount):""
function genAutoMemo(payway,account,amount) { function genAutoMemo(payway,account,amount) {
if (!account) if (!account)
@ -52,16 +62,12 @@
} }
let autoMemo; let autoMemo;
$: autoMemo = genAutoMemo(payway, $: autoMemo = genAutoMemo(payway, briefAccount, realAmount)
accountComplete && accounts[inputId],
realAmount)
let lightningAddress; let lightningAddress;
$: lightningAddress = accountComplete ? $: lightningAddress = accountComplete ? genAddress(payway, extAccount, realAmount):""
genAddress(payway, accounts[inputId], realAmount):""
function lnurlEncode(url) { function lnurlEncode(url) {
return bech32.encode("LNURL", return bech32.encode("LNURL", bech32.toWords(UTF8.setBytesFromString(url)),2048)
bech32.toWords(UTF8.setBytesFromString(url)),2048)
} }
function toHexString(byteArray) { function toHexString(byteArray) {
@ -69,6 +75,7 @@
return ('0' + (byte & 0xFF).toString(16)).slice(-2); return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('') }).join('')
} }
function genAddress(payway,account,amount) { function genAddress(payway,account,amount) {
if (!account || !payway) if (!account || !payway)
return "" return ""
@ -80,11 +87,12 @@
return prefix + account + "@" + payway.id + ".lnurl-pay.me" return prefix + account + "@" + payway.id + ".lnurl-pay.me"
} }
function genLNURL(payway,account,amount,memo) { function genLNURL(payway,account,amount,memo,isEncrypted) {
let params = new URLSearchParams(); let params = new URLSearchParams();
params.set("mtg","pay"); params.set("mtg","pay");
params.set("p",payway.id); params.set("p",payway.id);
params.set("acc",toHexString(UTF8.setBytesFromString(account))) // don't hex-encode bech32-encoded account
params.set("acc",isEncrypted? account: toHexString(UTF8.setBytesFromString(account)))
params.set("v","1") params.set("v","1")
if (amount) { if (amount) {
params.set(payway.currency.toLowerCase(),amount) params.set(payway.currency.toLowerCase(),amount)
@ -95,6 +103,7 @@
return lnurlEncode("https://lnurl-pay.me/pay?"+params.toString()).toUpperCase() return lnurlEncode("https://lnurl-pay.me/pay?"+params.toString()).toUpperCase()
} }
let accounts = {}; let accounts = {};
let amounts = {}; let amounts = {};
let memo; let memo;
@ -102,7 +111,7 @@
let lnurl; let lnurl;
$: lnurl = accountComplete ? $: lnurl = accountComplete ?
genLNURL(payway, accounts[inputId], realAmount, memo) : ""; genLNURL(payway, extAccount, realAmount, memo, encrypt) : "";
const curr = { const curr = {
"UAH":"₴", "UAH":"₴",
@ -138,15 +147,26 @@
{#key payway} {#key payway}
<label class="form-label mb-3">{payway.acc} <label class="form-label mb-3">{payway.acc}
<InputMask <div class="input-group">
unmask <InputMask
bind:value={accounts[inputId]} unmask
bind:isComplete={accountComplete} bind:value={accounts[inputId]}
imask={payway.imask||{mask:/.*/}} bind:isComplete={accountComplete}
autocomplete={payway.autocomplete} imask={payway.imask||{mask:/.*/}}
inputmode={payway.inputmode||"numeric"} autocomplete={payway.autocomplete}
placeholder={payway.placeholder} inputmode={payway.inputmode||"numeric"}
class="form-control"/> placeholder={payway.placeholder}
class="form-control"/>
<span class="input-group-text">
<div class="form-check-inline form-check input-group-prepend">
<input class="form-check-input" type="checkbox" id="cbEncrypt"
bind:checked={encrypt}>
<label class="form-check-label" for="cbEncrypt">
Encrypt
</label>
</div>
</span>
</div>
</label> </label>
<label class="form-label mb-3"> <label class="form-label mb-3">
Amount: {payway.currency} {payway.min} {payway.max} Amount: {payway.currency} {payway.min} {payway.max}
@ -243,7 +263,6 @@
</CTC> </CTC>
{/if} {/if}
</SiteCard> </SiteCard>
</SiteDeck> </SiteDeck>

38
src/ecies.js Normal file

@ -0,0 +1,38 @@
const receiverPublicKey = "02d38170b929dc26be2192071756edd5fb94c4e0cf1aec352ce4c8245e635d9bed";
import {hkdf} from "@noble/hashes/hkdf";
import {sha256} from "@noble/hashes/sha256";
import * as jscrypto from "jscrypto";
import * as secp from "@noble/secp256k1";
import { bech32 } from 'bech32';
let receiverPoint;
function getReceiverPoint() {
if (receiverPoint)
return receiverPoint;
receiverPoint = secp.Point.fromHex(receiverPublicKey);
return receiverPoint;
}
const noIV = jscrypto.Hex.parse("00000000000000000000000000000000");
export function ecEncrypt(plainText) {
const pub = getReceiverPoint();
const iv = noIV;
const ek = secp.utils.randomPrivateKey();
const epk = secp.Point.fromPrivateKey(ek);
const sharedSecret = secp.getSharedSecret(ek, pub);
const key = hkdf(sha256,new Uint8Array([...epk.toRawBytes(),...sharedSecret]));
const jskkey = new jscrypto.Word32Array(key);
const ed = jscrypto.AES.encrypt(jscrypto.Utf8.parse(plainText),
jskkey,{mode:jscrypto.mode.GCM, padding:jscrypto.pad.NoPadding, iv: noIV});
const at = jscrypto.mode.GCM.mac(jscrypto.AES,jskkey,noIV,null,ed.cipherText,16);
const ctbytes = new Uint8Array([...at.toUint8Array(),
...epk.toRawBytes(true),
...ed.cipherText.toUint8Array()]);
const bech = bech32.encode("",bech32.toWords(ctbytes), 1024);
return bech.slice(1,-6)
}