Skip to content

Commit

Permalink
feat: add signature for structured data
Browse files Browse the repository at this point in the history
closes #2387
  • Loading branch information
beguene authored and kyranjamie committed May 16, 2022
1 parent ef217be commit 17591a2
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 42 deletions.
2 changes: 1 addition & 1 deletion src/app/common/actions/finalize-message-signature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExternalMethods, MESSAGE_SOURCE, SignatureResponseMessage } from '@shared/message-types';
import { logger } from '@shared/logger';
import { SignatureData } from '@shared/crypto/sign-message';
import { SignatureData } from '@stacks/connect';

export const finalizeMessageSignature = (
requestPayload: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ClarityType, ClarityValue, cvToString } from '@stacks/transactions';
import { principalToString } from '@stacks/transactions/dist/esm/clarity/types/principalCV';

export function ClarityValueListComponent(props: {
val: ClarityValue;
encoding?: 'tryAscii' | 'hex';
isRoot?: boolean;
}): JSX.Element {
const { val, encoding, isRoot = true } = props;

function wrapText(text: string): JSX.Element {
return <>{text}</>;
}
switch (val.type) {
case ClarityType.BoolTrue:
return wrapText('true');
case ClarityType.BoolFalse:
return wrapText('false');
case ClarityType.Int:
return wrapText(val.value.toString());
case ClarityType.UInt:
return wrapText(`u${val.value.toString()}`);
case ClarityType.Buffer:
if (encoding === 'tryAscii') {
const str = val.buffer.toString('ascii');
if (/[ -~]/.test(str)) {
return wrapText(JSON.stringify(str));
}
}
return wrapText(`0x${val.buffer.toString('hex')}`);
case ClarityType.OptionalNone:
return wrapText('none');
case ClarityType.OptionalSome:
return wrapText(`some ${cvToString(val.value, encoding)}`);
case ClarityType.ResponseErr:
return wrapText(`err ${cvToString(val.value, encoding)}`);
case ClarityType.ResponseOk:
return wrapText(`ok ${cvToString(val.value, encoding)}`);
case ClarityType.PrincipalStandard:
case ClarityType.PrincipalContract:
return wrapText(principalToString(val));
case ClarityType.List:
return wrapText(`[${val.list.map(v => cvToString(v, encoding)).join(', ')}]`);
case ClarityType.Tuple:
return (
<dl
style={{
display: 'flex',
flexFlow: 'row',
flexWrap: 'wrap',
paddingTop: isRoot ? '0' : '20px',
overflow: 'visible',
}}
>
{Object.keys(val.data).map(key => {
return (
<>
<dt style={{ flex: '0 0 20%', color: '#74777D' }}>{key}:</dt>
<dd style={{ flex: '0 0 80%' }}>
<ClarityValueListComponent
val={val.data[key]}
encoding={'tryAscii'}
isRoot={false}
/>
</dd>
</>
);
})}
</dl>
);
case ClarityType.StringASCII:
return wrapText(`"${val.data}"`);
case ClarityType.StringUTF8:
return wrapText(`u"${val.data}"`);
}
}
32 changes: 25 additions & 7 deletions src/app/pages/signature-request/components/message-box.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { ClarityValue, deserializeCV } from '@stacks/transactions';
import { color, Stack, Text } from '@stacks/ui';
import { useEffect, useState } from 'react';
import { sha256 } from 'sha.js';
import { ClarityValueListComponent } from './ClarityValueListComponent';
import { HashDrawer } from './hash-drawer';
import { useEffect, useState } from 'react';
import { isStructuredMessage, SignatureMessage } from './sign-action';

