Add Sign-In with Ethereum (#14438)

Co-authored-by: Gregório Granado Magalhães <greg.magalhaes@gmail.com>
Co-authored-by: George Marshall <georgewrmarshall@gmail.com>
Co-authored-by: georgewrmarshall <george.marshall@consensys.net>
Co-authored-by: Ariella Vu <20778143+digiwand@users.noreply.github.com>
Co-authored-by: brad-decker <bhdecker84@gmail.com>
feature/default_network_editable
Sam Gbafa 2 years ago committed by GitHub
parent 2e6aea412a
commit 5802805597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .metamaskrc.dist
  2. 57
      app/_locales/en/messages.json
  3. 6
      app/scripts/lib/personal-message-manager.js
  4. 2
      development/build/scripts.js
  5. 18
      lavamoat/browserify/beta/policy.json
  6. 18
      lavamoat/browserify/flask/policy.json
  7. 18
      lavamoat/browserify/main/policy.json
  8. 1433
      lavamoat/build-system/policy.json
  9. 1
      package.json
  10. 150
      shared/modules/siwe.js
  11. 1
      ui/components/app/app-components.scss
  12. 8
      ui/components/app/permissions-connect-header/permissions-connect-header.component.js
  13. 15
      ui/components/app/signature-request-siwe/README.mdx
  14. 1
      ui/components/app/signature-request-siwe/index.js
  15. 74
      ui/components/app/signature-request-siwe/index.scss
  16. 1
      ui/components/app/signature-request-siwe/signature-request-siwe-header/index.js
  17. 26
      ui/components/app/signature-request-siwe/signature-request-siwe-header/index.scss
  18. 69
      ui/components/app/signature-request-siwe/signature-request-siwe-header/signature-request-siwe-header.js
  19. 36
      ui/components/app/signature-request-siwe/signature-request-siwe-header/signature-request-siwe-header.stories.js
  20. 1
      ui/components/app/signature-request-siwe/signature-request-siwe-message/index.js
  21. 13
      ui/components/app/signature-request-siwe/signature-request-siwe-message/index.scss
  22. 53
      ui/components/app/signature-request-siwe/signature-request-siwe-message/signature-request-siwe-message.js
  23. 51
      ui/components/app/signature-request-siwe/signature-request-siwe-message/signature-request-siwe-message.stories.js
  24. 174
      ui/components/app/signature-request-siwe/signature-request-siwe.js
  25. 165
      ui/components/app/signature-request-siwe/signature-request-siwe.stories.js
  26. 6
      ui/components/ui/site-origin/site-origin.js
  27. 10
      ui/components/ui/site-origin/site-origin.stories.js
  28. 21
      ui/pages/confirm-transaction/conf-tx.js
  29. 12
      yarn.lock

@ -7,5 +7,8 @@ SWAPS_USE_DEV_APIS=
COLLECTIBLES_V1=
TOKEN_DETECTION_V2=
; Set this to '1' to enable support for Sign-In with Ethereum [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361)
SIWE_V1=
; Set this to test changes to the phishing warning page.
PHISHING_WARNING_PAGE_URL=

@ -44,6 +44,60 @@
"QRHardwareWalletSteps2Description": {
"message": "Ngrave (Coming Soon)"
},
"SIWEAddressInvalid": {
"message": "The address in the sign-in request does not match the address of the account you are using to sign in."
},
"SIWEDomainInvalid": {
"message": "The website you are attempting to sign in to ($1) does not match the domain in the sign-in request. Proceed with caution.",
"description": "$1 represents the website domain"
},
"SIWEDomainWarningBody": {
"message": "The website ($1) is asking you to sign in to the wrong domain. This may be a phishing attack.",
"description": "$1 represents the website domain"
},
"SIWELabelChainID": {
"message": "Chain ID:"
},
"SIWELabelExpirationTime": {
"message": "Expires At:"
},
"SIWELabelIssuedAt": {
"message": "Issued At:"
},
"SIWELabelMessage": {
"message": "Message:"
},
"SIWELabelNonce": {
"message": "Nonce:"
},
"SIWELabelNotBefore": {
"message": "Not Before:"
},
"SIWELabelRequestID": {
"message": "Request ID:"
},
"SIWELabelResources": {
"message": "Resources: $1",
"description": "$1 represents the number of resources"
},
"SIWELabelURI": {
"message": "URI:"
},
"SIWELabelVersion": {
"message": "Version:"
},
"SIWESiteRequestSubtitle": {
"message": "This site is requesting to sign in with"
},
"SIWESiteRequestTitle": {
"message": "Sign-in request"
},
"SIWEWarningSubtitle": {
"message": "To confirm you understand, check:"
},
"SIWEWarningTitle": {
"message": "Are you sure?"
},
"about": {
"message": "About"
},
@ -3012,6 +3066,9 @@
"signed": {
"message": "Signed"
},
"signin": {
"message": "Sign-In"
},
"simulationErrorMessageV2": {
"message": "We were not able to estimate gas. There might be an error in the contract and this transaction may fail."
},

@ -7,6 +7,7 @@ import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from '../../../shared/modules/random-id';
import { EVENT } from '../../../shared/constants/metametrics';
import { detectSIWE } from '../../../shared/modules/siwe';
import { addHexPrefix } from './util';
const hexRe = /^[0-9A-Fa-f]+$/gu;
@ -135,6 +136,11 @@ export default class PersonalMessageManager extends EventEmitter {
msgParams.origin = req.origin;
}
msgParams.data = this.normalizeMsgData(msgParams.data);
// check for SIWE message
const siwe = detectSIWE(msgParams);
msgParams.siwe = siwe;
// create txData obj with parameters and meta data
const time = new Date().getTime();
const msgId = createId();

@ -44,6 +44,7 @@ const metamaskrc = require('rc')('metamask', {
SENTRY_DSN_DEV:
process.env.SENTRY_DSN_DEV ||
'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496',
SIWE_V1: process.env.SIWE_V1,
});
const { streamFlatMap } = require('../stream-flat-map');
@ -1131,6 +1132,7 @@ function getEnvironmentVariables({ buildType, devMode, testing, version }) {
INFURA_PROJECT_ID: getInfuraProjectId({ buildType, environment, testing }),
SEGMENT_HOST: metamaskrc.SEGMENT_HOST,
SEGMENT_WRITE_KEY: getSegmentWriteKey({ buildType, environment }),
SIWE_V1: metamaskrc.SIWE_V1 === '1',
SWAPS_USE_DEV_APIS: process.env.SWAPS_USE_DEV_APIS === '1',
ONBOARDING_V2: metamaskrc.ONBOARDING_V2 === '1',
COLLECTIBLES_V1: metamaskrc.COLLECTIBLES_V1 === '1',

@ -3735,6 +3735,24 @@
"localforage": true
}
},
"@spruceid/siwe-parser": {
"globals": {
"console.error": true,
"console.log": true
},
"packages": {
"@spruceid/siwe-parser>apg-js": true
}
},
"@spruceid/siwe-parser>apg-js": {
"globals": {
"mode": true
},
"packages": {
"browserify>buffer": true,
"browserify>insert-module-globals>is-buffer": true
}
},
"@storybook/api>regenerator-runtime": {
"globals": {
"regeneratorRuntime": "write"

@ -3735,6 +3735,24 @@
"localforage": true
}
},
"@spruceid/siwe-parser": {
"globals": {
"console.error": true,
"console.log": true
},
"packages": {
"@spruceid/siwe-parser>apg-js": true
}
},
"@spruceid/siwe-parser>apg-js": {
"globals": {
"mode": true
},
"packages": {
"browserify>buffer": true,
"browserify>insert-module-globals>is-buffer": true
}
},
"@storybook/api>regenerator-runtime": {
"globals": {
"regeneratorRuntime": "write"

@ -3735,6 +3735,24 @@
"localforage": true
}
},
"@spruceid/siwe-parser": {
"globals": {
"console.error": true,
"console.log": true
},
"packages": {
"@spruceid/siwe-parser>apg-js": true
}
},
"@spruceid/siwe-parser>apg-js": {
"globals": {
"mode": true
},
"packages": {
"browserify>buffer": true,
"browserify>insert-module-globals>is-buffer": true
}
},
"@storybook/api>regenerator-runtime": {
"globals": {
"regeneratorRuntime": "write"

File diff suppressed because it is too large Load Diff

@ -138,6 +138,7 @@
"@reduxjs/toolkit": "^1.6.2",
"@sentry/browser": "^6.0.0",
"@sentry/integrations": "^6.0.0",
"@spruceid/siwe-parser": "^1.1.3",
"@truffle/codec": "^0.11.18",
"@truffle/decoder": "^5.1.0",
"@zxing/browser": "^0.0.10",

@ -0,0 +1,150 @@
import { stripHexPrefix } from 'ethereumjs-util';
import { ParsedMessage } from '@spruceid/siwe-parser';
import log from 'loglevel';
const msgHexToText = (hex) => {
try {
const stripped = stripHexPrefix(hex);
const buff = Buffer.from(stripped, 'hex');
return buff.length === 32 ? hex : buff.toString('utf8');
} catch (e) {
log.error(e);
return hex;
}
};
/**
* A locally defined object used to provide data to identify a Sign-In With Ethereum (SIWE)(EIP-4361) message and provide the parsed message
*
* @typedef localSIWEObject
* @param {boolean} isSIWEMessage - Does the intercepted message conform to the SIWE specification?
* @param {ParsedMessage} parsedMessage - The data parsed out of the message
*/
/**
* This function intercepts a sign message, detects if it's a
* Sign-In With Ethereum (SIWE)(EIP-4361) message, and returns an object with
* relevant SIWE data.
*
* {@see {@link https://eips.ethereum.org/EIPS/eip-4361}}
*
* @param {object} msgParams - The params of the message to sign
* @returns {localSIWEObject}
*/
export const detectSIWE = (msgParams) => {
try {
const { data } = msgParams;
const message = msgHexToText(data);
const parsedMessage = new ParsedMessage(message);
return {
isSIWEMessage: true,
parsedMessage,
};
} catch (error) {
// ignore error, it's not a valid SIWE message
return {
isSIWEMessage: false,
parsedMessage: null,
};
}
};
/**
* Takes in a parsed Sign-In with Ethereum Message (EIP-4361)
* and generates an array of label-value pairs
*
* @param {object} parsedMessage - A parsed SIWE message with message contents
* @param {Function} t - i18n function
* @returns {Array} An array of label-value pairs with the type of the value as the label
*/
export const formatMessageParams = (parsedMessage, t) => {
const output = [];
const {
statement,
uri,
version,
chainId,
nonce,
issuedAt,
expirationTime,
notBefore,
requestId,
resources,
} = parsedMessage;
if (statement) {
output.push({
label: t('SIWELabelMessage'),
value: statement,
});
}
if (uri) {
output.push({
label: t('SIWELabelURI'),
value: uri,
});
}
if (version) {
output.push({
label: t('SIWELabelVersion'),
value: version,
});
}
if (chainId) {
output.push({
label: t('SIWELabelChainID'),
value: chainId,
});
}
if (nonce) {
output.push({
label: t('SIWELabelNonce'),
value: nonce,
});
}
if (issuedAt) {
output.push({
label: t('SIWELabelIssuedAt'),
value: issuedAt,
});
}
if (expirationTime) {
output.push({
label: t('SIWELabelExpirationTime'),
value: expirationTime,
});
}
if (notBefore) {
output.push({
label: t('SIWELabelNotBefore'),
value: notBefore,
});
}
if (requestId) {
output.push({
label: t('SIWELabelRequestID'),
value: requestId,
});
}
if (resources && resources.length > 0) {
output.push({
label: t('SIWELabelResources', [resources.length]),
value: resources
.reduce((previous, resource) => `${previous}${resource}\n`, '')
.trim(),
});
}
return output;
};

@ -58,6 +58,7 @@
@import 'step-progress-bar/index.scss';
@import 'selected-account/index';
@import 'signature-request/index';
@import 'signature-request-siwe/index';
@import 'signature-request-original/index';
@import 'srp-input/srp-input';
@import 'tab-bar/index';

@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classnames from 'classnames';
import SiteOrigin from '../../ui/site-origin';
import Box from '../../ui/box';
import {
@ -18,12 +19,14 @@ export default class PermissionsConnectHeader extends Component {
///: END:ONLY_INCLUDE_IN
static propTypes = {
className: PropTypes.string,
iconUrl: PropTypes.string,
iconName: PropTypes.string.isRequired,
siteOrigin: PropTypes.string.isRequired,
headerTitle: PropTypes.node,
boxProps: PropTypes.shape({ ...Box.propTypes }),
headerText: PropTypes.string,
rightIcon: PropTypes.node,
///: BEGIN:ONLY_INCLUDE_IN(flask)
snapVersion: PropTypes.string,
isSnapInstall: PropTypes.bool,
@ -42,6 +45,7 @@ export default class PermissionsConnectHeader extends Component {
iconUrl,
iconName,
siteOrigin,
rightIcon,
///: BEGIN:ONLY_INCLUDE_IN(flask)
isSnapInstall,
///: END:ONLY_INCLUDE_IN
@ -60,6 +64,7 @@ export default class PermissionsConnectHeader extends Component {
siteOrigin={siteOrigin}
iconSrc={iconUrl}
name={iconName}
rightIcon={rightIcon}
/>
</div>
);
@ -68,6 +73,7 @@ export default class PermissionsConnectHeader extends Component {
render() {
const {
boxProps,
className,
headerTitle,
headerText,
///: BEGIN:ONLY_INCLUDE_IN(flask)
@ -78,7 +84,7 @@ export default class PermissionsConnectHeader extends Component {
} = this.props;
return (
<Box
className="permissions-connect-header"
className={classnames('permissions-connect-header', className)}
flexDirection={FLEX_DIRECTION.COLUMN}
justifyContent={JUSTIFY_CONTENT.CENTER}
{...boxProps}

@ -0,0 +1,15 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import SignatureRequestSIWE from '.';
# Signature Request SIWE
dApp requesting the user to Sign in with Ethereum.
<Canvas>
<Story id="ui-components-app-signature-request-siwe-signature-request-siwe-stories-js--default-story" />
</Canvas>
## Component API
<ArgsTable of={SignatureRequestSIWE} />

@ -0,0 +1 @@
export { default } from './signature-request-siwe';

@ -0,0 +1,74 @@
@import 'signature-request-siwe-header/index';
@import 'signature-request-siwe-message/index';
.signature-request-siwe {
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
background-color: var(--color-background-default);
margin-right: auto;
margin-left: auto;
overflow-y: auto;
@media screen and (min-width: $break-large) {
width: 408px;
max-height: 82vh;
min-height: 570px;
flex: 0 0 auto;
border-radius: 8px;
box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08);
}
.signature-request-siwe__actionable-message {
margin: 4px 16px;
svg {
width: 13px;
height: 13px;
left: 10px;
top: 20px;
}
}
}
.signature-request-siwe__page-container-footer.page-container__footer {
border-top: none;
}
.signature-request-siwe__warning-popover {
.page-container__footer {
border-top: none;
padding: 0;
display: block;
}
.popover-footer {
padding: inherit;
display: block;
}
&__checkbox-wrapper {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 8px 16px 24px;
&__label {
@include H7;
color: var(--color-text-default);
margin-inline-start: 8px;
margin-top: 1px;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
cursor: pointer;
}
.check-box {
color: var(--color-error-default);
}
}
}

@ -0,0 +1 @@
export { default } from './signature-request-siwe-header';

@ -0,0 +1,26 @@
.signature-request-siwe-header {
display: flex;
padding: 16px;
justify-content: center;
align-items: center;
flex-direction: column;
&__tooltip__container {
display: flex !important;
}
.account-list-item {
margin: 8px 0 0;
&__top-row {
align-items: center;
margin: 0;
}
&__account-name {
@include H5;
font-weight: 500;
}
}
}

@ -0,0 +1,69 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import AccountListItem from '../../account-list-item';
import { I18nContext } from '../../../../contexts/i18n';
import Tooltip from '../../../ui/tooltip';
import InfoIcon from '../../../ui/icon/info-icon.component';
import { SEVERITIES } from '../../../../helpers/constants/design-system';
import PermissionsConnectHeader from '../../permissions-connect-header';
export default function SignatureRequestSIWEHeader({
fromAccount,
domain,
isSIWEDomainValid,
subjectMetadata,
}) {
const t = useContext(I18nContext);
return (
<div className="signature-request-siwe-header">
<PermissionsConnectHeader
iconUrl={subjectMetadata.iconUrl}
iconName={subjectMetadata.name}
headerTitle={t('SIWESiteRequestTitle')}
headerText={t('SIWESiteRequestSubtitle')}
siteOrigin={domain}
className={isSIWEDomainValid ? '' : 'bad-domain'}
rightIcon={
!isSIWEDomainValid && (
<Tooltip
position="bottom"
html={<p>{t('SIWEDomainWarningBody', [domain])}</p>}
wrapperClassName="signature-request-siwe-header__tooltip"
containerClassName="signature-request-siwe-header__tooltip__container"
>
<InfoIcon severity={SEVERITIES.DANGER} />
</Tooltip>
)
}
/>
{fromAccount && (
<AccountListItem
account={fromAccount}
className="signature-request-siwe-header__account-list-item"
/>
)}
</div>
);
}
SignatureRequestSIWEHeader.propTypes = {
/**
* The account that is requesting permissions
*/
fromAccount: PropTypes.object,
/**
* The domain that the request is for
*/
domain: PropTypes.string,
/**
* Whether the domain is valid
*/
isSIWEDomainValid: PropTypes.bool,
/**
* The metadata for the subject. This is used to display the icon and name
* and is selected from the domain in the SIWE request.
*/
subjectMetadata: PropTypes.object,
};

@ -0,0 +1,36 @@
import React from 'react';
import testData from '../../../../../.storybook/test-data';
import SignatureRequestSIWEHeader from '.';
const primaryIdentity = Object.values(testData.metamask.identities)[0];
const subjectMetadata = {
iconUrl: '/images/logo/metamask-fox.svg',
name: 'MetaMask',
origin: 'http://localhost:8080',
};
export default {
title: 'Components/App/SignatureRequestSIWE/SignatureRequestSIWEHeader',
id: __filename,
argTypes: {
fromAccount: {
table: {
address: { control: 'text' },
balance: { control: 'text' },
name: { control: 'text' },
},
},
domain: { control: 'text' },
subjectMetadata: { control: 'object' },
},
};
export const DefaultStory = (args) => <SignatureRequestSIWEHeader {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
fromAccount: primaryIdentity,
domain: window.location.host,
subjectMetadata,
};

@ -0,0 +1 @@
export { default } from './signature-request-siwe-message';

@ -0,0 +1,13 @@
.signature-request-siwe-message {
flex: 1 100%;
border-radius: 8px;
padding: 8px 16px;
margin: 16px;
border: 1px solid var(--color-border-muted);
&__sub-text {
white-space: pre-line;
overflow: hidden;
word-wrap: break-word;
}
}

@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import Typography from '../../../ui/typography';
import {
FLEX_DIRECTION,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
const SignatureRequestSIWEMessage = ({ data }) => {
return (
<Box className="signature-request-siwe-message">
<Box flexDirection={FLEX_DIRECTION.COLUMN}>
{data.map(({ label, value }, i) => (
<Box key={i.toString()} marginTop={2} marginBottom={2}>
<Typography variant={TYPOGRAPHY.H4} marginTop={2} marginBottom={2}>
{label}
</Typography>
<Typography
className="signature-request-siwe-message__sub-text"
variant={TYPOGRAPHY.H6}
marginTop={2}
marginBottom={2}
>
{value}
</Typography>
</Box>
))}
</Box>
</Box>
);
};
SignatureRequestSIWEMessage.propTypes = {
/**
* The data array that contains objects of data about the message
*/
data: PropTypes.arrayOf(
PropTypes.shape({
/**
* The label or title of the value data
*/
label: PropTypes.string,
/**
* The value of the data
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
),
};
export default React.memo(SignatureRequestSIWEMessage);

@ -0,0 +1,51 @@
import React from 'react';
import SignatureRequestMessage from '.';
export default {
title: 'Components/App/SignatureRequestSIWE/SignatureRequestMessage',
id: __filename,
argTypes: {
data: {
controls: 'object',
},
},
args: {
data: [
{ label: 'Label:', value: 'value' },
{
label: 'Message:',
value:
'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos',
},
{
label: 'URI:',
value: 'http://localhost:8080',
},
{
label: 'Version:',
value: '1',
},
{
label: 'Chain ID:',
value: 1,
},
{
label: 'Nonce:',
value: 'STMt6KQMwwdOXE306',
},
{
label: 'Issued At:',
value: '2022-03-18T21:40:40.823Z',
},
{
label: 'Resources: 2',
value:
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu\nhttps://example.com/my-web2-claim.json',
},
],
},
};
export const DefaultStory = (args) => <SignatureRequestMessage {...args} />;
DefaultStory.storyName = 'Default';

@ -0,0 +1,174 @@
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import log from 'loglevel';
import ActionableMessage from '../../ui/actionable-message';
import Popover from '../../ui/popover';
import Checkbox from '../../ui/check-box';
import { I18nContext } from '../../../contexts/i18n';
import { PageContainerFooter } from '../../ui/page-container';
import {
accountsWithSendEtherInfoSelector,
getSubjectMetadata,
} from '../../../selectors';
import { getAccountByAddress } from '../../../helpers/utils/util';
import { formatMessageParams } from '../../../../shared/modules/siwe';
import Header from './signature-request-siwe-header';
import Message from './signature-request-siwe-message';
export default function SignatureRequestSIWE({
txData,
cancelPersonalMessage,
signPersonalMessage,
}) {
const allAccounts = useSelector(accountsWithSendEtherInfoSelector);
const subjectMetadata = useSelector(getSubjectMetadata);
const {
msgParams: {
from,
origin,
siwe: { parsedMessage },
},
} = txData;
const fromAccount = getAccountByAddress(allAccounts, from);
const targetSubjectMetadata = subjectMetadata[origin];
const t = useContext(I18nContext);
const isMatchingAddress =
from.toLowerCase() === parsedMessage.address.toLowerCase();
const checkSIWEDomain = () => {
let isSIWEDomainValid = false;
if (origin) {
const { host } = new URL(origin);
isSIWEDomainValid = parsedMessage.domain === host;
}
return isSIWEDomainValid;
};
const isSIWEDomainValid = checkSIWEDomain();
const [isShowingDomainWarning, setIsShowingDomainWarning] = useState(false);
const [agreeToDomainWarning, setAgreeToDomainWarning] = useState(false);
const onSign = useCallback(
async (event) => {
try {
await signPersonalMessage(event);
} catch (e) {
log.error(e);
}
},
[signPersonalMessage],
);
const onCancel = useCallback(
async (event) => {
try {
await cancelPersonalMessage(event);
} catch (e) {
log.error(e);
}
},
[cancelPersonalMessage],
);
return (
<div className="signature-request-siwe">
<Header
fromAccount={fromAccount}
domain={parsedMessage.domain}
isSIWEDomainValid={isSIWEDomainValid}
subjectMetadata={targetSubjectMetadata}
/>
<Message data={formatMessageParams(parsedMessage, t)} />
{!isMatchingAddress && (
<ActionableMessage
className="signature-request-siwe__actionable-message"
type="warning"
message={t('SIWEAddressInvalid', [
parsedMessage.address,
fromAccount.address,
])}
iconFillColor="var(--color-warning-default)"
useIcon
withRightButton
/>
)}
{!isSIWEDomainValid && (
<ActionableMessage
className="signature-request-siwe__actionable-message"
type="danger"
message={t('SIWEDomainInvalid', [parsedMessage.domain])}
iconFillColor="var(--color-error-default)"
useIcon
withRightButton
/>
)}
<PageContainerFooter
footerClassName="signature-request-siwe__page-container-footer"
onCancel={onCancel}
onSubmit={
isSIWEDomainValid ? onSign : () => setIsShowingDomainWarning(true)
}
cancelText={t('cancel')}
submitText={t('signin')}
/>
{isShowingDomainWarning && (
<Popover
onClose={() => setIsShowingDomainWarning(false)}
title={t('SIWEWarningTitle')}
subtitle={t('SIWEWarningSubtitle')}
className="signature-request-siwe__warning-popover"
footerClassName="signature-request-siwe__warning-popover__footer"
footer={
<PageContainerFooter
footerClassName="signature-request-siwe__warning-popover__footer__warning-footer"
onCancel={() => setIsShowingDomainWarning(false)}
cancelText={t('cancel')}
cancelButtonType="default"
onSubmit={onSign}
submitText={t('confirm')}
submitButtonType="danger-primary"
disabled={!agreeToDomainWarning}
/>
}
>
<div className="signature-request-siwe__warning-popover__checkbox-wrapper">
<Checkbox
id="signature-request-siwe_domain-checkbox"
checked={agreeToDomainWarning}
className="signature-request-siwe__warning-popover__checkbox-wrapper__checkbox"
onClick={() => setAgreeToDomainWarning((checked) => !checked)}
/>
<label
className="signature-request-siwe__warning-popover__checkbox-wrapper__label"
htmlFor="signature-request-siwe_domain-checkbox"
>
{t('SIWEDomainWarningBody', [parsedMessage.domain])}
</label>
</div>
</Popover>
)}
</div>
);
}
SignatureRequestSIWE.propTypes = {
/**
* The display content of transaction data
*/
txData: PropTypes.object.isRequired,
/**
* Handler for cancel button
*/
cancelPersonalMessage: PropTypes.func.isRequired,
/**
* Handler for sign button
*/
signPersonalMessage: PropTypes.func.isRequired,
};

@ -0,0 +1,165 @@
import React from 'react';
import testData from '../../../../.storybook/test-data';
import README from './README.mdx';
import SignatureRequestSIWE from './signature-request-siwe';
const { identities, selectedAddress } = testData.metamask;
const otherIdentity = Object.values(identities)[0];
export default {
title: 'Components/App/SignatureRequestSIWE',
id: __filename,
component: SignatureRequestSIWE,
parameters: {
docs: {
page: README,
},
},
argTypes: {
txData: { control: 'object' },
cancelPersonalMessage: { action: 'Cancel' },
signPersonalMessage: { action: 'Sign' },
},
};
const msgParams = {
from: selectedAddress,
data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e',
origin: 'http://localhost:8080',
siwe: {
isSIWEMessage: true,
isSIWEDomainValid: true,
parsedMessage: {
domain: 'localhost:8080',
address: selectedAddress,
statement:
'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos',
uri: 'http://localhost:8080',
version: '1',
nonce: 'STMt6KQMwwdOXE306',
chainId: 1,
issuedAt: '2022-03-18T21:40:40.823Z',
resources: [
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
'https://example.com/my-web2-claim.json',
],
},
},
};
const badDomainParams = {
from: selectedAddress,
data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e',
origin: 'http://localhost:8080',
siwe: {
isSIWEMessage: true,
isSIWEDomainValid: false,
parsedMessage: {
domain: 'baddomain.com',
address: selectedAddress,
statement:
'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos',
uri: 'http://localhost:8080',
version: '1',
nonce: 'STMt6KQMwwdOXE306',
chainId: 1,
issuedAt: '2022-03-18T21:40:40.823Z',
resources: [
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
'https://example.com/my-web2-claim.json',
],
},
},
};
const badAddressParams = {
from: otherIdentity.address,
data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e',
origin: 'http://localhost:8080',
siwe: {
isSIWEMessage: true,
isSIWEDomainValid: true,
parsedMessage: {
domain: 'localhost:8080',
address: selectedAddress,
statement:
'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos',
uri: 'http://localhost:8080',
version: '1',
nonce: 'STMt6KQMwwdOXE306',
chainId: 1,
issuedAt: '2022-03-18T21:40:40.823Z',
resources: [
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
'https://example.com/my-web2-claim.json',
],
},
},
};
const badDomainAndAddressParams = {
from: otherIdentity.address,
data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e',
origin: 'http://localhost:8080',
siwe: {
isSIWEMessage: true,
isSIWEDomainValid: false,
parsedMessage: {
domain: 'baddomain.com',
address: selectedAddress,
statement:
'Click to sign in and accept the Terms of Service: https://community.metamask.io/tos',
uri: 'http://localhost:8080',
version: '1',
nonce: 'STMt6KQMwwdOXE306',
chainId: 1,
issuedAt: '2022-03-18T21:40:40.823Z',
resources: [
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
'https://example.com/my-web2-claim.json',
],
},
},
};
export const DefaultStory = (args) => {
return <SignatureRequestSIWE {...args} />;
};
DefaultStory.storyName = 'Default';
DefaultStory.args = {
txData: {
msgParams,
},
};
export const BadDomainStory = (args) => {
return <SignatureRequestSIWE {...args} />;
};
BadDomainStory.args = {
txData: {
msgParams: badDomainParams,
},
};
export const BadAddressStory = (args) => {
return <SignatureRequestSIWE {...args} />;
};
BadAddressStory.args = {
txData: {
msgParams: badAddressParams,
},
};
export const BadDomainAndAddressStory = (args) => {
return <SignatureRequestSIWE {...args} />;
};
BadDomainAndAddressStory.args = {
txData: {
msgParams: badDomainAndAddressParams,
},
};

@ -11,6 +11,7 @@ export default function SiteOrigin({
chip,
className,
title,
rightIcon,
}) {
return (
<div className={classnames('site-origin', className)} title={title}>
@ -21,6 +22,7 @@ export default function SiteOrigin({
leftIcon={
<IconWithFallback icon={iconSrc} name={iconName} size={32} />
}
rightIcon={rightIcon}
/>
) : (
<span>{siteOrigin}</span>
@ -57,4 +59,8 @@ SiteOrigin.propTypes = {
* if false iconSrc and iconName props will not be used.
*/
chip: PropTypes.bool,
/**
* The icon to display on the right side of the chip.
*/
rightIcon: PropTypes.node,
};

@ -1,4 +1,5 @@
import React from 'react';
import InfoIcon from '../icon/info-icon.component';
import SiteOrigin from '.';
@ -36,3 +37,12 @@ DefaultStory.args = {
iconSrc: './metamark.svg',
chip: true,
};
export const RightIcon = (args) => <SiteOrigin {...args} />;
RightIcon.args = {
siteOrigin: 'https://metamask.io',
iconName: 'MetaMask',
iconSrc: './metamark.svg',
rightIcon: <InfoIcon />,
};

@ -7,6 +7,7 @@ import log from 'loglevel';
import * as actions from '../../store/actions';
import txHelper from '../../helpers/utils/tx-helper';
import SignatureRequest from '../../components/app/signature-request';
import SignatureRequestSIWE from '../../components/app/signature-request-siwe';
import SignatureRequestOriginal from '../../components/app/signature-request-original';
import Loading from '../../components/ui/loading-screen';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
@ -114,7 +115,12 @@ class ConfirmTxScreen extends Component {
: unconfTxList[index];
}
signatureSelect(type, version) {
signatureSelect(txData) {
const {
type,
msgParams: { version, siwe },
} = txData;
// Temporarily direct only v3 and v4 requests to new code.
if (
type === MESSAGE_TYPE.ETH_SIGN_TYPED_DATA &&
@ -123,6 +129,10 @@ class ConfirmTxScreen extends Component {
return SignatureRequest;
}
if (process.env.SIWE_V1 && siwe?.isSIWEMessage) {
return SignatureRequestSIWE;
}
return SignatureRequestOriginal;
}
@ -251,18 +261,15 @@ class ConfirmTxScreen extends Component {
const { currentCurrency, blockGasLimit } = this.props;
const txData = this.getTxData() || {};
const {
msgParams,
type,
msgParams: { version },
} = txData;
const { msgParams } = txData;
log.debug('msgParams detected, rendering pending msg');
if (!msgParams) {
return <Loading />;
}
const SigComponent = this.signatureSelect(type, version);
const SigComponent = this.signatureSelect(txData);
return (
<SigComponent
txData={txData}

@ -3460,6 +3460,13 @@
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@spruceid/siwe-parser@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@spruceid/siwe-parser/-/siwe-parser-1.1.3.tgz#0eebe8bbd63c6de89cb44c06b6329b00b305df65"
integrity sha512-oQ8PcwDqjGWJvLmvAF2yzd6iniiWxK0Qtz+Dw+gLD/W5zOQJiKIUXwslHOm8VB8OOOKW9vfR3dnPBhHaZDvRsw==
dependencies:
apg-js "^4.1.1"
"@stablelib/utf8@^0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@stablelib/utf8/-/utf8-0.10.1.tgz#eecf54884da7b2bee235e3c70efb8cd5c07ba5bd"
@ -5803,6 +5810,11 @@ anymatch@^3.0.0, anymatch@^3.0.3, anymatch@^3.1.0, anymatch@~3.1.1, anymatch@~3.
normalize-path "^3.0.0"
picomatch "^2.0.4"
apg-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/apg-js/-/apg-js-4.1.1.tgz#5ee8a74e26517073e1bda66705eca6fc22b305e2"
integrity sha512-DwTfzx1YuCrnEvywiU/AYKiX8Y6JzhY8PwaM9syh54zzBPaHzonN7c4YsAspC6YcdSu/jfBXBJ1S9hj1QsiePA==
app-module-path@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"

Loading…
Cancel
Save