Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV-6133, DEV-6134, DEV-6136: wyre security model support #4

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/manifest/chrome.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"externally_connectable": {
"matches": ["https://metamask.io/*"],
"matches": ["https://metamask.io/*", "https://pay.sendwyre.com/*"],
"ids": ["*"]
},
"minimum_chrome_version": "63"
Expand Down
23 changes: 23 additions & 0 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ function setupController(initState, initLangCode) {
extension.runtime.onConnect.addListener(connectRemote);
extension.runtime.onConnectExternal.addListener(connectExternal);

//
// get message from other contexts
//
extension.runtime.onMessageExternal.addListener(getMessage);

const metamaskInternalProcessHash = {
[ENVIRONMENT_TYPE_POPUP]: true,
[ENVIRONMENT_TYPE_NOTIFICATION]: true,
Expand Down Expand Up @@ -377,6 +382,24 @@ function setupController(initState, initLangCode) {
controller.setupUntrustedCommunication(portStream, remotePort.sender);
}

/**
* A runtime.MessageSender object, as provided by the browser:
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender
* @typedef MessageSender
* @type Object
*/

/**
* Get message from external web pages or extensions
* This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages).
* @param {Object} request - The message provided by a web page or extension.
* @param {MessageSender} sender - The object giving details about the message sender.
* @param {Function} sendResponse - The function which it can use to send a response back to the sender.
*/
function getMessage(request, sender, sendResponse) {
controller.getMessageExternal(request, sender, sendResponse);
}

//
// User Interface setup
//
Expand Down
10 changes: 10 additions & 0 deletions app/scripts/controllers/app-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class AppStateController extends EventEmitter {
browserEnvironment: {},
recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(),
ESIDToken: null,
...initState,
});
this.timer = null;
Expand Down Expand Up @@ -136,6 +137,15 @@ export default class AppStateController extends EventEmitter {
});
}

/**
* Sets the secure random token
*/
setESIDToken(token) {
this.store.updateState({
ESIDToken: token,
});
}

