Skip to content

Commit

Permalink
feat(modern auth): merged changes from secure-channel-login
Browse files Browse the repository at this point in the history
  • Loading branch information
milan-bc committed Sep 24, 2020
2 parents 8e2d026 + 12f6d1f commit c42cc4a
Show file tree
Hide file tree
Showing 21 changed files with 132 additions and 44 deletions.
8 changes: 8 additions & 0 deletions packages/blockchain-info-components/src/Images/Images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import introSendGif from './img/intro-send.gif'
import introSwap from './img/intro-swap.png'
import introSwapGif from './img/intro-swap.gif'
import linkedinWhite from './img/linkedin-white.svg'
import logo144 from './img/logo-144.png'
import logo192 from './img/logo-192.png'
import logo512 from './img/logo-512.png'
import logo96 from './img/logo-96.png'
import logoLoader from './img/logo-loader.gif'
import mastercardLogo from './img/mastercard-logo.svg'
import microDepositsWhole from './img/micro-deposits-whole.svg'
Expand Down Expand Up @@ -178,6 +182,10 @@ const Images = {
'ledger-logo3': ledgerLogo3,
'ledger-nano-s': deviceNanoS,
'linkedin-white': linkedinWhite,
'logo-96': logo96,
'logo-144': logo144,
'logo-192': logo192,
'logo-512': logo512,
'lockbox-device': deviceLockbox,
'lockbox-failed': lockboxFailed,
'lockbox-failed2': lockboxFailed2,
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Banner, BlockchainLoader } from 'blockchain-info-components'
import { checkHasWebcam } from 'utils/helpers'
import { FormattedMessage } from 'react-intl'
import PropTypes from 'prop-types'
import QrReader from 'react-qr-reader'
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
Expand All @@ -22,7 +21,7 @@ const Wrapper = styled.div`
margin: 20px;
`