export function MessageBox(props: SignatureMessage): JSX.Element | null {
const { message, messageType } = props;

interface MessageBoxProps {
message: string;
}
export function MessageBox(props: MessageBoxProps): JSX.Element | null {
const { message } = props;
const [hash, setHash] = useState<string | undefined>();
const [displayMessage, setDisplayMessage] = useState<string | undefined>();
const [clarityValueMessage, setClarityValueMessage] = useState<ClarityValue | undefined>();

useEffect(() => {
if (isStructuredMessage(messageType)) {
// setDisplayMessage(clarityValueToDisplay(deserializeCV(Buffer.from(message, 'hex'))));
setClarityValueMessage(deserializeCV(Buffer.from(message, 'hex')));
} else {
setDisplayMessage(message);
}
}, [message, messageType]);

useEffect(() => {
if (!message) return;
setHash(new sha256().update(message).digest('hex'));
}, [message]);

Expand All @@ -34,7 +48,11 @@ export function MessageBox(props: MessageBoxProps): JSX.Element | null {
>
<Stack spacing="base-tight">
<Text display="block" fontSize={2} lineHeight="1.6" wordBreak="break-all">
{message}
{clarityValueMessage && messageType === 'structured' ? (
<ClarityValueListComponent val={clarityValueMessage} encoding={'tryAscii'} />
) : (
displayMessage
)}
</Text>
</Stack>
</Stack>
Expand Down
18 changes: 16 additions & 2 deletions src/app/pages/signature-request/components/sign-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,24 @@ function useSignMessageSoftwareWallet() {
);
}

interface SignActionProps {
export type SignatureMessageType = 'utf8' | 'structured';

export interface SignatureMessage {
message: string;
messageType: SignatureMessageType;
}

export function isStructuredMessage(
messageType: SignatureMessageType
): messageType is 'structured' {
return messageType === 'structured';
}
export function SignAction(props: SignActionProps): JSX.Element | null {

export function isSignatureMessageType(message: unknown): message is SignatureMessageType {
return typeof message === 'string' || typeof message === 'object';
}

export function SignAction(props: SignatureMessage): JSX.Element | null {
const { message } = props;
const signSoftwareWalletMessage = useSignMessageSoftwareWallet();
const { tabId, requestToken } = useSignatureRequestSearchParams();
Expand Down
23 changes: 15 additions & 8 deletions src/app/pages/signature-request/signature-request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
import { PageTop } from './components/page-top';
import { MessageBox } from './components/message-box';
import { NetworkRow } from './components/network-row';
import { SignAction } from './components/sign-action';
import {
isSignatureMessageType,
SignAction,
SignatureMessage,
SignatureMessageType,
} from './components/sign-action';
import { StacksNetwork, StacksTestnet } from '@stacks/network';
import { FiAlertTriangle } from 'react-icons/fi';
import { Caption } from '@app/components/typography';
Expand All @@ -22,15 +27,17 @@ import { openInNewTab } from '@app/common/utils/open-in-new-tab';

function SignatureRequestBase(): JSX.Element | null {
const validSignatureRequest = useIsSignatureRequestValid();
const { requestToken } = useSignatureRequestSearchParams();
const { requestToken, messageType } = useSignatureRequestSearchParams();

useRouteHeader(<PopupHeader />);

if (!requestToken) return null;
if (!requestToken || !messageType) return null;
const signatureRequest = getPayloadFromToken(requestToken);
if (!signatureRequest) return null;
if (isUndefined(validSignatureRequest)) return null;
const appName = signatureRequest?.appDetails?.name;
const { message, network } = signatureRequest;
if (!isSignatureMessageType(message)) return null;

return (
<Stack px={['loose', 'unset']} spacing="loose" width="100%">
Expand All @@ -42,6 +49,7 @@ function SignatureRequestBase(): JSX.Element | null {
message={message}
network={network || new StacksTestnet()}
appName={appName}
messageType={messageType as unknown as SignatureMessageType}
/>
)}
</Stack>
Expand Down Expand Up @@ -73,19 +81,18 @@ function Disclaimer(props: DisclaimerProps) {
);
}

interface SignatureRequestContentProps {
interface SignatureRequestContentProps extends SignatureMessage {
network: StacksNetwork;
message: string;
appName: string | undefined;
}

function SignatureRequestContent(props: SignatureRequestContentProps) {
const { message, network, appName } = props;
const { message, messageType, appName, network } = props;
return (
<>
<MessageBox message={message} />
<MessageBox message={message} messageType={messageType} />
<NetworkRow network={network} />
<SignAction message={message} />
<SignAction message={message} messageType={messageType} />
<hr />
<Disclaimer appName={appName} />
</>
Expand Down
1 change: 1 addition & 0 deletions src/app/store/signatures/requests.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function useSignatureRequestSearchParams() {
requestToken: searchParams.get('request'),
tabId: searchParams.get('tabId'),
origin: searchParams.get('origin'),
messageType: searchParams.get('messageType'),
}),
[searchParams]
);
Expand Down
19 changes: 19 additions & 0 deletions src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ chrome.runtime.onConnect.addListener(port =>
}
break;
}
case ExternalMethods.structuredDataSignatureRequest: {
const path = RouteUrls.SignatureRequest; // TODO refactor
const urlParams = new URLSearchParams();
if (!port.sender) return;
const { tab, url } = port.sender;
if (!tab?.id || !url) return;
const origin = new URL(url).origin;
urlParams.set('request', payload);
urlParams.set('tabId', tab.id.toString());
urlParams.set('origin', origin);
urlParams.set('messageType', 'structured');
if (IS_TEST_ENV) {
await openRequestInFullPage(path, urlParams);
} else {
popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` });
}
break;
}
case ExternalMethods.signatureRequest: {
const path = RouteUrls.SignatureRequest;
const urlParams = new URLSearchParams();
Expand All @@ -81,6 +99,7 @@ chrome.runtime.onConnect.addListener(port =>
urlParams.set('request', payload);
urlParams.set('tabId', tab.id.toString());
urlParams.set('origin', origin);
urlParams.set('messageType', 'utf8');
if (IS_TEST_ENV) {
await openRequestInFullPage(path, urlParams);
} else {
Expand Down
12 changes: 12 additions & 0 deletions src/content-scripts/content-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ document.addEventListener(DomEventName.signatureRequest, ((event: SignatureReque
});
}) as EventListener);

// Listen for a CustomEvent (structured data signature request) coming from the web app
document.addEventListener(DomEventName.structuredDataSignatureRequest, ((
event: SignatureRequestEvent
) => {
forwardDomEventToBackground({
path: RouteUrls.SignatureRequest,
payload: event.detail.signatureRequest,
urlParam: 'request',
method: ExternalMethods.structuredDataSignatureRequest,
});
}) as EventListener);

// Inject inpage script (Stacks Provider)
const inpage = document.createElement('script');
inpage.src = chrome.runtime.getURL('inpage.js');
Expand Down
24 changes: 24 additions & 0 deletions src/inpage/inpage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ const provider: Omit<StacksProvider, 'structuredDataSignatureRequest'> = {
const { url } = await callAndReceive('getURL');
return url;
},
structuredDataSignatureRequest: async signatureRequest => {
const event = new CustomEvent<SignatureRequestEventDetails>(
DomEventName.structuredDataSignatureRequest,
{
detail: { signatureRequest },
}
);
document.dispatchEvent(event);
return new Promise((resolve, reject) => {
const handleMessage = (event: MessageEvent<SignatureResponseMessage>) => {
if (!isValidEvent(event, ExternalMethods.signatureResponse)) return;
if (event.data.payload?.signatureRequest !== signatureRequest) return;
window.removeEventListener('message', handleMessage);
if (event.data.payload.signatureResponse === 'cancel') {
reject(event.data.payload.signatureResponse);
return;
}
if (typeof event.data.payload.signatureResponse !== 'string') {
resolve(event.data.payload.signatureResponse);
}
};
window.addEventListener('message', handleMessage);
});
},
signatureRequest: async signatureRequest => {
const event = new CustomEvent<SignatureRequestEventDetails>(DomEventName.signatureRequest, {
detail: { signatureRequest },
Expand Down
6 changes: 1 addition & 5 deletions src/shared/crypto/sign-message.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { SignatureData } from '@stacks/connect';
import { signECDSA, hashMessage } from '@stacks/encryption';
import { StacksPrivateKey } from '@stacks/transactions';

export interface SignatureData {
signature: string; // - Hex encoded DER signature
publicKey: string; // - Hex encoded private string taken from privateKey
}

export function signMessage(message: string, privateKey: StacksPrivateKey): SignatureData {
const privateKeyUncompressed = privateKey.data.slice(0, 32);
const hash = hashMessage(message);
Expand Down
1 change: 1 addition & 0 deletions src/shared/inpage-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export enum DomEventName {
authenticationRequest = 'stacksAuthenticationRequest',
signatureRequest = 'signatureRequest',
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
transactionRequest = 'stacksTransactionRequest',
}

Expand Down
13 changes: 10 additions & 3 deletions src/shared/message-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FinishedTxPayload, SponsoredFinishedTxPayload } from '@stacks/connect';
import { SignatureData } from './crypto/sign-message';
import { FinishedTxPayload, SignatureData, SponsoredFinishedTxPayload } from '@stacks/connect';

export const MESSAGE_SOURCE = 'stacks-wallet' as const;

Expand All @@ -12,6 +11,8 @@ export enum ExternalMethods {
authenticationResponse = 'authenticationResponse',
signatureRequest = 'signatureRequest',
signatureResponse = 'signatureResponse',
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
structuredDataSignatureResponse = 'structuredDataSignatureResponse',
}

export enum InternalMethods {
Expand Down Expand Up @@ -57,6 +58,11 @@ export type SignatureResponseMessage = Message<
}
>;

type StructuredDataSignatureRequestMessage = Message<
ExternalMethods.structuredDataSignatureRequest,
string
>;

type TransactionRequestMessage = Message<ExternalMethods.transactionRequest, string>;

export type TxResult = SponsoredFinishedTxPayload | FinishedTxPayload;
Expand All @@ -72,7 +78,8 @@ export type TransactionResponseMessage = Message<
export type MessageFromContentScript =
| AuthenticationRequestMessage
| TransactionRequestMessage
| SignatureRequestMessage;
| SignatureRequestMessage
| StructuredDataSignatureRequestMessage;
export type MessageToContentScript =
| AuthenticationResponseMessage
| TransactionResponseMessage
Expand Down

0 comments on commit 17591a2

Please sign in to comment.