/**
* Sets the last active time to the current time
* @returns {void}
Expand Down
16 changes: 12 additions & 4 deletions app/scripts/lib/buy-eth-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ const fetchWithTimeout = getFetchWithTimeout(SECOND * 30);
* @param {String} address Ethereum destination address
* @returns String
*/
const createWyrePurchaseUrl = async (address) => {
const createWyrePurchaseUrl = async (address, esid) => {
const fiatOnRampUrlApi = `${METASWAP_CHAINID_API_HOST_MAP[MAINNET_CHAIN_ID]}/fiatOnRampUrl?serviceName=wyre&destinationAddress=${address}`;
const wyrePurchaseUrlFallback = `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card`;
const esidParam = esid ? `&ESID=${esid}` : '';
const wyrePurchaseUrlFallback = `https://pay.sendwyre.com/purchase?dest=ethereum:${address}&destCurrency=ETH&accountId=AC-7AG3W4XH4N2&paymentMethod=debit-card${esidParam}`;
// NOTE: actions.js buyEth method depends on the ESID being at the end of this querystring
try {
const response = await fetchWithTimeout(fiatOnRampUrlApi, {
method: 'GET',
Expand Down Expand Up @@ -63,11 +65,17 @@ const createTransakUrl = (address) => {
* @param {Object} opts - Options required to determine the correct url
* @param {string} opts.chainId - The chainId for which to return a url
* @param {string} opts.address - The address the bought ETH should be sent to. Only relevant if chainId === '0x1'.
* @param {string} esidToken - The random generated secure token.
* @returns {string|undefined} The url at which the user can access ETH, while in the given chain. If the passed
* chainId does not match any of the specified cases, or if no chainId is given, returns undefined.
*
*/
export default async function getBuyEthUrl({ chainId, address, service }) {
export default async function getBuyEthUrl({
chainId,
address,
service,
esid,
}) {
// default service by network if not specified
if (!service) {
// eslint-disable-next-line no-param-reassign
Expand All @@ -76,7 +84,7 @@ export default async function getBuyEthUrl({ chainId, address, service }) {

switch (service) {
case 'wyre':
return await createWyrePurchaseUrl(address);
return createWyrePurchaseUrl(address, esid);
case 'transak':
return createTransakUrl(address);
case 'metamask-faucet':
Expand Down
9 changes: 9 additions & 0 deletions app/scripts/lib/buy-eth-url.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ describe('buy-eth-url', function () {
);
});

it('returns a fallback Wyre url with ESID if /orders/reserve API call fails', async function () {
const wyreUrl = await getBuyEthUrl({ ...MAINNET, esid: '123' });

assert.equal(
wyreUrl,
`https://pay.sendwyre.com/purchase?dest=ethereum:${ETH_ADDRESS}&destCurrency=ETH&accountId=${WYRE_ACCOUNT_ID}&paymentMethod=debit-card&ESID=123`,
);
});

it('returns Transak url with an ETH address for Ethereum mainnet', async function () {
const transakUrl = await getBuyEthUrl({ ...MAINNET, service: 'transak' });

Expand Down
37 changes: 37 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,10 @@ export default class MetamaskController extends EventEmitter {
this.appStateController.setRecoveryPhraseReminderLastShown,
this.appStateController,
),
setESIDToken: nodeify(
this.appStateController.setESIDToken,
this.appStateController,
),

// EnsController
tryReverseResolveAddress: nodeify(
Expand Down Expand Up @@ -2671,6 +2675,39 @@ export default class MetamaskController extends EventEmitter {
}
}

/**
* A runtime.MessageSender object, as provided by the browser:
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender
* @typedef MessageSender
* @type Object
*/

/**
* Connects a Port to the MetaMask controller via a multiplexed duplex stream.
* This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages).
* @param {Object} request - The message/data provided by a web page or extension.
* @param {MessageSender} sender - The object giving details about the message sender.
* @param {Function} sendResponse - The function which it can use to send a response back to the sender.
*/
getMessageExternal(request, sender, sendResponse) {
const { usePhishDetect } = this.preferencesController.store.getState();
const { hostname } = new URL(sender.url);
// Check if new connection is blocked if phishing detection is on
if (usePhishDetect && this.phishingController.test(hostname)) {
log.debug('MetaMask - sending phishing warning for', hostname);
// TODO: sendPhishingWarning ?
return;
}

if (sender.origin === 'https://pay.sendwyre.com' && request.wyreToken) {
const savedESIDToken = this.appStateController.store.getState().ESIDToken;
sendResponse({
wyreToken: request.wyreToken,
ESID: savedESIDToken,
});
}
}

//=============================================================================
// CONFIG
//=============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export default function EditGasDisplay({
warning,
gasErrors,
onManualChange,
networkSupports1559,
}) {
const t = useContext(I18nContext);

Expand Down Expand Up @@ -267,5 +266,4 @@ EditGasDisplay.propTypes = {
transaction: PropTypes.object,
gasErrors: PropTypes.object,
onManualChange: PropTypes.func,
networkSupports1559: PropTypes.boolean,
};
12 changes: 12 additions & 0 deletions ui/helpers/utils/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,15 @@ export function constructTxParams({
}
return addHexPrefixToObjectValues(txParams);
}

/**
* Generates 32 secure bytes token
* @returns {string}
*/
export function createToken() {
const array = new Uint32Array(16);
window.crypto.getRandomValues(array);
return Array.prototype.map
.call(array, (x) => `00${x.toString(16)}`.slice(-2))
.join('');
}
4 changes: 4 additions & 0 deletions ui/selectors/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ export function getIsSwapsChain(state) {
return ALLOWED_SWAPS_CHAIN_IDS[chainId];
}

export function getESIDToken(state) {
return state.metamask.ESIDToken;
}

export function getNativeCurrencyImage(state) {
const nativeCurrency = getNativeCurrency(state).toUpperCase();
return NATIVE_CURRENCY_TOKEN_IMAGE_MAP[nativeCurrency];
Expand Down
3 changes: 3 additions & 0 deletions ui/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import log from 'loglevel';
import { captureException } from '@sentry/browser';
import { capitalize, isEqual } from 'lodash';
import getBuyEthUrl from '../../app/scripts/lib/buy-eth-url';
import { createToken } from '../helpers/utils/util';
import {
fetchLocale,
loadRelativeTimeFormatLocaleData,
Expand Down Expand Up @@ -1840,6 +1841,8 @@ export function showSendTokenPage() {

export function buyEth(opts) {
return async (dispatch) => {
opts.esid = createToken();
promisifiedBackground.setESIDToken(opts.esid);
const url = await getBuyEthUrl(opts);
global.platform.openTab({ url });
dispatch({
Expand Down