1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Merge branch 'master' into ImportAccountMessageV2

This commit is contained in:
Dan Finlay 2018-03-06 10:32:47 -08:00 committed by GitHub
commit a4bd7992cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 327 additions and 103 deletions

View File

@ -4,11 +4,16 @@
- Change Loose label to Imported. - Change Loose label to Imported.
- Add Imported Account disclaimer. - 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 ## 4.1.3 2018-2-28
- Ensure MetaMask's inpage provider is named MetamaskInpageProvider to keep some sites from breaking. - Ensure MetaMask's inpage provider is named MetamaskInpageProvider to keep some sites from breaking.
- Add retry transaction button back into classic ui. - Add retry transaction button back into classic ui.
- Add network dropdown styles to support long custom RPC urls
## 4.1.2 2018-2-28 ## 4.1.2 2018-2-28

View File

@ -16,6 +16,7 @@ const firstTimeState = require('./first-time-state')
const setupRaven = require('./lib/setupRaven') const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
@ -32,6 +33,12 @@ global.METAMASK_NOTIFIER = notificationManager
const release = platform.getVersion() const release = platform.getVersion()
const raven = setupRaven({ release }) 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 popupIsOpen = false
let openMetamaskTabsIDs = {} let openMetamaskTabsIDs = {}
@ -81,6 +88,7 @@ function setupController (initState) {
initState, initState,
// platform specific api // platform specific api
platform, platform,
encryptor: isEdge ? new EdgeEncryptor() : undefined,
}) })
global.metamaskController = controller global.metamaskController = controller

View File

@ -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

View File