const QRReader = props => {
const QRReader = (props: Props) => {
const { onScan, onError } = props
const [hasWebcam, setHasWebcam] = useState(false)
const [isLoading, setIsLoading] = useState(true)
Expand Down Expand Up @@ -52,9 +51,9 @@ const QRReader = props => {
)
}

QRReader.propTypes = {
onScan: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired
type Props = {
onError: () => void
onScan: () => void
}

export default QRReader
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGOUT = 'LOGOUT'
export const LOGOUT_CLEAR_REDUX_STORE = 'LOGOUT_CLEAR_REDUX_STORE'
export const MOBILE_LOGIN = 'MOBILE_LOGIN'
export const MOBILE_LOGIN_START = 'MOBILE_LOGIN_START'
export const MOBILE_LOGIN_FINISH = 'MOBILE_LOGIN_FINISH'
export const REGISTER = 'REGISTER'
export const REGISTER_FAILURE = 'REGISTER_FAILURE'
export const REGISTER_LOADING = 'REGISTER_LOADING'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const mobileLogin = data => ({
type: AT.MOBILE_LOGIN,
payload: { data }
})
export const mobileLoginStarted = () => ({ type: AT.MOBILE_LOGIN_START })
export const mobileLoginFinish = () => ({ type: AT.MOBILE_LOGIN_FINISH })

export const register = (email, password, language) => ({
type: AT.REGISTER,
payload: { email, password, language }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const INITIAL_STATE = {
isLoggingIn: false,
isAuthenticated: false,
firstLogin: false,
mobileLoginStarted: false,
login: Remote.NotAsked,
reset_2fa: Remote.NotAsked,
restoring: Remote.NotAsked,
Expand Down Expand Up @@ -34,6 +35,12 @@ const auth = (state = INITIAL_STATE, action) => {
case AT.AUTHENTICATE: {
return assoc('isAuthenticated', true, state)
}
case AT.MOBILE_LOGIN_START: {
return assoc('mobileLoginStarted', true, state)
}
case AT.MOBILE_LOGIN_FINISH: {
return assoc('mobileLoginStarted', false, state)
}
case AT.REGISTER_LOADING: {
return assoc('registering', Remote.Loading, state)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/blockchain-wallet-v4-frontend/src/data/auth/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export default ({ api, coreSagas }) => {
}
const mobileLogin = function * (action) {
try {
yield put(actions.auth.mobileLoginStarted())
const { guid, sharedKey, password } = yield call(
coreSagas.settings.decodePairingCode,
action.payload
Expand All @@ -342,6 +343,7 @@ export default ({ api, coreSagas }) => {
true
)
yield call(login, loginAction)
yield put(actions.auth.mobileLoginFinish())
} catch (error) {
yield put(actions.logs.logErrorMessage(logLocation, 'mobileLogin', error))
if (error === 'qr_code_expired') {
Expand All @@ -351,6 +353,7 @@ export default ({ api, coreSagas }) => {
} else {
yield put(actions.alerts.displayError(C.MOBILE_LOGIN_ERROR))
}
yield put(actions.auth.mobileLoginFinish())
}
}
const register = function * (action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export const getAuthType = path(['auth', 'auth_type'])
export const getReset2fa = path(['auth', 'reset_2fa'])
export const getSecureChannelLogin = path(['auth', 'secureChannelLogin'])
export const getLogin = path(['auth', 'login'])
export const getMobileLoginStarted = path(['auth', 'mobileLoginStarted'])
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export const ANNOUNCEMENT_DISMISSED = '@EVENT.ANNOUNCEMENT_DISMISSED'
export const ANNOUNCEMENT_TOGGLED = '@EVENT.ANNOUNCEMENT_TOGGLED'
export const GUID_ENTERED = '@EVENT.GUID_ENTERED'
export const CHANNEL_PRIV_KEY_CREATED = '@EVENT.CHANNEL_PRIV_KEY_CREATED'
export const CHANNEL_RUID_CREATED = '@EVENT.CHANNEL_RUID_CREATED'
export const CHANNEL_CHANNEL_ID_CREATED = '@EVENT.CHANNEL_CHANNEL_ID_CREATED'
export const CHANNEL_PHONE_CONNECTED = '@EVENT.CHANNEL_PHONE_CONNECTED'
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export const channelPrivKeyCreated = privKey => ({
type: AT.CHANNEL_PRIV_KEY_CREATED,
payload: { privKey }
})
export const channelRuidCreated = ruid => ({
type: AT.CHANNEL_RUID_CREATED,
payload: { ruid }
export const channelChannelIdCreated = channelId => ({
type: AT.CHANNEL_CHANNEL_ID_CREATED,
payload: { channelId }
})
export const channelPhoneConnected = phonePubkey => ({
type: AT.CHANNEL_PHONE_CONNECTED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const cache = (state = INITIAL_STATE, action) => {
const { privKey } = payload
return assoc('channelPrivKey', privKey, state)
}
case AT.CHANNEL_RUID_CREATED: {
const { ruid } = payload
return assoc('channelRuid', ruid, state)
case AT.CHANNEL_CHANNEL_ID_CREATED: {
const { channelId } = payload
return assoc('channelChannelId', channelId, state)
}
case AT.CHANNEL_PHONE_CONNECTED: {
const { phonePubkey } = payload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const getLastAnnouncementState = state =>
export const getLastGuid = state => path(['cache', 'lastGuid'], state)
export const getChannelPrivKey = state =>
path(['cache', 'channelPrivKey'], state)
export const getChannelRuid = state => path(['cache', 'channelRuid'], state)
export const getChannelChannelId = state =>
path(['cache', 'channelChannelId'], state)
export const getPhonePubkey = state =>
path(['cache', 'channelPhonePubkey'], state)
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ function uuidv4 () {
export default ({ api, socket }) => {
const send = socket.send.bind(socket)

const pingPhone = function * (ruid, secretHex, phonePubkey, guid) {
const pingPhone = function * (channelId, secretHex, phonePubkey, guid) {
let currentState = yield select(selectors.auth.getSecureChannelLogin)
if (currentState !== Remote.NotAsked) {
return
}
let msg = {
type: 'login_wallet',
ruid: ruid,
channelId: channelId,
timestamp: Date.now()
}

Expand All @@ -56,30 +56,30 @@ export default ({ api, socket }) => {

const onOpen = function * () {
let secretHex = yield select(selectors.cache.getChannelPrivKey)
let ruid = yield select(selectors.cache.getChannelRuid)
let channelId = yield select(selectors.cache.getChannelChannelId)

if (!secretHex || !ruid) {
if (!secretHex || !channelId) {
secretHex = crypto.randomBytes(32).toString('hex')
yield put(actions.cache.channelPrivKeyCreated(secretHex))

ruid = uuidv4()
yield put(actions.cache.channelRuidCreated(ruid))
channelId = uuidv4()
yield put(actions.cache.channelChannelIdCreated(channelId))
}

yield call(
send,
JSON.stringify({
command: 'subscribe',
entity: 'secure_channel',
param: { ruid: ruid }
param: { channelId: channelId }
})
)

// Also, if we already know a phone, let's ping it to give us it's secrets
let phonePubkey = yield select(selectors.cache.getPhonePubkey)
let guid = yield select(selectors.cache.getLastGuid)
if (phonePubkey && guid) {
yield pingPhone(ruid, secretHex, phonePubkey, guid)
yield pingPhone(channelId, secretHex, phonePubkey, guid)
}
}

Expand Down Expand Up @@ -255,7 +255,7 @@ export default ({ api, socket }) => {
payload = JSON.parse(message.msg)
} catch (e) {}

if (payload.ruid) {
if (payload.channelId) {
if (!payload.success) {
// TODO should this be a new action to delete, or is this fine?
yield put(actions.cache.channelPhoneConnected(undefined))
Expand All @@ -279,8 +279,13 @@ export default ({ api, socket }) => {
let decrypted = JSON.parse(decryptedRaw.toString('utf8'))

if (decrypted.type === 'handshake') {
let ruid = yield select(selectors.cache.getChannelRuid)
yield pingPhone(ruid, secretHex, payload.pubkey, decrypted.guid)
let channelId = yield select(selectors.cache.getChannelChannelId)
yield pingPhone(
channelId,
secretHex,
payload.pubkey,
decrypted.guid
)
} else if (decrypted.type === 'login_wallet') {
if (decrypted.remember) {
yield put(
Expand Down
1 change: 1 addition & 0 deletions packages/blockchain-wallet-v4-frontend/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="description" content="Discover the world's most popular bitcoin wallet. Visit today to create your free simple, secure and safe Blockchain Wallet.">
<meta name="keywords" content="bitcoin wallet, blockchain wallet, online bitcoin wallet, bitcoin wallet online">
<title>Blockchain.com Wallet - Exchange Cryptocurrency</title>
<link rel="manifest" href="/manifest.webmanifest" />
<script nonce="**CSP_NONCE**">
window.NONCE = '**CSP_NONCE**'
</script>
Expand Down
38 changes: 38 additions & 0 deletions packages/blockchain-wallet-v4-frontend/src/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "Blockchain Wallet",
"short_name": "Blockchain",
"description": "Blockchain Crypto Wallet",
"icons": [
{
"src": "/img/logo-144.png",
"sizes": "144x144",
"type": "image/x-icon",
"purpose": "any maskable"
},
{
"src": "/img/logo-192.png",
"sizes": "192x192",
"type": "image/x-icon",
"purpose": "any maskable"
},
{
"src": "/img/logo-512.png",
"sizes": "512x512",
"type": "image/x-icon",
"purpose": "any maskable"
}
],
"start_url": "/?source=pwa",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"prefer_related_applications": true,
"related_applications": [
{
"platform": "play",
"id": "piuk.blockchain.android",
"url": "https://play.google.com/store/apps/details?id=piuk.blockchain.android"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const mapStateToProps = state => ({
qr_data: selectors.cache.getChannelPrivKey(state)
? JSON.stringify({
type: 'login_wallet',
ruid: selectors.cache.getChannelRuid(state),
channelId: selectors.cache.getChannelChannelId(state),
pubkey: wCrypto
.derivePubFromPriv(
Buffer.from(selectors.cache.getChannelPrivKey(state), 'hex')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
import * as C from 'services/AlertService'
import { actions } from 'data'
import { actions, selectors } from 'data'
import { bindActionCreators } from 'redux'
import { connect, ConnectedProps } from 'react-redux'
import { isEmpty, isNil } from 'ramda'
import MobileLogin from './template'
import React from 'react'

class MobileLoginContainer extends React.PureComponent<Props> {
handleScan = result => {
const MobileLoginContainer = (props: Props) => {
const handleScan = result => {
if (!isNil(result) && !isEmpty(result)) {
this.props.authActions.mobileLogin(result)
props.authActions.mobileLogin(result)
}
}

handleError = error => {
const handleError = error => {
if (isNil(error) && isEmpty(error)) {
this.props.alertsActions.displayError(C.MOBILE_LOGIN_SCAN_ERROR)
props.alertsActions.displayError(C.MOBILE_LOGIN_SCAN_ERROR)
}
}

render () {
return (
<MobileLogin
{...this.props}
handleScan={this.handleScan}
handleError={this.handleError}
/>
)
}
return (
<MobileLogin
{...props}
handleScan={handleScan}
handleError={handleError}
isScanning={props.logginStarted}
/>
)
}

const mapStateToProps = state => ({
logginStarted: selectors.auth.getMobileLoginStarted(state)
})

const mapDispatchToProps = dispatch => ({
alertsActions: bindActionCreators(actions.alerts, dispatch),
authActions: bindActionCreators(actions.auth, dispatch),
modalActions: bindActionCreators(actions.modals, dispatch)
})

const connector = connect(undefined, mapDispatchToProps)
const connector = connect(mapStateToProps, mapDispatchToProps)

type Props = ConnectedProps<typeof connector>

Expand Down

0 comments on commit c42cc4a

Please sign in to comment.