From 56b690753b9f83fc03bbcbd700be71f05c1c8ff3 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com> Date: Fri, 25 Sep 2020 18:50:39 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20=F0=9F=94=A7=20add=20ln-auth=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 27 +++++ package.json | 4 + server/schema/lnurl/resolvers.ts | 114 +++++++++++++++--- server/schema/lnurl/types.ts | 5 + server/schema/types.ts | 2 +- server/types/ln-service.types.ts | 4 + .../__generated__/lnUrl.generated.tsx | 46 +++++++ src/graphql/mutations/lnUrl.ts | 9 ++ src/graphql/types.ts | 19 +-- src/views/home/quickActions/lnurl/index.tsx | 28 ++++- 10 files changed, 228 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16d84c90..8893fa7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b512b8d7..ff3e4bf5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/schema/lnurl/resolvers.ts b/server/schema/lnurl/resolvers.ts index c35058a9..aa047619 100644 --- a/server/schema/lnurl/resolvers.ts +++ b/server/schema/lnurl/resolvers.ts @@ -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 => { + ): 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( - // getPublicKey({ lnd, family: 138, index: 0 }) - // ); + const wallet = await to(getWalletInfo({ lnd })); - // const hashed = hmacSHA256(fullUrl.host, info.public_key); + // Generate entropy + const secret = await to( + diffieHellmanComputeSecret({ + lnd, + key_family: 138, + key_index: 0, + partner_public_key: wallet?.public_key, + }) + ); + + // Generate hash from host and entropy + const hashed = hmacSHA256(host, secret.secret).toString(enc.Hex); - // return info.public_key; - // } + const indexes = + hashed.match(/.{1,4}/g)?.map(index => parseInt(index, 16)) || []; - logger.debug({ type, url }); + // 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'); + } - return 'confirmed'; + // 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, diff --git a/server/schema/lnurl/types.ts b/server/schema/lnurl/types.ts index b28d4d73..44c48546 100644 --- a/server/schema/lnurl/types.ts +++ b/server/schema/lnurl/types.ts @@ -21,6 +21,11 @@ export const lnUrlTypes = gql` union LnUrlRequest = WithdrawRequest | PayRequest + type AuthResponse { + status: String! + message: String! + } + type PaySuccess { tag: String description: String diff --git a/server/schema/types.ts b/server/schema/types.ts index 2df81f24..9478a1c5 100644 --- a/server/schema/types.ts +++ b/server/schema/types.ts @@ -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! diff --git a/server/types/ln-service.types.ts b/server/types/ln-service.types.ts index 1c4e12db..b9621ded 100644 --- a/server/types/ln-service.types.ts +++ b/server/types/ln-service.types.ts @@ -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 = {}; diff --git a/src/graphql/mutations/__generated__/lnUrl.generated.tsx b/src/graphql/mutations/__generated__/lnUrl.generated.tsx index 7b1fe16c..e74ea70f 100644 --- a/src/graphql/mutations/__generated__/lnUrl.generated.tsx +++ b/src/graphql/mutations/__generated__/lnUrl.generated.tsx @@ -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 + ) } +); + export type PayLnUrlMutationVariables = Types.Exact<{ callback: Types.Scalars['String']; amount: Types.Scalars['Int']; @@ -95,6 +108,39 @@ export function useFetchLnUrlMutation(baseOptions?: Apollo.MutationHookOptions; export type FetchLnUrlMutationResult = Apollo.MutationResult; export type FetchLnUrlMutationOptions = Apollo.BaseMutationOptions; +export const AuthLnUrlDocument = gql` + mutation AuthLnUrl($url: String!) { + lnUrlAuth(url: $url) { + status + message + } +} + `; +export type AuthLnUrlMutationFn = Apollo.MutationFunction; + +/** + * __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) { + return Apollo.useMutation(AuthLnUrlDocument, baseOptions); + } +export type AuthLnUrlMutationHookResult = ReturnType; +export type AuthLnUrlMutationResult = Apollo.MutationResult; +export type AuthLnUrlMutationOptions = Apollo.BaseMutationOptions; export const PayLnUrlDocument = gql` mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) { lnUrlPay(callback: $callback, amount: $amount, comment: $comment) { diff --git a/src/graphql/mutations/lnUrl.ts b/src/graphql/mutations/lnUrl.ts index 98fce3e9..27a13adf 100644 --- a/src/graphql/mutations/lnUrl.ts +++ b/src/graphql/mutations/lnUrl.ts @@ -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) { diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 3a11737f..32e0e040 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -208,10 +208,10 @@ export type QueryGetSessionTokenArgs = { export type Mutation = { __typename?: 'Mutation'; + lnUrlAuth: AuthResponse; lnUrlPay: PaySuccess; lnUrlWithdraw: Scalars['String']; fetchLnUrl?: Maybe; - lnUrl: Scalars['String']; createBaseInvoice?: Maybe; createThunderPoints: Scalars['Boolean']; closeChannel?: Maybe; @@ -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; diff --git a/src/views/home/quickActions/lnurl/index.tsx b/src/views/home/quickActions/lnurl/index.tsx index 42a48c8b..d23ccfbd 100644 --- a/src/views/home/quickActions/lnurl/index.tsx +++ b/src/views/home/quickActions/lnurl/index.tsx @@ -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(''); const [modalOpen, setModalOpen] = useState(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 = () => { setLnUrl(value)} onEnter={() => handleDecode()} @@ -51,7 +71,7 @@ export const LnUrlCard = () => { handleDecode()} >