mirror of
https://github.com/alexbosworth/balanceofsatoshis.git
synced 2026-08-13 12:33:37 +02:00
merge changes
This commit is contained in:
parent
8cf056b1d0
commit
8fb2aa8314
5 changed files with 174 additions and 65 deletions
152
lnurl/auth.js
152
lnurl/auth.js
|
|
@ -1,24 +1,23 @@
|
|||
const asyncAuto = require('async/auto');
|
||||
const {bech32} = require('bech32');
|
||||
const {createHash} = require('crypto');
|
||||
const {createHmac} = require('crypto');
|
||||
const {returnResult} = require('asyncjs-util');
|
||||
const {signMessage} = require('ln-service');
|
||||
const {ecdsaSign} = require('secp256k1');
|
||||
const {publicKeyCreate} = require('secp256k1');
|
||||
const {signatureExport} = require('secp256k1');
|
||||
const tinysecp = require('tiny-secp256k1');
|
||||
|
||||
const signAuthChallenge = require('./sign_auth_challenge');
|
||||
|
||||
const actionKey = 'action';
|
||||
const {decode} = bech32;
|
||||
const defaultAction = 'authenticate';
|
||||
const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
|
||||
const bech32CharLimit = 2000;
|
||||
const bytesToHexString = (bytes) => bytes.reduce((memo, i) => memo + ('0' + i.toString(16)).slice(-2), "");
|
||||
const challengeKey = 'k1';
|
||||
const errorStatus = 'ERROR';
|
||||
const hexToUint8Array = (n) => new Uint8Array(n.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
||||
const knownActions = ['auth', 'link', 'login', 'register'];
|
||||
const okStatus = 'OK';
|
||||
const prefix = 'lnurl';
|
||||
const lnurlAuthCanonicalPhrase = "USE THIS PHRASE TO DERIVE HASHING KEY";
|
||||
const sha256 = n => createHash('sha256').update(n).digest();
|
||||
const sha256hmac = (key, url) => createHmac('sha256', key).update(url).digest();
|
||||
const stringToUint8Array = (n) => Uint8Array.from(n, x => x.charCodeAt(0));
|
||||
const tlsProtocol = 'https:';
|
||||
const lud13AuthPhrase = 'DO NOT EVER SIGN THIS TEXT WITH YOUR PRIVATE KEYS! IT IS ONLY USED FOR DERIVATION OF LNURL-AUTH HASHING-KEY, DISCLOSING ITS SIGNATURE WILL COMPROMISE YOUR LNURL-AUTH IDENTITY AND MAY LEAD TO LOSS OF FUNDS!';
|
||||
const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
|
||||
|
||||
/** Authenticate using lnurl
|
||||
|
|
@ -36,6 +35,9 @@ const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
|
|||
module.exports = (args, cbk) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return asyncAuto({
|
||||
// Import the ECPair library
|
||||
ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp),
|
||||
|
||||
// Check arguments
|
||||
validate: cbk => {
|
||||
if (!args.ask) {
|
||||
|
|
@ -43,7 +45,7 @@ module.exports = (args, cbk) => {
|
|||
}
|
||||
|
||||
if (!args.lnurl) {
|
||||
return cbk([400, 'ExpectedUrlToAuthenticateUsing']);
|
||||
return cbk([400, 'ExpectedUrlToAuthenticateToLnurl']);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -57,98 +59,118 @@ module.exports = (args, cbk) => {
|
|||
}
|
||||
|
||||
if (!args.lnd) {
|
||||
return cbk([400, 'ExpectedLndToAuthenticateUsing']);
|
||||
return cbk([400, 'ExpectedLndToAuthenticateUsingLnurl']);
|
||||
}
|
||||
|
||||
if (!args.logger) {
|
||||
return cbk([400, 'ExpectedLoggerToAuthenticateUsing']);
|
||||
return cbk([400, 'ExpectedLoggerToAuthenticateUsingLnurl']);
|
||||
}
|
||||
|
||||
if (!args.request) {
|
||||
return cbk([400, 'ExpectedRequestFunctionToGetLnurlAuthenticationData']);
|
||||
return cbk([400, 'ExpectedRequestFunctionToGetLnurlAuthentication']);
|
||||
}
|
||||
|
||||
return cbk();
|
||||
},
|
||||
|
||||
// Parse lnurl
|
||||
parseLnurl: ['validate', ({}, cbk) => {
|
||||
// Parse the encoded Lnurl
|
||||
parse: ['validate', ({}, cbk) => {
|
||||
const {words} = decode(asLnurl(args.lnurl), bech32CharLimit);
|
||||
|
||||
const url = wordsAsUtf8(words);
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (err) {
|
||||
return cbk([503, 'ExpectedValidCallbackUrlInDecodedLnurlForAuthentication']);
|
||||
return cbk([400, 'ExpectedValidCallbackUrlInDecodedLnurlForAuth']);
|
||||
}
|
||||
|
||||
const decodeUrl = new URL(url);
|
||||
const k1 = decodeUrl.searchParams.get('k1');
|
||||
const domain = decodeUrl.hostname;
|
||||
const {hostname, protocol, searchParams} = new URL(url);
|
||||
|
||||
if (protocol !== tlsProtocol) {
|
||||
return cbk([501, 'UnsupportedUrlProtocolForLnurlAuthentication']);
|
||||
}
|
||||
|
||||
const action = searchParams.get(actionKey);
|
||||
|
||||
if (!!action && !knownActions.includes(action)) {
|
||||
return cbk([503, 'UnknownAuthenticationActionForLnurlAuth']);
|
||||
}
|
||||
|
||||
const k1 = searchParams.get(challengeKey);
|
||||
|
||||
if (!k1) {
|
||||
return cbk([503, 'ExpectedK1InDecodedLnurlForAuthentication']);
|
||||
return cbk([503, 'ExpectedChallengeK1ValueInDecodedLnurlForAuth']);
|
||||
}
|
||||
|
||||
return cbk(null, {domain, k1, url});
|
||||
return cbk(null, {hostname, k1, url, action: action || defaultAction});
|
||||
}],
|
||||
|
||||
// Sign the Canonical Phrase
|
||||
signMessage: ['validate', ({}, cbk) => {
|
||||
return signMessage({lnd: args.lnd, message: lnurlAuthCanonicalPhrase}, cbk);
|
||||
// Sign the canonical phrase for LUD-13 signMessage based seed generation
|
||||
seed: ['parse', ({}, cbk) => {
|
||||
return signMessage({lnd: args.lnd, message: lud13AuthPhrase}, cbk);
|
||||
}],
|
||||
|
||||
// Derive keys and get signatures
|
||||
getSignatures: ['parseLnurl', 'signMessage', ({parseLnurl, signMessage}, cbk) => {
|
||||
const {k1} = parseLnurl;
|
||||
const {domain} = parseLnurl;
|
||||
const {signature} = signMessage;
|
||||
|
||||
const hashingKey = sha256(stringToUint8Array(signature));
|
||||
|
||||
const linkingKeyPriv = sha256hmac(hashingKey, stringToUint8Array(domain));
|
||||
const linkingKeyPub = publicKeyCreate(linkingKeyPriv, true);
|
||||
|
||||
const signedMessage = ecdsaSign(hexToUint8Array(k1), linkingKeyPriv);
|
||||
const signedMessageDER = signatureExport(signedMessage.signature)
|
||||
sign: ['ecp', 'parse', 'seed', ({ecp, parse, seed}, cbk) => {
|
||||
const sign = signAuthChallenge({
|
||||
ecp,
|
||||
hostname: parse.hostname,
|
||||
k1: parse.k1,
|
||||
seed: seed.signature,
|
||||
});
|
||||
|
||||
return cbk(null, {
|
||||
sig: bytesToHexString(signedMessageDER),
|
||||
key: bytesToHexString(linkingKeyPub),
|
||||
public_key: sign.public_key,
|
||||
signature: sign.signature,
|
||||
});
|
||||
}],
|
||||
|
||||
// Authenticate using lnurl
|
||||
auth: [
|
||||
'getSignatures',
|
||||
'parseLnurl',
|
||||
({getSignatures, parseLnurl}, cbk) => {
|
||||
const {url} = parseLnurl;
|
||||
const {key} = getSignatures;
|
||||
const {sig} = getSignatures;
|
||||
// Display confirmation dialog with domain name and action
|
||||
ok: ['parse', 'sign', ({parse, sign}, cbk) => {
|
||||
return args.ask({
|
||||
default: true,
|
||||
message: `Do you want to ${parse.action} with ${parse.hostname}?`,
|
||||
name: 'ok',
|
||||
type: 'confirm',
|
||||
},
|
||||
({ok}) => cbk(null, ok));
|
||||
}],
|
||||
|
||||
const qs = {key, sig};
|
||||
return args.request({url, qs, json: true}, (err, r, json) => {
|
||||
if (!!err) {
|
||||
return cbk([503, 'FailedToGetLnurlAuthenticationData', {err}]);
|
||||
}
|
||||
// Transmit authenticating signature and key to the host
|
||||
send: ['ok', 'parse', 'sign', ({ok, parse, sign}, cbk) => {
|
||||
if (!ok) {
|
||||
return cbk([400, 'AuthenticationCanceled']);
|
||||
}
|
||||
|
||||
if (!json) {
|
||||
return cbk([503, 'ExpectedJsonObjectReturnedInLnurlResponseForAuthentication']);
|
||||
}
|
||||
args.logger.info({sending_authentication: sign.public_key});
|
||||
|
||||
if (json.status === errorStatus) {
|
||||
return cbk([503, 'LnurlAuthenticationReturnedErr', {err: json.reason}]);
|
||||
}
|
||||
return args.request({
|
||||
json: true,
|
||||
qs: {key: sign.public_key, sig: sign.signature},
|
||||
url: parse.url,
|
||||
},
|
||||
(err, r, json) => {
|
||||
if (!!err) {
|
||||
return cbk([503, 'FailedToGetLnurlAuthenticationData', {err}]);
|
||||
}
|
||||
|
||||
if (json.status !== 'OK') {
|
||||
return cbk([503, 'ExpectedStatusToBeOkInLnurlResponseJsonForAuthentication']);
|
||||
}
|
||||
if (!json) {
|
||||
return cbk([503, 'ExpectedJsonReturnedInLnurlResponseForAuth']);
|
||||
}
|
||||
|
||||
args.logger.info({is_authenticated: true});
|
||||
if (json.status === errorStatus) {
|
||||
return cbk([503, 'LnurlAuthenticationFail', {err: json.reason}]);
|
||||
}
|
||||
|
||||
return cbk();
|
||||
});
|
||||
if (json.status !== okStatus) {
|
||||
return cbk([503, 'ExpectedOkStatusInLnurlResponseJsonForAuth']);
|
||||
}
|
||||
|
||||
args.logger.info({is_authenticated: true});
|
||||
|
||||
return cbk();
|
||||
});
|
||||
}],
|
||||
},
|
||||
returnResult({reject, resolve}, cbk));
|
||||
|
|
|
|||
37
lnurl/der_encode_signature.js
Normal file
37
lnurl/der_encode_signature.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const asDer = n => (n[0]&128)?Buffer.concat([Buffer.alloc(1),n],1+n.length):n;
|
||||
const bufferAsHex = buffer => buffer.toString('hex');
|
||||
const {concat} = Buffer;
|
||||
const decomposeSignature = sig => [sig.slice(0, 32), sig.slice(32, 64)];
|
||||
const {from} = Buffer;
|
||||
const header = 0x30;
|
||||
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
||||
const int = 0x02;
|
||||
|
||||
/** DER encode a signature given r and s values
|
||||
|
||||
{
|
||||
signature: <Signature Buffer Object>
|
||||
}
|
||||
|
||||
@returns
|
||||
{
|
||||
encoded: <DER Encoded Signature Buffer Object>
|
||||
}
|
||||
*/
|
||||
module.exports = ({signature}) => {
|
||||
// Split the signature for DER encoding
|
||||
const [r, s] = decomposeSignature(hexAsBuffer(signature)).map(asDer);
|
||||
|
||||
const encoded = bufferAsHex(concat([
|
||||
from([header]), // Header byte indicating compound structure
|
||||
from([r.length + s.length + [int, int, r.length, s.length].length]), // Len
|
||||
from([int]), // Integer indicator
|
||||
from([r.length]), // Length of data
|
||||
r,
|
||||
from([int]), // Integer indicator
|
||||
from([s.length]), // Length of data
|
||||
s,
|
||||
]));
|
||||
|
||||
return {encoded};
|
||||
};
|
||||
46
lnurl/sign_auth_challenge.js
Normal file
46
lnurl/sign_auth_challenge.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
const {createHash} = require('crypto');
|
||||
const {createHmac} = require('crypto');
|
||||
|
||||
const derEncodeSignature = require('./der_encode_signature');
|
||||
|
||||
const asDer = n => (n[0]&128)?Buffer.concat([Buffer.alloc(1),n],1+n.length):n;
|
||||
const bufferAsHex = buffer => buffer.toString('hex');
|
||||
const {from} = Buffer;
|
||||
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
||||
const hmacSha256 = (pk, url) => createHmac('sha256', pk).update(url).digest();
|
||||
const sha256 = n => createHash('sha256').update(n).digest();
|
||||
const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8');
|
||||
|
||||
/** Sign an authentication challenge for LNURL Auth
|
||||
|
||||
{
|
||||
ecp: <ECPair Object>
|
||||
hostname: <Domain for Authentication Challenge String>
|
||||
k1: <Challenge Nonce String>
|
||||
seed: <Seed Signature String>
|
||||
}
|
||||
|
||||
@returns
|
||||
{
|
||||
public_key: <Signing Identity Public Key Hex String>
|
||||
signature: <Signature For Authentication Challenge Hex String>
|
||||
}
|
||||
*/
|
||||
module.exports = ({ecp, hostname, k1, seed}) => {
|
||||
// LUD-13: LN wallet defines hashingKey as sha256(signature)
|
||||
const hashingKey = sha256(utf8AsBuffer(seed));
|
||||
|
||||
// LUD-13: linkingPrivKey is defined as hmacSha256(hashingKey, domain)
|
||||
const linkingPrivKey = hmacSha256(hashingKey, utf8AsBuffer(hostname));
|
||||
|
||||
// Instantiate the key pair from this derived private key
|
||||
const linkingKey = ecp.fromPrivateKey(linkingPrivKey);
|
||||
|
||||
// Using the host-specific linking key, sign the challenge k1 value
|
||||
const signature = bufferAsHex(from(linkingKey.sign(hexAsBuffer(k1))));
|
||||
|
||||
return {
|
||||
public_key: bufferAsHex(linkingKey.publicKey),
|
||||
signature: derEncodeSignature({signature}).encoded,
|
||||
};
|
||||
};
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -27,6 +27,7 @@
|
|||
"colorette": "2.0.16",
|
||||
"crypto-js": "4.1.1",
|
||||
"csv-parse": "5.0.4",
|
||||
"ecpair": "2.0.1",
|
||||
"goldengate": "11.2.1",
|
||||
"grammy": "1.7.3",
|
||||
"hot-formula-parser": "4.0.0",
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
"sanitize-filename": "1.6.3",
|
||||
"socks-proxy-agent": "6.2.0-beta.0",
|
||||
"table": "6.8.0",
|
||||
"tiny-secp256k1": "2.2.1",
|
||||
"update-notifier": "5.1.0",
|
||||
"window-size": "1.1.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
"colorette": "2.0.16",
|
||||
"crypto-js": "4.1.1",
|
||||
"csv-parse": "5.0.4",
|
||||
"ecpair": "2.0.1",
|
||||
"goldengate": "11.2.1",
|
||||
"grammy": "1.7.3",
|
||||
"hot-formula-parser": "4.0.0",
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
"sanitize-filename": "1.6.3",
|
||||
"socks-proxy-agent": "6.2.0-beta.0",
|
||||
"table": "6.8.0",
|
||||
"tiny-secp256k1": "2.2.1",
|
||||
"update-notifier": "5.1.0",
|
||||
"window-size": "1.1.1"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue