merge changes

This commit is contained in:
Alex Bosworth 2022-04-17 14:39:30 -07:00
parent 8cf056b1d0
commit 8fb2aa8314
No known key found for this signature in database
GPG key ID: E80D2F3F311FD87E
5 changed files with 174 additions and 65 deletions

View file

@ -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));

View 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};
};

View 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
View file

@ -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"
},

View file

@ -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"
},