diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | app/scripts/background.js | 8 | ||||
-rw-r--r-- | app/scripts/edge-encryptor.js | 69 | ||||
-rw-r--r-- | mascara/src/app/first-time/import-seed-phrase-screen.js | 191 | ||||
-rw-r--r-- | mascara/src/app/first-time/index.css | 16 | ||||
-rw-r--r-- | old-ui/app/info.js | 6 | ||||
-rw-r--r-- | package.json | 6 | ||||
-rw-r--r-- | test/unit/edge-encryptor-test.js | 101 | ||||
-rw-r--r-- | ui/app/add-token.js | 2 | ||||
-rw-r--r-- | ui/app/app.js | 8 | ||||
-rw-r--r-- | ui/app/css/itcss/components/header.scss | 7 | ||||
-rw-r--r-- | ui/app/send-v2.js | 5 |
12 files changed, 324 insertions, 100 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc9d2145..fdc7d7155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,15 @@ ## Current Master +- Allow adding custom tokens to classic ui when balance is 0 +- Allow editing of symbol and decimal info when adding custom token in new-ui +- NewUI shapeshift form can select all coins (not just BTC) + ## 4.1.3 2018-2-28 - Ensure MetaMask's inpage provider is named MetamaskInpageProvider to keep some sites from breaking. - Add retry transaction button back into classic ui. +- Add network dropdown styles to support long custom RPC urls ## 4.1.2 2018-2-28 diff --git a/app/scripts/background.js b/app/scripts/background.js index 4487ff318..601ae0372 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -16,6 +16,7 @@ const firstTimeState = require('./first-time-state') const setupRaven = require('./lib/setupRaven') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') +const EdgeEncryptor = require('./edge-encryptor') const STORAGE_KEY = 'metamask-config' @@ -32,6 +33,12 @@ global.METAMASK_NOTIFIER = notificationManager const release = platform.getVersion() const raven = setupRaven({ release }) +// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser +// Internet Explorer 6-11 +const isIE = !!document.documentMode +// Edge 20+ +const isEdge = !isIE && !!window.StyleMedia + let popupIsOpen = false let openMetamaskTabsIDs = {} @@ -81,6 +88,7 @@ function setupController (initState) { initState, // platform specific api platform, + encryptor: isEdge ? new EdgeEncryptor() : undefined, }) global.metamaskController = controller diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js new file mode 100644 index 000000000..24c0c93a8 --- /dev/null +++ b/app/scripts/edge-encryptor.js @@ -0,0 +1,69 @@ +const asmcrypto = require('asmcrypto.js') +const Unibabel = require('browserify-unibabel') + +class EdgeEncryptor { + + encrypt (password, dataObject) { + + var salt = this._generateSalt() + return this._keyFromPassword(password, salt) + .then(function (key) { + + var data = JSON.stringify(dataObject) + var dataBuffer = Unibabel.utf8ToBuffer(data) + var vector = global.crypto.getRandomValues(new Uint8Array(16)) + var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector) + + var buffer = new Uint8Array(resultbuffer) + var vectorStr = Unibabel.bufferToBase64(vector) + var vaultStr = Unibabel.bufferToBase64(buffer) + return JSON.stringify({ + data: vaultStr, + iv: vectorStr, + salt: salt, + }) + }) + } + + decrypt (password, text) { + + const payload = JSON.parse(text) + const salt = payload.salt + return this._keyFromPassword(password, salt) + .then(function (key) { + const encryptedData = Unibabel.base64ToBuffer(payload.data) + const vector = Unibabel.base64ToBuffer(payload.iv) + return new Promise((resolve, reject) => { + var result + try { + result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector) + } catch (err) { + return reject(new Error('Incorrect password')) + } + const decryptedData = new Uint8Array(result) + const decryptedStr = Unibabel.bufferToUtf8(decryptedData) + const decryptedObj = JSON.parse(decryptedStr) + resolve(decryptedObj) + }) + }) + } + + _keyFromPassword (password, salt) { + + var passBuffer = Unibabel.utf8ToBuffer(password) + var saltBuffer = Unibabel.base64ToBuffer(salt) + return new Promise((resolve) => { + var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000) + resolve(key) + }) + } + + _generateSalt (byteCount = 32) { + var view = new Uint8Array(byteCount) + global.crypto.getRandomValues(view) + var b64encoded = btoa(String.fromCharCode.apply(null, view)) + return b64encoded + } +} + +module.exports = EdgeEncryptor diff --git a/mascara/src/app/first-time/import-seed-phrase-screen.js b/mascara/src/app/first-time/import-seed-phrase-screen.js index 93c3f9203..de8d675e1 100644 --- a/mascara/src/app/first-time/import-seed-phrase-screen.js +++ b/mascara/src/app/first-time/import-seed-phrase-screen.js @@ -1,13 +1,12 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' -import LoadingScreen from './loading-screen' +import classnames from 'classnames' import { createNewVaultAndRestore, hideWarning, displayWarning, unMarkPasswordForgotten, - clearNotices, } from '../../../../ui/app/actions' class ImportSeedPhraseScreen extends Component { @@ -17,8 +16,8 @@ class ImportSeedPhraseScreen extends Component { next: PropTypes.func.isRequired, createNewVaultAndRestore: PropTypes.func.isRequired, hideWarning: PropTypes.func.isRequired, - isLoading: PropTypes.bool.isRequired, displayWarning: PropTypes.func, + leaveImportSeedScreenState: PropTypes.func, }; state = { @@ -27,98 +26,130 @@ class ImportSeedPhraseScreen extends Component { confirmPassword: '', } - onClick = () => { - const { password, seedPhrase, confirmPassword } = this.state - const { createNewVaultAndRestore, next, displayWarning, leaveImportSeedScreenState } = this.props + parseSeedPhrase = (seedPhrase) => { + return seedPhrase + .match(/\w+/g) + .join(' ') + } - if (seedPhrase.split(' ').length !== 12) { - this.warning = 'Seed Phrases are 12 words long' - displayWarning(this.warning) - return - } + onChange = ({ seedPhrase, password, confirmPassword }) => { + const { + password: prevPassword, + confirmPassword: prevConfirmPassword, + } = this.state + const { displayWarning, hideWarning } = this.props + + let warning = null - if (password.length < 8) { - this.warning = 'Passwords require a mimimum length of 8' - displayWarning(this.warning) - return + if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { + warning = 'Seed Phrases are 12 words long' + } else if (password && password.length < 8) { + warning = 'Passwords require a mimimum length of 8' + } else if ((password || prevPassword) !== (confirmPassword || prevConfirmPassword)) { + warning = 'Confirmed password does not match' } - if (password !== confirmPassword) { - this.warning = 'Confirmed password does not match' - displayWarning(this.warning) - return + if (warning) { + displayWarning(warning) + } else { + hideWarning() } - this.warning = null + + seedPhrase && this.setState({ seedPhrase }) + password && this.setState({ password }) + confirmPassword && this.setState({ confirmPassword }) + } + + onClick = () => { + const { password, seedPhrase } = this.state + const { + createNewVaultAndRestore, + next, + displayWarning, + leaveImportSeedScreenState, + } = this.props + leaveImportSeedScreenState() - createNewVaultAndRestore(password, seedPhrase) + createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase)) .then(next) } render () { - return this.props.isLoading - ? <LoadingScreen loadingMessage="Creating your new account" /> - : ( - <div className="import-account"> - <a - className="import-account__back-button" - onClick={e => { - e.preventDefault() - this.props.back() - }} - href="#" - > - {`< Back`} - </a> - <div className="import-account__title"> - Import an Account with Seed Phrase - </div> - <div className="import-account__selector-label"> - Enter your secret twelve word phrase here to restore your vault. - </div> - <div className="import-account__input-wrapper"> - <label className="import-account__input-label">Wallet Seed</label> - <textarea - className="import-account__secret-phrase" - onChange={e => this.setState({seedPhrase: e.target.value})} - placeholder="Separate each word with a single space" - /> - </div> - <span - className="error" - > - {this.props.warning} - </span> - <div className="import-account__input-wrapper"> - <label className="import-account__input-label">New Password</label> - <input - className="first-time-flow__input" - type="password" - placeholder="New Password (min 8 characters)" - onChange={e => this.setState({password: e.target.value})} - /> - </div> - <div className="import-account__input-wrapper"> - <label className="import-account__input-label">Confirm Password</label> - <input - className="first-time-flow__input" - type="password" - placeholder="Confirm Password" - onChange={e => this.setState({confirmPassword: e.target.value})} - /> - </div> - <button - className="first-time-flow__button" - onClick={this.onClick} - > - Import - </button> + const { seedPhrase, password, confirmPassword } = this.state + const { warning } = this.props + const importDisabled = warning || !seedPhrase || !password || !confirmPassword + return ( + <div className="import-account"> + <a + className="import-account__back-button" + onClick={e => { + e.preventDefault() + this.props.back() + }} + href="#" + > + {`< Back`} + </a> + <div className="import-account__title"> + Import an Account with Seed Phrase + </div> + <div className="import-account__selector-label"> + Enter your secret twelve word phrase here to restore your vault. + </div> + <div className="import-account__input-wrapper"> + <label className="import-account__input-label">Wallet Seed</label> + <textarea + className="import-account__secret-phrase" + onChange={e => this.onChange({seedPhrase: e.target.value})} + value={this.state.seedPhrase} + placeholder="Separate each word with a single space" + /> + </div> + <span + className="error" + > + {this.props.warning} + </span> + <div className="import-account__input-wrapper"> + <label className="import-account__input-label">New Password</label> + <input + className="first-time-flow__input" + type="password" + placeholder="New Password (min 8 characters)" + onChange={e => this.onChange({password: e.target.value})} + /> + </div> + <div className="import-account__input-wrapper"> + <label + className="import-account__input-label" + className={classnames('import-account__input-label', { + 'import-account__input-label__disabled': password.length < 8, + })} + >Confirm Password</label> + <input + className={classnames('first-time-flow__input', { + 'first-time-flow__input__disabled': password.length < 8, + })} + type="password" + placeholder="Confirm Password" + onChange={e => this.onChange({confirmPassword: e.target.value})} + disabled={password.length < 8} + /> </div> - ) + <button + className="first-time-flow__button" + onClick={() => !importDisabled && this.onClick()} + disabled={importDisabled} + > + Import + </button> + </div> + ) } } export default connect( - ({ appState: { isLoading, warning } }) => ({ isLoading, warning }), + ({ appState: { warning } }) => ({ warning }), dispatch => ({ leaveImportSeedScreenState: () => { dispatch(unMarkPasswordForgotten()) diff --git a/mascara/src/app/first-time/index.css b/mascara/src/app/first-time/index.css index 109946e1d..f59eb4ce1 100644 --- a/mascara/src/app/first-time/index.css +++ b/mascara/src/app/first-time/index.css @@ -1,11 +1,11 @@ .first-time-flow { - height: 100vh; width: 100vw; - background-color: #FFF; + background-color: #fff; overflow: auto; display: flex; justify-content: center; + flex: 1 0 auto; } .alpha-warning { @@ -45,12 +45,12 @@ .buy-ether { display: flex; flex-flow: column nowrap; - margin: 67px 0 50px 146px; + margin: 67px 0 50px; max-width: 35rem; } .create-password { - margin: 67px 0 50px 0px; + margin: 67px 0 50px; } .import-account { @@ -418,6 +418,10 @@ button.backup-phrase__confirm-seed-option:hover { line-height: 23px; } +.import-account__input-label__disabled { + opacity: 0.5; +} + .import-account__input { width: 325px !important; } @@ -564,6 +568,10 @@ button.backup-phrase__confirm-seed-option:hover { background-color: #FFFFFF; } +.first-time-flow__input__disabled { + opacity: 0.5; +} + .first-time-flow__input::placeholder { color: #9B9B9B; font-weight: 200; diff --git a/old-ui/app/info.js b/old-ui/app/info.js index db9f30f23..d79b8a3d2 100644 --- a/old-ui/app/info.js +++ b/old-ui/app/info.js @@ -63,7 +63,7 @@ InfoScreen.prototype.render = function () { h('a', { href: 'https://metamask.io/privacy.html', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, + onClick: (event) => { this.navigateTo(event.target.href) }, }, [ h('div.info', 'Privacy Policy'), ]), @@ -72,7 +72,7 @@ InfoScreen.prototype.render = function () { h('a', { href: 'https://metamask.io/terms.html', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, + onClick: (event) => { this.navigateTo(event.target.href) }, }, [ h('div.info', 'Terms of Use'), ]), @@ -81,7 +81,7 @@ InfoScreen.prototype.render = function () { h('a', { href: 'https://metamask.io/attributions.html', target: '_blank', - onClick (event) { this.navigateTo(event.target.href) }, + onClick: (event) => { this.navigateTo(event.target.href) }, }, [ h('div.info', 'Attributions'), ]), diff --git a/package.json b/package.json index d4b498bc8..80949901a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "abi-decoder": "^1.0.9", + "asmcrypto.js": "0.22.0", "async": "^2.5.0", "await-semaphore": "^0.1.1", "babel-runtime": "^6.23.0", @@ -64,6 +65,7 @@ "boron": "^0.2.3", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", + "browserify-unibabel": "^3.0.0", "classnames": "^2.2.5", "client-sw-ready-event": "^3.3.0", "clone": "^2.1.1", @@ -78,11 +80,11 @@ "eslint-plugin-react": "^7.4.0", "eth-bin-to-ops": "^1.0.1", "eth-block-tracker": "^2.3.0", + "eth-contract-metadata": "^1.1.5", + "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.5", "eth-json-rpc-infura": "^3.0.0", "eth-keyring-controller": "^2.1.4", - "eth-contract-metadata": "^1.1.5", - "eth-hd-keyring": "^1.2.1", "eth-phishing-detect": "^1.1.4", "eth-query": "^2.1.2", "eth-sig-util": "^1.4.2", diff --git a/test/unit/edge-encryptor-test.js b/test/unit/edge-encryptor-test.js new file mode 100644 index 000000000..d3f014d74 --- /dev/null +++ b/test/unit/edge-encryptor-test.js @@ -0,0 +1,101 @@ +const assert = require('assert') + +const EdgeEncryptor = require('../../app/scripts/edge-encryptor') + +var password = 'passw0rd1' +var data = 'some random data' + +global.crypto = global.crypto || { + getRandomValues: function (array) { + for (let i = 0; i < array.length; i++) { + array[i] = Math.random() * 100 + } + return array + } +} + +describe('EdgeEncryptor', function () { + + const edgeEncryptor = new EdgeEncryptor() + describe('encrypt', function () { + + it('should encrypt the data.', function (done) { + edgeEncryptor.encrypt(password, data) + .then(function (encryptedData) { + assert.notEqual(data, encryptedData) + assert.notEqual(encryptedData.length, 0) + done() + }).catch(function (err) { + done(err) + }) + }) + + it('should return proper format.', function (done) { + edgeEncryptor.encrypt(password, data) + .then(function (encryptedData) { + let encryptedObject = JSON.parse(encryptedData) + assert.ok(encryptedObject.data, 'there is no data') + assert.ok(encryptedObject.iv && encryptedObject.iv.length != 0, 'there is no iv') + assert.ok(encryptedObject.salt && encryptedObject.salt.length != 0, 'there is no salt') + done() + }).catch(function (err) { + done(err) + }) + }) + + it('should not return the same twice.', function (done) { + + const encryptPromises = [] + encryptPromises.push(edgeEncryptor.encrypt(password, data)) + encryptPromises.push(edgeEncryptor.encrypt(password, data)) + + Promise.all(encryptPromises).then((encryptedData) => { + assert.equal(encryptedData.length, 2) + assert.notEqual(encryptedData[0], encryptedData[1]) + assert.notEqual(encryptedData[0].length, 0) + assert.notEqual(encryptedData[1].length, 0) + done() + }) + }) + }) + + describe('decrypt', function () { + it('should be able to decrypt the encrypted data.', function (done) { + + edgeEncryptor.encrypt(password, data) + .then(function (encryptedData) { + edgeEncryptor.decrypt(password, encryptedData) + .then(function (decryptedData) { + assert.equal(decryptedData, data) + done() + }) + .catch(function (err) { + done(err) + }) + }) + .catch(function (err) { + done(err) + }) + }) + + it('cannot decrypt the encrypted data with wrong password.', function (done) { + + edgeEncryptor.encrypt(password, data) + .then(function (encryptedData) { + edgeEncryptor.decrypt('wrong password', encryptedData) + .then(function (decryptedData) { + assert.fail('could decrypt with wrong password') + done() + }) + .catch(function (err) { + assert.ok(err instanceof Error) + assert.equal(err.message, 'Incorrect password') + done() + }) + }) + .catch(function (err) { + done(err) + }) + }) + }) +}) diff --git a/ui/app/add-token.js b/ui/app/add-token.js index a1729ba8e..51c577987 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -52,7 +52,7 @@ function AddTokenScreen () { isShowingConfirmation: false, customAddress: '', customSymbol: '', - customDecimals: null, + customDecimals: '', searchQuery: '', isCollapsed: true, selectedTokens: {}, diff --git a/ui/app/app.js b/ui/app/app.js index d243e72a4..4e6da24c3 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -288,10 +288,10 @@ App.prototype.renderAppBar = function () { }), // metamask name - h('h1', 'MetaMask'), - - h('div.beta-label', 'BETA'), - + h('.flex-row', [ + h('h1', 'MetaMask'), + h('div.beta-label', 'BETA'), + ]), ]), h('div.header__right-actions', [ diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/css/itcss/components/header.scss index d91ab3c48..eeed9ee06 100644 --- a/ui/app/css/itcss/components/header.scss +++ b/ui/app/css/itcss/components/header.scss @@ -80,11 +80,10 @@ font-family: Roboto; text-transform: uppercase; font-weight: 500; - font-size: 0.8rem; - padding-left: 9px; + font-size: .8rem; color: $buttercup; - align-self: flex-start; - margin-top: 10px; + margin-left: 5px; + line-height: initial; @media screen and (max-width: 575px) { display: none; diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 6ee7c0ca5..fc1df1f51 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -397,14 +397,15 @@ SendTransactionScreen.prototype.renderAmountRow = function () { amount, setMaxModeTo, maxModeOn, + gasTotal, } = this.props return h('div.send-v2__form-row', [ - h('div.send-v2__form-label', [ + h('div.send-v2__form-label', [ 'Amount:', this.renderErrorMessage('amount'), - !errors.amount && h('div.send-v2__amount-max', { + !errors.amount && gasTotal && h('div.send-v2__amount-max', { onClick: (event) => { event.preventDefault() setMaxModeTo(true) |