@ -1,13 +1,12 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import LoadingScreen from './loading-screen' import classnames from 'classnames'
import { import {
createNewVaultAndRestore, createNewVaultAndRestore,
hideWarning, hideWarning,
displayWarning, displayWarning,
unMarkPasswordForgotten, unMarkPasswordForgotten,
clearNotices,
} from '../../../../ui/app/actions' } from '../../../../ui/app/actions'
class ImportSeedPhraseScreen extends Component { class ImportSeedPhraseScreen extends Component {
@ -17,8 +16,8 @@ class ImportSeedPhraseScreen extends Component {
next: PropTypes.func.isRequired, next: PropTypes.func.isRequired,
createNewVaultAndRestore: PropTypes.func.isRequired, createNewVaultAndRestore: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired, hideWarning: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
displayWarning: PropTypes.func, displayWarning: PropTypes.func,
leaveImportSeedScreenState: PropTypes.func,
}; };
state = { state = {
@ -27,98 +26,130 @@ class ImportSeedPhraseScreen extends Component {
confirmPassword: '', confirmPassword: '',
} }
parseSeedPhrase = (seedPhrase) => {
return seedPhrase
.match(/\w+/g)
.join(' ')
}
onChange = ({ seedPhrase, password, confirmPassword }) => {
const {
password: prevPassword,
confirmPassword: prevConfirmPassword,
} = this.state
const { displayWarning, hideWarning } = this.props
let warning = null
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 (warning) {
displayWarning(warning)
} else {
hideWarning()
}
seedPhrase && this.setState({ seedPhrase })
password && this.setState({ password })
confirmPassword && this.setState({ confirmPassword })
}
onClick = () => { onClick = () => {
const { password, seedPhrase, confirmPassword } = this.state const { password, seedPhrase } = this.state
const { createNewVaultAndRestore, next, displayWarning, leaveImportSeedScreenState } = this.props const {
createNewVaultAndRestore,
next,
displayWarning,
leaveImportSeedScreenState,
} = this.props
if (seedPhrase.split(' ').length !== 12) {
this.warning = 'Seed Phrases are 12 words long'
displayWarning(this.warning)
return
}
if (password.length < 8) {
this.warning = 'Passwords require a mimimum length of 8'
displayWarning(this.warning)
return
}
if (password !== confirmPassword) {
this.warning = 'Confirmed password does not match'
displayWarning(this.warning)
return
}
this.warning = null
leaveImportSeedScreenState() leaveImportSeedScreenState()
createNewVaultAndRestore(password, seedPhrase) createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
.then(next) .then(next)
} }
render () { render () {
return this.props.isLoading const { seedPhrase, password, confirmPassword } = this.state
? <LoadingScreen loadingMessage="Creating your new account" /> const { warning } = this.props
: ( const importDisabled = warning || !seedPhrase || !password || !confirmPassword
<div className="import-account"> return (
<a <div className="import-account">
className="import-account__back-button" <a
onClick={e => { className="import-account__back-button"
e.preventDefault() onClick={e => {
this.props.back() e.preventDefault()
}} this.props.back()
href="#" }}
> href="#"
{`< Back`} >
</a> {`< Back`}
<div className="import-account__title"> </a>
Import an Account with Seed Phrase <div className="import-account__title">
</div> Import an Account with Seed Phrase
<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>
</div> </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( export default connect(
({ appState: { isLoading, warning } }) => ({ isLoading, warning }), ({ appState: { warning } }) => ({ warning }),
dispatch => ({ dispatch => ({
leaveImportSeedScreenState: () => { leaveImportSeedScreenState: () => {
dispatch(unMarkPasswordForgotten()) dispatch(unMarkPasswordForgotten())

View File

@ -1,11 +1,11 @@
.first-time-flow { .first-time-flow {
height: 100vh;
width: 100vw; width: 100vw;
background-color: #FFF; background-color: #fff;
overflow: auto; overflow: auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
flex: 1 0 auto;
} }
.alpha-warning { .alpha-warning {
@ -45,12 +45,12 @@
.buy-ether { .buy-ether {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
margin: 67px 0 50px 146px; margin: 67px 0 50px;
max-width: 35rem; max-width: 35rem;
} }
.create-password { .create-password {
margin: 67px 0 50px 0px; margin: 67px 0 50px;
} }
.import-account { .import-account {
@ -418,6 +418,10 @@ button.backup-phrase__confirm-seed-option:hover {
line-height: 23px; line-height: 23px;
} }
.import-account__input-label__disabled {
opacity: 0.5;
}
.import-account__input { .import-account__input {
width: 325px !important; width: 325px !important;
} }
@ -564,6 +568,10 @@ button.backup-phrase__confirm-seed-option:hover {
background-color: #FFFFFF; background-color: #FFFFFF;
} }
.first-time-flow__input__disabled {
opacity: 0.5;
}
.first-time-flow__input::placeholder { .first-time-flow__input::placeholder {
color: #9B9B9B; color: #9B9B9B;
font-weight: 200; font-weight: 200;

View File

@ -54,6 +54,7 @@
}, },
"dependencies": { "dependencies": {
"abi-decoder": "^1.0.9", "abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0", "async": "^2.5.0",
"await-semaphore": "^0.1.1", "await-semaphore": "^0.1.1",
"babel-runtime": "^6.23.0", "babel-runtime": "^6.23.0",
@ -64,6 +65,7 @@
"boron": "^0.2.3", "boron": "^0.2.3",
"browser-passworder": "^2.0.3", "browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4", "browserify-derequire": "^0.9.4",
"browserify-unibabel": "^3.0.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"client-sw-ready-event": "^3.3.0", "client-sw-ready-event": "^3.3.0",
"clone": "^2.1.1", "clone": "^2.1.1",
@ -78,11 +80,11 @@
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.3.0", "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-filters": "^1.2.5",
"eth-json-rpc-infura": "^3.0.0", "eth-json-rpc-infura": "^3.0.0",
"eth-keyring-controller": "^2.1.4", "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-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2", "eth-sig-util": "^1.4.2",

View File

@ -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)
})
})
})
})

View File

@ -90,6 +90,7 @@ function mapStateToProps (state) {
isMouseUser: state.appState.isMouseUser, isMouseUser: state.appState.isMouseUser,
betaUI: state.metamask.featureFlags.betaUI, betaUI: state.metamask.featureFlags.betaUI,
isRevealingSeedWords: state.metamask.isRevealingSeedWords, isRevealingSeedWords: state.metamask.isRevealingSeedWords,
Qr: state.appState.Qr,
// state needed to get account dropdown temporarily rendering from app bar // state needed to get account dropdown temporarily rendering from app bar
identities, identities,
@ -287,10 +288,10 @@ App.prototype.renderAppBar = function () {
}), }),
// metamask name // metamask name
h('h1', 'MetaMask'), h('.flex-row', [
h('h1', 'MetaMask'),
h('div.beta-label', 'BETA'), h('div.beta-label', 'BETA'),
]),
]), ]),
h('div.header__right-actions', [ h('div.header__right-actions', [
@ -368,6 +369,7 @@ App.prototype.renderPrimary = function () {
isOnboarding, isOnboarding,
betaUI, betaUI,
isRevealingSeedWords, isRevealingSeedWords,
Qr,
} = props } = props
const isMascaraOnboarding = isMascara && isOnboarding const isMascaraOnboarding = isMascara && isOnboarding
const isBetaUIOnboarding = betaUI && isOnboarding && !props.isPopup && !isRevealingSeedWords const isBetaUIOnboarding = betaUI && isOnboarding && !props.isPopup && !isRevealingSeedWords
@ -508,7 +510,7 @@ App.prototype.renderPrimary = function () {
width: '285px', width: '285px',
}, },
}, [ }, [
h(QrView, {key: 'qr'}), h(QrView, {key: 'qr', Qr}),
]), ]),
]) ])

