chore: 🔧 add ln-auth (#148)

This commit is contained in:
Anthony Potdevin 2020-09-25 18:50:39 +02:00 committed by GitHub
parent 78efc34e68
commit 56b690753b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 228 additions and 30 deletions

27
package-lock.json generated
View File

@ -7309,6 +7309,15 @@
"@types/redis": "*" "@types/redis": "*"
} }
}, },
"@types/secp256k1": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.1.tgz",
"integrity": "sha512-+ZjSA8ELlOp8SlKi0YLB2tz9d5iPNEmOBd+8Rz21wTMdaXQIa9b6TEnD6l5qKOCypE7FSyPyck12qZJxSDNoog==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/serve-static": { "@types/serve-static": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz",
@ -9612,6 +9621,24 @@
} }
} }
}, },
"bip39": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz",
"integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==",
"requires": {
"@types/node": "11.11.6",
"create-hash": "^1.1.0",
"pbkdf2": "^3.0.9",
"randombytes": "^2.0.1"
},
"dependencies": {
"@types/node": {
"version": "11.11.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz",
"integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ=="
}
}
},
"bip65": { "bip65": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz", "resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz",

View File

@ -41,6 +41,8 @@
"balanceofsatoshis": "^5.47.0", "balanceofsatoshis": "^5.47.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bech32": "^1.1.4", "bech32": "^1.1.4",
"bip32": "^2.0.5",
"bip39": "^3.0.2",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
@ -72,6 +74,7 @@
"react-table": "^7.5.1", "react-table": "^7.5.1",
"react-toastify": "^6.0.8", "react-toastify": "^6.0.8",
"react-tooltip": "^4.2.10", "react-tooltip": "^4.2.10",
"secp256k1": "^4.0.2",
"styled-components": "^5.2.0", "styled-components": "^5.2.0",
"styled-react-modal": "^2.0.1", "styled-react-modal": "^2.0.1",
"styled-theming": "^2.2.0", "styled-theming": "^2.2.0",
@ -112,6 +115,7 @@
"@types/qrcode.react": "^1.0.1", "@types/qrcode.react": "^1.0.1",
"@types/react-copy-to-clipboard": "^4.3.0", "@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-table": "^7.0.23", "@types/react-table": "^7.0.23",
"@types/secp256k1": "^4.0.1",
"@types/styled-components": "^5.1.3", "@types/styled-components": "^5.1.3",
"@types/styled-react-modal": "^1.2.0", "@types/styled-react-modal": "^1.2.0",
"@types/styled-theming": "^2.2.5", "@types/styled-theming": "^2.2.5",

View File

@ -3,14 +3,27 @@ import { to } from 'server/helpers/async';
import { logger } from 'server/helpers/logger'; import { logger } from 'server/helpers/logger';
import { requestLimiter } from 'server/helpers/rateLimiter'; import { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes'; import { ContextType } from 'server/types/apiTypes';
import { createInvoice, decodePaymentRequest, pay } from 'ln-service'; import {
createInvoice,
decodePaymentRequest,
pay,
getWalletInfo,
diffieHellmanComputeSecret,
} from 'ln-service';
import { import {
CreateInvoiceType, CreateInvoiceType,
DecodedType, DecodedType,
DiffieHellmanComputeSecretType,
GetWalletInfoType,
PayInvoiceType, PayInvoiceType,
} from 'server/types/ln-service.types'; } from 'server/types/ln-service.types';
// import { GetPublicKeyType } from 'server/types/ln-service.types';
// import hmacSHA256 from 'crypto-js/hmac-sha256'; import hmacSHA256 from 'crypto-js/hmac-sha256';
import { enc } from 'crypto-js';
import * as bip39 from 'bip39';
import * as bip32 from 'bip32';
import * as secp256k1 from 'secp256k1';
import { BIP32Interface } from 'bip32';
type LnUrlPayResponseType = { type LnUrlPayResponseType = {
pr?: string; pr?: string;
@ -57,34 +70,99 @@ type WithdrawRequestType = {
type RequestType = PayRequestType | WithdrawRequestType; type RequestType = PayRequestType | WithdrawRequestType;
type RequestWithType = { isTypeOf: string } & RequestType; type RequestWithType = { isTypeOf: string } & RequestType;
const fromHexString = (hexString: string) =>
new Uint8Array(
hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
);
const toHexString = (bytes: Uint8Array) =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
export const lnUrlResolvers = { export const lnUrlResolvers = {
Mutation: { Mutation: {
lnUrl: async ( lnUrlAuth: async (
_: undefined, _: undefined,
{ type, url }: LnUrlParams, { url }: LnUrlParams,
context: ContextType context: ContextType
): Promise<string> => { ): Promise<{ status: string; message: string }> => {
await requestLimiter(context.ip, 'lnUrl'); await requestLimiter(context.ip, 'lnUrl');
const { lnd } = context;
// const fullUrl = new URL(url); const domainUrl = new URL(url);
const host = domainUrl.host;
// const { lnd } = context; const k1 = domainUrl.searchParams.get('k1');
// if (type === 'login') { if (!host || !k1) {
// logger.debug({ type, url }); logger.error('Missing host or k1 in url: %o', url);
throw new Error('WrongUrlFormat');
}
// const info = await to<GetPublicKeyType>( const wallet = await to<GetWalletInfoType>(getWalletInfo({ lnd }));
// getPublicKey({ lnd, family: 138, index: 0 })
// );
// const hashed = hmacSHA256(fullUrl.host, info.public_key); // Generate entropy
const secret = await to<DiffieHellmanComputeSecretType>(
diffieHellmanComputeSecret({
lnd,
key_family: 138,
key_index: 0,
partner_public_key: wallet?.public_key,
})
);
// return info.public_key; // Generate hash from host and entropy
// } const hashed = hmacSHA256(host, secret.secret).toString(enc.Hex);
logger.debug({ type, url }); const indexes =
hashed.match(/.{1,4}/g)?.map(index => parseInt(index, 16)) || [];
return 'confirmed'; // Generate private seed from entropy
const secretKey = bip39.entropyToMnemonic(hashed);
const base58 = bip39.mnemonicToSeedSync(secretKey);
// Derive private seed from previous private seed and path
const node: BIP32Interface = bip32.fromSeed(base58);
const derived = node.derivePath(
`m/138/${indexes[0]}/${indexes[1]}/${indexes[2]}/${indexes[3]}`
);
// Get private and public key from derived private seed
const privateKey = derived.privateKey?.toString('hex');
const linkingKey = derived.publicKey.toString('hex');
if (!privateKey || !linkingKey) {
logger.error('Error deriving private or public key: %o', url);
throw new Error('ErrorDerivingPrivateKey');
}
// Sign k1 with derived private seed
const sigObj = secp256k1.ecdsaSign(
fromHexString(k1),
fromHexString(privateKey)
);
// Get signature
const signature = secp256k1.signatureExport(sigObj.signature);
const encodedSignature = toHexString(signature);
// Build final url with signature and public key
const finalUrl = `${url}&sig=${encodedSignature}&key=${linkingKey}`;
try {
const response = await fetch(finalUrl);
const json = await response.json();
logger.debug('LnUrlAuth response: %o', json);
if (json.status === 'ERROR') {
return { ...json, message: json.reason || 'LnServiceError' };
}
return { ...json, message: json.event || 'LnServiceSuccess' };
} catch (error) {
logger.error('Error authenticating with LnUrl service: %o', error);
throw new Error('ProblemAuthenticatingWithLnUrlService');
}
}, },
fetchLnUrl: async ( fetchLnUrl: async (
_: undefined, _: undefined,

View File

@ -21,6 +21,11 @@ export const lnUrlTypes = gql`
union LnUrlRequest = WithdrawRequest | PayRequest union LnUrlRequest = WithdrawRequest | PayRequest
type AuthResponse {
status: String!
message: String!
}
type PaySuccess { type PaySuccess {
tag: String tag: String
description: String description: String

View File

@ -91,6 +91,7 @@ export const queryTypes = gql`
export const mutationTypes = gql` export const mutationTypes = gql`
type Mutation { type Mutation {
lnUrlAuth(url: String!): AuthResponse!
lnUrlPay(callback: String!, amount: Int!, comment: String): PaySuccess! lnUrlPay(callback: String!, amount: Int!, comment: String): PaySuccess!
lnUrlWithdraw( lnUrlWithdraw(
callback: String! callback: String!
@ -99,7 +100,6 @@ export const mutationTypes = gql`
description: String description: String
): String! ): String!
fetchLnUrl(url: String!): LnUrlRequest fetchLnUrl(url: String!): LnUrlRequest
lnUrl(type: String!, url: String!): String!
createBaseInvoice(amount: Int!): baseInvoiceType createBaseInvoice(amount: Int!): baseInvoiceType
createThunderPoints( createThunderPoints(
id: String! id: String!

View File

@ -103,6 +103,10 @@ export type GetWalletInfoType = {
public_key: string; public_key: string;
}; };
export type DiffieHellmanComputeSecretType = {
secret: string;
};
export type GetNodeType = { alias: string; color: string }; export type GetNodeType = { alias: string; color: string };
export type UtxoType = {}; export type UtxoType = {};

View File

@ -19,6 +19,19 @@ export type FetchLnUrlMutation = (
)> } )> }
); );
export type AuthLnUrlMutationVariables = Types.Exact<{
url: Types.Scalars['String'];
}>;
export type AuthLnUrlMutation = (
{ __typename?: 'Mutation' }
& { lnUrlAuth: (
{ __typename?: 'AuthResponse' }
& Pick<Types.AuthResponse, 'status' | 'message'>
) }
);
export type PayLnUrlMutationVariables = Types.Exact<{ export type PayLnUrlMutationVariables = Types.Exact<{
callback: Types.Scalars['String']; callback: Types.Scalars['String'];
amount: Types.Scalars['Int']; amount: Types.Scalars['Int'];
@ -95,6 +108,39 @@ export function useFetchLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<F
export type FetchLnUrlMutationHookResult = ReturnType<typeof useFetchLnUrlMutation>; export type FetchLnUrlMutationHookResult = ReturnType<typeof useFetchLnUrlMutation>;
export type FetchLnUrlMutationResult = Apollo.MutationResult<FetchLnUrlMutation>; export type FetchLnUrlMutationResult = Apollo.MutationResult<FetchLnUrlMutation>;
export type FetchLnUrlMutationOptions = Apollo.BaseMutationOptions<FetchLnUrlMutation, FetchLnUrlMutationVariables>; export type FetchLnUrlMutationOptions = Apollo.BaseMutationOptions<FetchLnUrlMutation, FetchLnUrlMutationVariables>;
export const AuthLnUrlDocument = gql`
mutation AuthLnUrl($url: String!) {
lnUrlAuth(url: $url) {
status
message
}
}
`;
export type AuthLnUrlMutationFn = Apollo.MutationFunction<AuthLnUrlMutation, AuthLnUrlMutationVariables>;
/**
* __useAuthLnUrlMutation__
*
* To run a mutation, you first call `useAuthLnUrlMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAuthLnUrlMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [authLnUrlMutation, { data, loading, error }] = useAuthLnUrlMutation({
* variables: {
* url: // value for 'url'
* },
* });
*/
export function useAuthLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<AuthLnUrlMutation, AuthLnUrlMutationVariables>) {
return Apollo.useMutation<AuthLnUrlMutation, AuthLnUrlMutationVariables>(AuthLnUrlDocument, baseOptions);
}
export type AuthLnUrlMutationHookResult = ReturnType<typeof useAuthLnUrlMutation>;
export type AuthLnUrlMutationResult = Apollo.MutationResult<AuthLnUrlMutation>;
export type AuthLnUrlMutationOptions = Apollo.BaseMutationOptions<AuthLnUrlMutation, AuthLnUrlMutationVariables>;
export const PayLnUrlDocument = gql` export const PayLnUrlDocument = gql`
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) { mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) { lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {

View File

@ -23,6 +23,15 @@ export const FETCH_LN_URL = gql`
} }
`; `;
export const AUTH_LN_URL = gql`
mutation AuthLnUrl($url: String!) {
lnUrlAuth(url: $url) {
status
message
}
}
`;
export const PAY_LN_URL = gql` export const PAY_LN_URL = gql`
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) { mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) { lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {

View File

@ -208,10 +208,10 @@ export type QueryGetSessionTokenArgs = {
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
lnUrlAuth: AuthResponse;
lnUrlPay: PaySuccess; lnUrlPay: PaySuccess;
lnUrlWithdraw: Scalars['String']; lnUrlWithdraw: Scalars['String'];
fetchLnUrl?: Maybe<LnUrlRequest>; fetchLnUrl?: Maybe<LnUrlRequest>;
lnUrl: Scalars['String'];
createBaseInvoice?: Maybe<BaseInvoiceType>; createBaseInvoice?: Maybe<BaseInvoiceType>;
createThunderPoints: Scalars['Boolean']; createThunderPoints: Scalars['Boolean'];
closeChannel?: Maybe<CloseChannelType>; closeChannel?: Maybe<CloseChannelType>;
@ -234,6 +234,11 @@ export type Mutation = {
}; };
export type MutationLnUrlAuthArgs = {
url: Scalars['String'];
};
export type MutationLnUrlPayArgs = { export type MutationLnUrlPayArgs = {
callback: Scalars['String']; callback: Scalars['String'];
amount: Scalars['Int']; amount: Scalars['Int'];
@ -254,12 +259,6 @@ export type MutationFetchLnUrlArgs = {
}; };
export type MutationLnUrlArgs = {
type: Scalars['String'];
url: Scalars['String'];
};
export type MutationCreateBaseInvoiceArgs = { export type MutationCreateBaseInvoiceArgs = {
amount: Scalars['Int']; amount: Scalars['Int'];
}; };
@ -1041,6 +1040,12 @@ export type PayRequest = {
export type LnUrlRequest = WithdrawRequest | PayRequest; export type LnUrlRequest = WithdrawRequest | PayRequest;
export type AuthResponse = {
__typename?: 'AuthResponse';
status: Scalars['String'];
message: Scalars['String'];
};
export type PaySuccess = { export type PaySuccess = {
__typename?: 'PaySuccess'; __typename?: 'PaySuccess';
tag?: Maybe<Scalars['String']>; tag?: Maybe<Scalars['String']>;

View File

@ -1,9 +1,11 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import { Card } from 'src/components/generic/Styled'; import { Card } from 'src/components/generic/Styled';
import { InputWithDeco } from 'src/components/input/InputWithDeco'; import { InputWithDeco } from 'src/components/input/InputWithDeco';
import Modal from 'src/components/modal/ReactModal'; import Modal from 'src/components/modal/ReactModal';
import { useAuthLnUrlMutation } from 'src/graphql/mutations/__generated__/lnUrl.generated';
import { getErrorContent } from 'src/utils/error';
import { decodeLnUrl } from 'src/utils/url'; import { decodeLnUrl } from 'src/utils/url';
import { LnUrlModal } from './lnUrlModal'; import { LnUrlModal } from './lnUrlModal';
@ -13,6 +15,24 @@ export const LnUrlCard = () => {
const [type, setType] = useState<string>(''); const [type, setType] = useState<string>('');
const [modalOpen, setModalOpen] = useState<boolean>(false); const [modalOpen, setModalOpen] = useState<boolean>(false);
const [auth, { data, loading }] = useAuthLnUrlMutation({
onError: error => toast.error(getErrorContent(error)),
});
useEffect(() => {
if (loading || !data?.lnUrlAuth) return;
const { status, message } = data.lnUrlAuth;
if (status === 'ERROR') {
toast.error(message);
} else {
toast.success(message);
setLnUrl('');
setUrl('');
setType('');
}
}, [data, loading]);
const handleDecode = () => { const handleDecode = () => {
if (!lnurl) { if (!lnurl) {
toast.warning('Please input a LNURL'); toast.warning('Please input a LNURL');
@ -31,7 +51,7 @@ export const LnUrlCard = () => {
setModalOpen(true); setModalOpen(true);
} }
if (tag === 'login') { if (tag === 'login') {
toast.warning('LnAuth is not available yet'); auth({ variables: { url: urlString } });
} }
} catch (error) { } catch (error) {
toast.error('Problem decoding LNURL'); toast.error('Problem decoding LNURL');
@ -43,7 +63,7 @@ export const LnUrlCard = () => {
<Card> <Card>
<InputWithDeco <InputWithDeco
value={lnurl} value={lnurl}
placeholder={'LnPay or LnWithdraw URL'} placeholder={'LnPay / LnWithdraw / LnAuth'}
title={'LNURL'} title={'LNURL'}
inputCallback={value => setLnUrl(value)} inputCallback={value => setLnUrl(value)}
onEnter={() => handleDecode()} onEnter={() => handleDecode()}
@ -51,7 +71,7 @@ export const LnUrlCard = () => {
<ColorButton <ColorButton
arrow={true} arrow={true}
fullWidth={true} fullWidth={true}
disabled={!lnurl} disabled={!lnurl || loading}
withMargin={'16px 0 0'} withMargin={'16px 0 0'}
onClick={() => handleDecode()} onClick={() => handleDecode()}
> >