diff options
author | Dan Finlay <542863+danfinlay@users.noreply.github.com> | 2018-03-07 02:32:47 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-07 02:32:47 +0800 |
commit | a4bd7992cd960b105f842e0e34637f2675a6665f (patch) | |
tree | 65f3afb32aa81429a6c9a4f7a5d7b32e47a75a85 | |
parent | d3a0a27c0caafb066e66e444d6fbe71243bd35f6 (diff) | |
parent | dd9cac69aed9a16f9bcf738e90dcd2c864f07bb5 (diff) | |
download | tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar.gz tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar.bz2 tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar.lz tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar.xz tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.tar.zst tangerine-wallet-browser-a4bd7992cd960b105f842e0e34637f2675a6665f.zip |
Merge branch 'master' into ImportAccountMessageV2
-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-- | package.json | 6 | ||||
-rw-r--r-- | test/unit/edge-encryptor-test.js | 101 | ||||
-rw-r--r-- | ui/app/app.js | 12 | ||||
-rw-r--r-- | ui/app/components/shapeshift-form.js | 9 | ||||
-rw-r--r-- | ui/app/css/itcss/components/header.scss | 7 |
10 files changed, 324 insertions, 100 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 993647300..92efa2481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,16 @@ - Change Loose label to Imported. - Add Imported Account disclaimer. +- 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) +- Add most of Microsoft Edge support. ## 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/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/app.js b/ui/app/app.js index bfa8d8aa7..4e6da24c3 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -90,6 +90,7 @@ function mapStateToProps (state) { isMouseUser: state.appState.isMouseUser, betaUI: state.metamask.featureFlags.betaUI, isRevealingSeedWords: state.metamask.isRevealingSeedWords, + Qr: state.appState.Qr, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -287,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', [ @@ -368,6 +369,7 @@ App.prototype.renderPrimary = function () { isOnboarding, betaUI, isRevealingSeedWords, + Qr, } = props const isMascaraOnboarding = isMascara && isOnboarding const isBetaUIOnboarding = betaUI && isOnboarding && !props.isPopup && !isRevealingSeedWords @@ -508,7 +510,7 @@ App.prototype.renderPrimary = function () { width: '285px', }, }, [ - h(QrView, {key: 'qr'}), + h(QrView, {key: 'qr', Qr}), ]), ]) diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 2270b8236..648b05049 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -51,8 +51,7 @@ ShapeshiftForm.prototype.componentWillMount = function () { this.props.shapeShiftSubview() } -ShapeshiftForm.prototype.onCoinChange = function (e) { - const coin = e.target.value +ShapeshiftForm.prototype.onCoinChange = function (coin) { this.setState({ depositCoin: coin, errorMessage: '', @@ -133,7 +132,7 @@ ShapeshiftForm.prototype.renderMarketInfo = function () { } ShapeshiftForm.prototype.renderQrCode = function () { - const { depositAddress, isLoading } = this.state + const { depositAddress, isLoading, depositCoin } = this.state const qrImage = qrcode(4, 'M') qrImage.addData(depositAddress) qrImage.make() @@ -141,7 +140,7 @@ ShapeshiftForm.prototype.renderQrCode = function () { return h('div.shapeshift-form', {}, [ h('div.shapeshift-form__deposit-instruction', [ - 'Deposit your BTC to the address below:', + `Deposit your ${depositCoin.toUpperCase()} to the address below:`, ]), h('div', depositAddress), @@ -182,7 +181,7 @@ ShapeshiftForm.prototype.render = function () { h(SimpleDropdown, { selectedOption: this.state.depositCoin, - onSelect: this.onCoinChange, + onSelect: (coin) => this.onCoinChange(coin), options: Object.entries(coinOptions).map(([coin]) => ({ value: coin.toLowerCase(), displayValue: coin, 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; |