View File

@ -51,8 +51,7 @@ ShapeshiftForm.prototype.componentWillMount = function () {
this.props.shapeShiftSubview() this.props.shapeShiftSubview()
} }
ShapeshiftForm.prototype.onCoinChange = function (e) { ShapeshiftForm.prototype.onCoinChange = function (coin) {
const coin = e.target.value
this.setState({ this.setState({
depositCoin: coin, depositCoin: coin,
errorMessage: '', errorMessage: '',
@ -133,7 +132,7 @@ ShapeshiftForm.prototype.renderMarketInfo = function () {
} }
ShapeshiftForm.prototype.renderQrCode = function () { ShapeshiftForm.prototype.renderQrCode = function () {
const { depositAddress, isLoading } = this.state const { depositAddress, isLoading, depositCoin } = this.state
const qrImage = qrcode(4, 'M') const qrImage = qrcode(4, 'M')
qrImage.addData(depositAddress) qrImage.addData(depositAddress)
qrImage.make() qrImage.make()
@ -141,7 +140,7 @@ ShapeshiftForm.prototype.renderQrCode = function () {
return h('div.shapeshift-form', {}, [ return h('div.shapeshift-form', {}, [
h('div.shapeshift-form__deposit-instruction', [ h('div.shapeshift-form__deposit-instruction', [
'Deposit your BTC to the address below:', `Deposit your ${depositCoin.toUpperCase()} to the address below:`,
]), ]),
h('div', depositAddress), h('div', depositAddress),
@ -182,7 +181,7 @@ ShapeshiftForm.prototype.render = function () {
h(SimpleDropdown, { h(SimpleDropdown, {
selectedOption: this.state.depositCoin, selectedOption: this.state.depositCoin,
onSelect: this.onCoinChange, onSelect: (coin) => this.onCoinChange(coin),
options: Object.entries(coinOptions).map(([coin]) => ({ options: Object.entries(coinOptions).map(([coin]) => ({
value: coin.toLowerCase(), value: coin.toLowerCase(),
displayValue: coin, displayValue: coin,

View File

@ -80,11 +80,10 @@
font-family: Roboto; font-family: Roboto;
text-transform: uppercase; text-transform: uppercase;
font-weight: 500; font-weight: 500;
font-size: 0.8rem; font-size: .8rem;
padding-left: 9px;
color: $buttercup; color: $buttercup;
align-self: flex-start; margin-left: 5px;
margin-top: 10px; line-height: initial;
@media screen and (max-width: 575px) { @media screen and (max-width: 575px) {
display: none; display: none;