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/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": {
"version": "1.13.4",
"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": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz",

View File

@ -41,6 +41,8 @@
"balanceofsatoshis": "^5.47.0",
"bcryptjs": "^2.4.3",
"bech32": "^1.1.4",
"bip32": "^2.0.5",
"bip39": "^3.0.2",
"cookie": "^0.4.1",
"crypto-js": "^4.0.0",
"date-fns": "^2.16.1",
@ -72,6 +74,7 @@
"react-table": "^7.5.1",
"react-toastify": "^6.0.8",
"react-tooltip": "^4.2.10",
"secp256k1": "^4.0.2",
"styled-components": "^5.2.0",
"styled-react-modal": "^2.0.1",
"styled-theming": "^2.2.0",
@ -112,6 +115,7 @@
"@types/qrcode.react": "^1.0.1",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-table": "^7.0.23",
"@types/secp256k1": "^4.0.1",
"@types/styled-components": "^5.1.3",
"@types/styled-react-modal": "^1.2.0",
"@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 { requestLimiter } from 'server/helpers/rateLimiter';
import { ContextType } from 'server/types/apiTypes';
import { createInvoice, decodePaymentRequest, pay } from 'ln-service';
import {
createInvoice,
decodePaymentRequest,
pay,
getWalletInfo,
diffieHellmanComputeSecret,
} from 'ln-service';
import {
CreateInvoiceType,
DecodedType,
DiffieHellmanComputeSecretType,
GetWalletInfoType,
PayInvoiceType,
} 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 = {
pr?: string;
@ -57,34 +70,99 @@ type WithdrawRequestType = {
type RequestType = PayRequestType | WithdrawRequestType;
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 = {
Mutation: {
lnUrl: async (
lnUrlAuth: async (
_: undefined,
{ type, url }: LnUrlParams,
{ url }: LnUrlParams,
context: ContextType
): Promise<string> => {
): Promise<{ status: string; message: string }> => {
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') {
// logger.debug({ type, url });
if (!host || !k1) {
logger.error('Missing host or k1 in url: %o', url);
throw new Error('WrongUrlFormat');
}
// const info = await to<GetPublicKeyType>(
// getPublicKey({ lnd, family: 138, index: 0 })
// );
const wallet = await to<GetWalletInfoType>(getWalletInfo({ lnd }));
// 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 (
_: undefined,

View File

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

View File

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

View File

@ -103,6 +103,10 @@ export type GetWalletInfoType = {
public_key: string;
};
export type DiffieHellmanComputeSecretType = {
secret: string;
};
export type GetNodeType = { alias: string; color: string };
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<{
callback: Types.Scalars['String'];
amount: Types.Scalars['Int'];
@ -95,6 +108,39 @@ export function useFetchLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<F
export type FetchLnUrlMutationHookResult = ReturnType<typeof useFetchLnUrlMutation>;
export type FetchLnUrlMutationResult = Apollo.MutationResult<FetchLnUrlMutation>;
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`
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
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`
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {

View File

@ -208,10 +208,10 @@ export type QueryGetSessionTokenArgs = {
export type Mutation = {
__typename?: 'Mutation';
lnUrlAuth: AuthResponse;
lnUrlPay: PaySuccess;
lnUrlWithdraw: Scalars['String'];
fetchLnUrl?: Maybe<LnUrlRequest>;
lnUrl: Scalars['String'];
createBaseInvoice?: Maybe<BaseInvoiceType>;
createThunderPoints: Scalars['Boolean'];
closeChannel?: Maybe<CloseChannelType>;
@ -234,6 +234,11 @@ export type Mutation = {
};
export type MutationLnUrlAuthArgs = {
url: Scalars['String'];
};
export type MutationLnUrlPayArgs = {
callback: Scalars['String'];
amount: Scalars['Int'];
@ -254,12 +259,6 @@ export type MutationFetchLnUrlArgs = {
};
export type MutationLnUrlArgs = {
type: Scalars['String'];
url: Scalars['String'];
};
export type MutationCreateBaseInvoiceArgs = {
amount: Scalars['Int'];
};
@ -1041,6 +1040,12 @@ export type PayRequest = {
export type LnUrlRequest = WithdrawRequest | PayRequest;
export type AuthResponse = {
__typename?: 'AuthResponse';
status: Scalars['String'];
message: Scalars['String'];
};
export type PaySuccess = {
__typename?: 'PaySuccess';
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 { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import { Card } from 'src/components/generic/Styled';
import { InputWithDeco } from 'src/components/input/InputWithDeco';
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 { LnUrlModal } from './lnUrlModal';
@ -13,6 +15,24 @@ export const LnUrlCard = () => {
const [type, setType] = useState<string>('');
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 = () => {
if (!lnurl) {
toast.warning('Please input a LNURL');
@ -31,7 +51,7 @@ export const LnUrlCard = () => {
setModalOpen(true);
}
if (tag === 'login') {
toast.warning('LnAuth is not available yet');
auth({ variables: { url: urlString } });
}
} catch (error) {
toast.error('Problem decoding LNURL');
@ -43,7 +63,7 @@ export const LnUrlCard = () => {
<Card>
<InputWithDeco
value={lnurl}
placeholder={'LnPay or LnWithdraw URL'}
placeholder={'LnPay / LnWithdraw / LnAuth'}
title={'LNURL'}
inputCallback={value => setLnUrl(value)}
onEnter={() => handleDecode()}
@ -51,7 +71,7 @@ export const LnUrlCard = () => {
<ColorButton
arrow={true}
fullWidth={true}
disabled={!lnurl}
disabled={!lnurl || loading}
withMargin={'16px 0 0'}
onClick={() => handleDecode()}
>