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",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "svelte-app",
"name": "lnurl-pay.me",
"version": "1.0.0",
"dependencies": {
"@imask/svelte": "^6.1.0",
"@noble/hashes": "^0.5.9",
"@noble/secp256k1": "^1.4.0",
"bech32": "^2.0.0",
"jscrypto": "^1.0.2",
"kjua": "^0.9.0",
"sirv-cli": "^1.0.0",
"sveltestrap": "^5.4.0",
@ -74,6 +77,16 @@
"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": {
"version": "1.0.0-next.15",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
@ -629,6 +642,14 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"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": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz",
@ -1220,6 +1241,16 @@
"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": {
"version": "1.0.0-next.15",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz",
@ -1660,6 +1691,11 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"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": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/kjua/-/kjua-0.9.0.tgz",

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

@ -8,6 +8,7 @@
import InputMask from './InputMask.svelte';
import payways from './payways.js';
import QR from './QR.svelte';
import CTC from './CTC.svelte';
@ -17,6 +18,7 @@
import UTF8 from 'utf-8'
import PayFlow from './PayFlow.svelte';
import { ecEncrypt } from './ecies.js';
let payway = payways[0];
@ -28,8 +30,12 @@
let inputId;
let amountMask;
let realAmount;
let extAccount;
let briefAccount;
let encrypt = false;
$: inputId = payway.iid||payway.id;
$: accountReady = accountComplete && accounts[inputId]
$: amountMask = {
mask: Number, scale:2,
min: payway.min, max:payway.max,
@ -40,6 +46,10 @@
$: realAmount = amounts[payway.id] && (
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) {
if (!account)
return "...automatic";
@ -52,16 +62,12 @@
}
let autoMemo;
$: autoMemo = genAutoMemo(payway,
accountComplete && accounts[inputId],
realAmount)
$: autoMemo = genAutoMemo(payway, briefAccount, realAmount)
let lightningAddress;
$: lightningAddress = accountComplete ?
genAddress(payway, accounts[inputId], realAmount):""
$: lightningAddress = accountComplete ? genAddress(payway, extAccount, realAmount):""
function lnurlEncode(url) {
return bech32.encode("LNURL",
bech32.toWords(UTF8.setBytesFromString(url)),2048)
return bech32.encode("LNURL", bech32.toWords(UTF8.setBytesFromString(url)),2048)
}
function toHexString(byteArray) {
@ -69,6 +75,7 @@
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('')
}
function genAddress(payway,account,amount) {
if (!account || !payway)
return ""
@ -80,11 +87,12 @@
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();
params.set("mtg","pay");
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")
if (amount) {
params.set(payway.currency.toLowerCase(),amount)
@ -95,6 +103,7 @@
return lnurlEncode("https://lnurl-pay.me/pay?"+params.toString()).toUpperCase()
}
let accounts = {};
let amounts = {};
let memo;
@ -102,7 +111,7 @@
let lnurl;
$: lnurl = accountComplete ?
genLNURL(payway, accounts[inputId], realAmount, memo) : "";
genLNURL(payway, extAccount, realAmount, memo, encrypt) : "";
const curr = {
"UAH":"₴",
@ -138,6 +147,7 @@
{#key payway}
<label class="form-label mb-3">{payway.acc}
<div class="input-group">
<InputMask
unmask
bind:value={accounts[inputId]}
@ -147,6 +157,16 @@
inputmode={payway.inputmode||"numeric"}
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 class="form-label mb-3">
Amount: {payway.currency} {payway.min} {payway.max}
@ -243,7 +263,6 @@
</CTC>
{/if}
</SiteCard>
</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)
}