-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
closes #1051
- Loading branch information
Showing
34 changed files
with
2,183 additions
and
1,359 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { ExternalMethods, MESSAGE_SOURCE, SignatureResponseMessage } from '@shared/message-types'; | ||
import { deleteTabForRequest, getTab, StorageKey } from '@shared/utils/storage'; | ||
import { logger } from '@shared/logger'; | ||
import { SignatureData } from '@shared/crypto/sign-message'; | ||
|
||
export const finalizeMessageSignature = (requestPayload: string, data: SignatureData | string) => { | ||
try { | ||
const tabId = getTab(StorageKey.signatureRequests, requestPayload); | ||
const responseMessage: SignatureResponseMessage = { | ||
source: MESSAGE_SOURCE, | ||
method: ExternalMethods.signatureResponse, | ||
payload: { | ||
signatureRequest: requestPayload, | ||
signatureResponse: data, | ||
}, | ||
}; | ||
chrome.tabs.sendMessage(tabId, responseMessage); | ||
deleteTabForRequest(StorageKey.signatureRequests, requestPayload); | ||
// If this is a string, then the transaction has been canceled | ||
// and the user has closed the window | ||
window.close(); | ||
} catch (error) { | ||
logger.debug('Failed to get Tab ID for message signature request:', requestPayload); | ||
throw new Error( | ||
'Your message was signed, but we lost communication with the app you started with.' | ||
); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { Account, getAppPrivateKey, Wallet } from '@stacks/wallet-sdk'; | ||
import { SignaturePayload } from '@stacks/connect'; | ||
import { decodeToken, TokenVerifier } from 'jsontokens'; | ||
import { getPublicKeyFromPrivate } from '@stacks/encryption'; | ||
import { getAddressFromPrivateKey, TransactionVersion } from '@stacks/transactions'; | ||
|
||
function getTransactionVersionFromRequest(signature: SignaturePayload) { | ||
const { network } = signature; | ||
if (!network) return TransactionVersion.Mainnet; | ||
if (![TransactionVersion.Mainnet, TransactionVersion.Testnet].includes(network.version)) { | ||
throw new Error('Invalid network version provided'); | ||
} | ||
return network.version; | ||
} | ||
|
||
const UNAUTHORIZED_SIGNATURE_REQUEST = | ||
'The signature request provided is not signed by this wallet.'; | ||
/** | ||
* Verify a transaction request. | ||
* A transaction request is a signed JWT that is created on an app, | ||
* via `@stacks/connect`. The private key used to sign this JWT is an | ||
* `appPrivateKey`, which an app can get from authentication. | ||
* | ||
* The payload in this JWT can include an `stxAddress`. This indicates the | ||
* 'default' STX address that should be used to sign this transaction. This allows | ||
* the wallet to use the same account to sign a transaction as it used to sign | ||
* in to the app. | ||
* | ||
* This JWT is invalidated if: | ||
* - The JWT is not signed properly | ||
* - The public key used to sign this tx request does not match an `appPrivateKey` | ||
* for any of the accounts in this wallet. | ||
* - The `stxAddress` provided in the payload does not match an STX address | ||
* for any of the accounts in this wallet. | ||
* | ||
* @returns The decoded and validated `SignaturePayload` | ||
* @throws if the transaction request is invalid | ||
*/ | ||
interface VerifySignatureRequestArgs { | ||
requestToken: string; | ||
accounts: Account[]; | ||
appDomain: string; | ||
} | ||
export async function verifySignatureRequest({ | ||
requestToken, | ||
accounts, | ||
appDomain, | ||
}: VerifySignatureRequestArgs): Promise<SignaturePayload> { | ||
const token = decodeToken(requestToken); | ||
const signature = token.payload as unknown as SignaturePayload; | ||
const { publicKey, stxAddress } = signature; | ||
const txVersion = getTransactionVersionFromRequest(signature); | ||
const verifier = new TokenVerifier('ES256k', publicKey); | ||
const isSigned = await verifier.verifyAsync(requestToken); | ||
if (!isSigned) { | ||
throw new Error('Signature request is not signed'); | ||
} | ||
const foundAccount = accounts.find(account => { | ||
const appPrivateKey = getAppPrivateKey({ | ||
account, | ||
appDomain, | ||
}); | ||
const appPublicKey = getPublicKeyFromPrivate(appPrivateKey); | ||
if (appPublicKey !== publicKey) return false; | ||
if (!stxAddress) return true; | ||
const accountStxAddress = getAddressFromPrivateKey(account.stxPrivateKey, txVersion); | ||
if (stxAddress !== accountStxAddress) return false; | ||
return true; | ||
}); | ||
if (!foundAccount) { | ||
throw new Error(UNAUTHORIZED_SIGNATURE_REQUEST); | ||
} | ||
return signature; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
src/app/pages/signature-request/components/hash-drawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Stack, Flex, Box, Text } from '@stacks/ui'; | ||
import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; | ||
import { useState } from 'react'; | ||
|
||
interface ShowHashButtonProps { | ||
expanded: boolean; | ||
} | ||
const ShowHashButton = (props: ShowHashButtonProps) => { | ||
const { expanded } = props; | ||
return <Box as={expanded ? FiChevronDown : FiChevronUp} mr="tight" size="20px" />; | ||
}; | ||
|
||
interface HashDrawerProps { | ||
hash: string; | ||
} | ||
|
||
export function HashDrawer(props: HashDrawerProps): JSX.Element | null { | ||
const { hash } = props; | ||
const [showHash, setShowHash] = useState(false); | ||
const [displayHash, setDisplayHash] = useState(hash); | ||
return ( | ||
<Stack py="tight" px="tight" spacing="loose"> | ||
<Flex marginBottom="0px !important"> | ||
<Text display="block" fontSize={'12px'}> | ||
{showHash ? 'Hide hash' : 'Show hash'} | ||
</Text> | ||
<Box | ||
_hover={{ cursor: 'pointer' }} | ||
style={{ marginLeft: 'auto' }} | ||
onClick={() => { | ||
setDisplayHash(showHash ? '' : hash); | ||
setShowHash(!showHash); | ||
}} | ||
> | ||
<ShowHashButton expanded={showHash} /> | ||
</Box> | ||
</Flex> | ||
<Box | ||
transition="all 0.65s cubic-bezier(0.23, 1, 0.32, 1)" | ||
py={showHash ? 'tight' : 'none'} | ||
height={showHash ? '100%' : '0'} | ||
visibility={showHash ? 'visible' : 'hidden'} | ||
> | ||
<Stack spacing="base-tight"> | ||
<Text | ||
display="block" | ||
color="#74777D" | ||
fontSize={2} | ||
fontWeight={500} | ||
lineHeight="1.6" | ||
wordBreak="break-all" | ||
fontFamily={'Fira Code'} | ||
> | ||
{displayHash} | ||
</Text> | ||
</Stack> | ||
</Box> | ||
</Stack> | ||
); | ||
} |
51 changes: 51 additions & 0 deletions
51
src/app/pages/signature-request/components/message-box.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { color, Stack, Text } from '@stacks/ui'; | ||
import { sha256 } from 'sha.js'; | ||
import { HashDrawer } from './hash-drawer'; | ||
import { useEffect, useState } from 'react'; | ||
|
||
interface MessageBoxProps { | ||
message: string; | ||
} | ||
export function MessageBox(props: MessageBoxProps): JSX.Element | null { | ||
const { message } = props; | ||
const [hash, setHash] = useState<string | undefined>(); | ||
useEffect(() => { | ||
setHash(new sha256().update(message).digest('hex')); | ||
}, [message]); | ||
|
||
if (!message) return null; | ||
|
||
return ( | ||
<> | ||
<Stack minHeight={'260px'}> | ||
<Stack | ||
border="4px solid" | ||
borderColor={color('border')} | ||
borderRadius="12px" | ||
backgroundColor={color('border')} | ||
> | ||
<Stack | ||
py="base-tight" | ||
px="base-loose" | ||
spacing="loose" | ||
borderRadius="12px" | ||
backgroundColor={'white'} | ||
> | ||
<Stack spacing="base-tight"> | ||
<Text | ||
display="block" | ||
fontSize={2} | ||
fontWeight={500} | ||
lineHeight="1.6" | ||
wordBreak="break-all" | ||
> | ||
{message} | ||
</Text> | ||
</Stack> | ||
</Stack> | ||
{hash ? <HashDrawer hash={hash} /> : null} | ||
</Stack> | ||
</Stack> | ||
</> | ||
); | ||
} |
28 changes: 28 additions & 0 deletions
28
src/app/pages/signature-request/components/network-row.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useCurrentNetwork } from '@app/common/hooks/use-current-network'; | ||
import { SpaceBetween } from '@app/components/space-between'; | ||
import { Caption } from '@app/components/typography'; | ||
import { StacksNetwork } from '@stacks/network'; | ||
import { ChainID } from '@stacks/transactions'; | ||
import { Stack } from '@stacks/ui'; | ||
import { useMemo } from 'react'; | ||
|
||
interface NetworkRowProps { | ||
network: StacksNetwork; | ||
} | ||
export function NetworkRow(props: NetworkRowProps): JSX.Element | null { | ||
const { network } = props; | ||
const isTestnetChain = useMemo(() => network.chainId === ChainID.Testnet, [network.chainId]); | ||
|
||
return ( | ||
<Stack spacing="base"> | ||
<SpaceBetween position="relative"> | ||
<Stack alignItems="center" isInline> | ||
<Caption>No fees will be incured</Caption> | ||
</Stack> | ||
<Caption> | ||
<span>{isTestnetChain ? 'Testnet' : 'Mainnet'}</span> | ||
</Caption> | ||
</SpaceBetween> | ||
</Stack> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { memo } from 'react'; | ||
import { Stack } from '@stacks/ui'; | ||
|
||
import { useCurrentNetwork } from '@app/common/hooks/use-current-network'; | ||
import { addPortSuffix, getUrlHostname } from '@app/common/utils'; | ||
import { Caption, Title } from '@app/components/typography'; | ||
import { useOrigin, useSignatureRequestState } from '@app/store/signatures/requests.hooks'; | ||
|
||
function PageTopBase(): JSX.Element | null { | ||
const signatureRequest = useSignatureRequestState(); | ||
const origin = useOrigin(); | ||
const network = useCurrentNetwork(); | ||
if (!signatureRequest) return null; | ||
|
||
const appName = signatureRequest?.appDetails?.name; | ||
const originAddition = origin ? ` (${getUrlHostname(origin)})` : ''; | ||
const testnetAddition = network.isTestnet | ||
? ` using ${getUrlHostname(network.url)}${addPortSuffix(network.url)}` | ||
: ''; | ||
const caption = appName ? `Requested by "${appName}"${originAddition}${testnetAddition}` : null; | ||
|
||
return ( | ||
<Stack pt="extra-loose" spacing="base"> | ||
<Title fontWeight="bold" as="h1"> | ||
Sign Message | ||
</Title> | ||
{caption && <Caption wordBreak="break-word">{caption}</Caption>} | ||
</Stack> | ||
); | ||
} | ||
|
||
export const PageTop = memo(PageTopBase); |
Oops, something went wrong.