From 8ab4b432900b46e33048ca0a656a80fb9d1fc789 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 27 Jun 2019 00:30:48 -0400 Subject: [PATCH 1/6] Write and read encrypted settings files for each identity. --- app/js/account/store/account/actions.js | 26 +++++++++++++++-- app/js/account/store/account/reducer.js | 14 ++++++++- app/js/account/store/account/types.js | 1 + app/js/account/utils/index.js | 38 +++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/app/js/account/store/account/actions.js b/app/js/account/store/account/actions.js index 30266fe24..237137773 100644 --- a/app/js/account/store/account/actions.js +++ b/app/js/account/store/account/actions.js @@ -11,7 +11,7 @@ import { } from '@utils' import { isCoreEndpointDisabled } from '@utils/window-utils' import { transactions, config, network } from 'blockstack' - +import { fetchIdentitySettings } from '../../../account/utils' import roundTo from 'round-to' import * as types from './types' import log4js from 'log4js' @@ -547,6 +547,27 @@ function usedIdentityAddress() { } } +function refreshIdentitySettings( + api: { gaiaHubConfig: GaiaHubConfig }, + ownerAddress: string, + identityKeyPair: { key: string } +) { + return dispatch => { + fetchIdentitySettings(api, ownerAddress, identityKeyPair) + .then((settings) => { + dispatch(updateIdentitySettings(identityIndex, settings)) + }) + } +} + +function updateIdentitySettings(identityIndex, settings) { + return { + type: types.UPDATE_IDENTITY_SETTINGS, + identityIndex, + settings + } +} + const AccountActions = { createAccount, updateBackupPhrase, @@ -569,7 +590,8 @@ const AccountActions = { usedIdentityAddress, displayedRecoveryCode, newIdentityAddress, - updateEmail + updateEmail, + refreshIdentitySettings } export default AccountActions diff --git a/app/js/account/store/account/reducer.js b/app/js/account/store/account/reducer.js index 66c2c3910..9a21f4a81 100644 --- a/app/js/account/store/account/reducer.js +++ b/app/js/account/store/account/reducer.js @@ -11,6 +11,8 @@ const initialState = { addresses: [], keypairs: [] }, + identitySettings: { + }, bitcoinAccount: { addresses: [], balances: { total: 0.0 } @@ -43,6 +45,7 @@ function AccountReducer(state = initialState, action) { publicKeychain: action.identityPublicKeychain, addresses: action.identityAddresses, keypairs: action.identityKeypairs, + settings: {}, addressIndex: 0 }, bitcoinAccount: { @@ -258,13 +261,22 @@ function AccountReducer(state = initialState, action) { ...state.identityAccount.addresses, action.keypair.address ], - keypairs: [...state.identityAccount.keypairs, action.keypair] + keypairs: [...state.identityAccount.keypairs, action.keypair], + settings: [...state.identityAccount.settings, {}] }) }) case types.CONNECTED_STORAGE: return Object.assign({}, state, { connectedStorageAtLeastOnce: true }) + case types.UPDATE_IDENTITY_SETTINGS: + return Object.assign({}, state, { + identityAccount: Object.assign({}, state.identityAccount, { + settings: state.identityAccount.settings.map( + (settingsRow, i) => i === action.identityIndex ? action.newSettings : settingsRow + ) + }) + }) default: return state } diff --git a/app/js/account/store/account/types.js b/app/js/account/store/account/types.js index 2b4ecd189..7bd78f178 100644 --- a/app/js/account/store/account/types.js +++ b/app/js/account/store/account/types.js @@ -23,3 +23,4 @@ export const RECOVERY_CODE_VERIFIED = 'account/RECOVERY_CODE_VERIFIED' export const INCREMENT_IDENTITY_ADDRESS_INDEX = 'account/INCREMENT_IDENTITY_ADDRESS_INDEX' export const CONNECTED_STORAGE = 'account/CONNECTED_STORAGE' export const UPDATE_EMAIL_ADDRESS = 'account/UPDATE_EMAIL_ADDRESS' +export const UPDATE_IDENTITY_SETTINGS = 'account/UPDATE_IDENTITY_SETTINGS' diff --git a/app/js/account/utils/index.js b/app/js/account/utils/index.js index 550432406..988e261a9 100644 --- a/app/js/account/utils/index.js +++ b/app/js/account/utils/index.js @@ -4,6 +4,7 @@ import { parseZoneFile } from 'zone-file' import type { GaiaHubConfig } from './blockstack-inc' import { connectToGaiaHub, uploadToGaiaHub } from './blockstack-inc' +import { encryptContent, decryptContent } from 'blockstack' import { getTokenFileUrlFromZoneFile } from '@utils/zone-utils' import log4js from 'log4js' @@ -11,6 +12,7 @@ const logger = log4js.getLogger(__filename) export const BLOCKSTACK_INC = 'gaia-hub' const DEFAULT_PROFILE_FILE_NAME = 'profile.json' +const DEFAULT_IDENTITY_SETTINGS_FILE_NAME = 'settings.json' function getProfileUploadLocation(identity: any, hubConfig: GaiaHubConfig) { if (identity.zoneFile) { @@ -23,6 +25,10 @@ function getProfileUploadLocation(identity: any, hubConfig: GaiaHubConfig) { } } +function getSettingsUploadLocation(hubConfig: GaiaHubConfig) { + return `${hubConfig.url_prefix}${hubConfig.address}/${DEFAULT_IDENTITY_SETTINGS_FILE_NAME}` +} + // aaron-debt: this should be moved into blockstack.js function canWriteUrl(url: string, hubConfig: GaiaHubConfig): ?string { const readPrefix = `${hubConfig.url_prefix}${hubConfig.address}/` @@ -116,3 +122,35 @@ export function uploadProfile( return uploadAttempt }) } + +export function uploadIdentitySettings( + api: { gaiaHubConfig: GaiaHubConfig, gaiaHubUrl: string}, + identityKeyPair: { key: string, keyID: string }, + settingsData: string +) { + const publicKey = identityKeyPair.keyID + const encryptedSettingsData = encryptContent(settingsData, { publicKey }) + return connectToGaiaHub(api.gaiaHubUrl, identityKeyPair.key).then(identityHubConfig => { + const urlToWrite = getSettingsUploadLocation(identityHubConfig) + return tryUpload( + urlToWrite, + encryptedSettingsData, + identityHubConfig, + 'application/json' + ) + }) +} + +export function fetchIdentitySettings( + api: { gaiaHubConfig: GaiaHubConfig }, + ownerAddress: string, + identityKeyPair: { key: string } +) { + const privateKey = identityKeyPair.key + const hubConfig = api.gaiaHubConfig + const url = `${hubConfig.url_prefix}${ownerAddress}/${DEFAULT_IDENTITY_SETTINGS_FILE_NAME}` + return fetch(url) + .then(response => response.text()) + .then(encryptedSettingsData => decryptContent(encryptedSettingsData, { privateKey })) + .then(decryptedSettingsData => JSON.parse(decryptedSettingsData)) +} From 3396a5f535184e25e9a7166b72da2a13780eba91 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 9 Jul 2019 12:47:07 -0400 Subject: [PATCH 2/6] Identity settings initialization. --- app/js/account/store/account/actions.js | 4 +++- app/js/account/store/account/reducer.js | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/js/account/store/account/actions.js b/app/js/account/store/account/actions.js index 237137773..e139ebcf3 100644 --- a/app/js/account/store/account/actions.js +++ b/app/js/account/store/account/actions.js @@ -549,6 +549,7 @@ function usedIdentityAddress() { function refreshIdentitySettings( api: { gaiaHubConfig: GaiaHubConfig }, + identityIndex: int, ownerAddress: string, identityKeyPair: { key: string } ) { @@ -591,7 +592,8 @@ const AccountActions = { displayedRecoveryCode, newIdentityAddress, updateEmail, - refreshIdentitySettings + refreshIdentitySettings, + updateIdentitySettings } export default AccountActions diff --git a/app/js/account/store/account/reducer.js b/app/js/account/store/account/reducer.js index 9a21f4a81..8f4d0accd 100644 --- a/app/js/account/store/account/reducer.js +++ b/app/js/account/store/account/reducer.js @@ -9,9 +9,8 @@ const initialState = { encryptedBackupPhrase: null, // persist identityAccount: { addresses: [], - keypairs: [] - }, - identitySettings: { + keypairs: [], + settings: [] }, bitcoinAccount: { addresses: [], @@ -45,7 +44,7 @@ function AccountReducer(state = initialState, action) { publicKeychain: action.identityPublicKeychain, addresses: action.identityAddresses, keypairs: action.identityKeypairs, - settings: {}, + settings: [{}], addressIndex: 0 }, bitcoinAccount: { @@ -273,7 +272,7 @@ function AccountReducer(state = initialState, action) { return Object.assign({}, state, { identityAccount: Object.assign({}, state.identityAccount, { settings: state.identityAccount.settings.map( - (settingsRow, i) => i === action.identityIndex ? action.newSettings : settingsRow + (settingsRow, i) => i === action.identityIndex ? action.settings : settingsRow ) }) }) From 20fec3eaab1c2976208d8ba78bf6e483286c94c2 Mon Sep 17 00:00:00 2001 From: Ken Date: Wed, 10 Jul 2019 16:45:56 -0400 Subject: [PATCH 3/6] More identity settings initialization. --- app/js/account/store/account/actions.js | 6 ++++-- app/js/account/store/account/reducer.js | 2 +- app/js/utils/account-utils.js | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/js/account/store/account/actions.js b/app/js/account/store/account/actions.js index e139ebcf3..73a1b536b 100644 --- a/app/js/account/store/account/actions.js +++ b/app/js/account/store/account/actions.js @@ -41,7 +41,8 @@ function createAccount( bitcoinPublicKeychain, firstBitcoinAddress, identityAddresses, - identityKeypairs + identityKeypairs, + identitySettings } = getBlockchainIdentities(masterKeychain, identitiesToGenerate) return { @@ -51,7 +52,8 @@ function createAccount( bitcoinPublicKeychain, firstBitcoinAddress, identityAddresses, - identityKeypairs + identityKeypairs, + identitySettings } } diff --git a/app/js/account/store/account/reducer.js b/app/js/account/store/account/reducer.js index 8f4d0accd..26150a1d5 100644 --- a/app/js/account/store/account/reducer.js +++ b/app/js/account/store/account/reducer.js @@ -44,7 +44,7 @@ function AccountReducer(state = initialState, action) { publicKeychain: action.identityPublicKeychain, addresses: action.identityAddresses, keypairs: action.identityKeypairs, - settings: [{}], + settings: action.identitySettings, addressIndex: 0 }, bitcoinAccount: { diff --git a/app/js/utils/account-utils.js b/app/js/utils/account-utils.js index fc238ff9a..c7e2025fe 100644 --- a/app/js/utils/account-utils.js +++ b/app/js/utils/account-utils.js @@ -440,6 +440,7 @@ export function getBlockchainIdentities(masterKeychain, identitiesToGenerate) { const identityAddresses = [] const identityKeypairs = [] + const identitySettings = [] // We pre-generate a number of identity addresses so that we // don't have to prompt the user for the password on each new profile @@ -455,6 +456,7 @@ export function getBlockchainIdentities(masterKeychain, identitiesToGenerate) { const identityKeyPair = deriveIdentityKeyPair(identityOwnerAddressNode) identityKeypairs.push(identityKeyPair) identityAddresses.push(identityKeyPair.address) + identitySettings.push({}) logger.debug(`createAccount: identity index: ${addressIndex}`) } @@ -463,6 +465,7 @@ export function getBlockchainIdentities(masterKeychain, identitiesToGenerate) { bitcoinPublicKeychain, firstBitcoinAddress, identityAddresses, - identityKeypairs + identityKeypairs, + identitySettings } } From b0cdbf9b868231110c00c2d35f924792ad00cf2d Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 11 Jul 2019 10:43:15 -0400 Subject: [PATCH 4/6] Identity settings refresh on profile page. --- app/js/account/store/account/actions.js | 45 +++++++++++++++++++++++-- app/js/account/store/account/reducer.js | 14 +++++--- app/js/account/store/account/types.js | 1 + app/js/account/utils/index.js | 14 ++++++-- app/js/profiles/DefaultProfilePage.js | 7 ++++ 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/app/js/account/store/account/actions.js b/app/js/account/store/account/actions.js index 73a1b536b..85e8585bf 100644 --- a/app/js/account/store/account/actions.js +++ b/app/js/account/store/account/actions.js @@ -549,17 +549,54 @@ function usedIdentityAddress() { } } +function refreshAllIdentitySettings( + api: { gaiaHubConfig: GaiaHubConfig }, + ownerAddresses: Array, + identityKeyPairs: Array +) { + return dispatch => { + const promises: Array> = ownerAddresses.map((address, index) => { + const promise: Promise<*> = new Promise((resolve, reject) => { + const keyPair = identityKeyPairs[index] + return fetchIdentitySettings(api, address, keyPair) + .then((settings) => { + resolve(settings) + }) + .catch(error => reject(error)) + }) + return promise + }) + + return Promise.all(promises) + .then(settings => { + return dispatch(updateAllIdentitySettings(settings)) + }) + .catch((error) => { + logger.error( + 'refreshIdentitySettings: error refreshing identity settings', + error + ) + return Promise.reject(error) + }) + } +} + function refreshIdentitySettings( api: { gaiaHubConfig: GaiaHubConfig }, identityIndex: int, ownerAddress: string, identityKeyPair: { key: string } ) { - return dispatch => { - fetchIdentitySettings(api, ownerAddress, identityKeyPair) + return dispatch => fetchIdentitySettings(api, ownerAddress, identityKeyPair) .then((settings) => { - dispatch(updateIdentitySettings(identityIndex, settings)) + return dispatch(updateIdentitySettings(identityIndex, settings)) }) +} + +function updateAllIdentitySettings(settings) { + return { + type: types.UPDATE_ALL_IDENTITY_SETTINGS, + settings } } @@ -594,7 +631,9 @@ const AccountActions = { displayedRecoveryCode, newIdentityAddress, updateEmail, + refreshAllIdentitySettings, refreshIdentitySettings, + updateAllIdentitySettings, updateIdentitySettings } diff --git a/app/js/account/store/account/reducer.js b/app/js/account/store/account/reducer.js index 26150a1d5..6f343f9a1 100644 --- a/app/js/account/store/account/reducer.js +++ b/app/js/account/store/account/reducer.js @@ -268,14 +268,20 @@ function AccountReducer(state = initialState, action) { return Object.assign({}, state, { connectedStorageAtLeastOnce: true }) - case types.UPDATE_IDENTITY_SETTINGS: + case types.UPDATE_ALL_IDENTITY_SETTINGS: return Object.assign({}, state, { identityAccount: Object.assign({}, state.identityAccount, { - settings: state.identityAccount.settings.map( - (settingsRow, i) => i === action.identityIndex ? action.settings : settingsRow - ) + settings: action.settings }) }) + case types.UPDATE_IDENTITY_SETTINGS: + return Object.assign({}, state, { + identityAccount: Object.assign({}, state.identityAccount, { + settings: state.identityAccount.settings.map( + (settingsRow, i) => i === action.identityIndex ? action.settings : settingsRow + ) + }) + }) default: return state } diff --git a/app/js/account/store/account/types.js b/app/js/account/store/account/types.js index 7bd78f178..cc25721e5 100644 --- a/app/js/account/store/account/types.js +++ b/app/js/account/store/account/types.js @@ -23,4 +23,5 @@ export const RECOVERY_CODE_VERIFIED = 'account/RECOVERY_CODE_VERIFIED' export const INCREMENT_IDENTITY_ADDRESS_INDEX = 'account/INCREMENT_IDENTITY_ADDRESS_INDEX' export const CONNECTED_STORAGE = 'account/CONNECTED_STORAGE' export const UPDATE_EMAIL_ADDRESS = 'account/UPDATE_EMAIL_ADDRESS' +export const UPDATE_ALL_IDENTITY_SETTINGS = 'account/UPDATE_ALL_IDENTITY_SETTINGS' export const UPDATE_IDENTITY_SETTINGS = 'account/UPDATE_IDENTITY_SETTINGS' diff --git a/app/js/account/utils/index.js b/app/js/account/utils/index.js index 988e261a9..c59290bae 100644 --- a/app/js/account/utils/index.js +++ b/app/js/account/utils/index.js @@ -150,7 +150,15 @@ export function fetchIdentitySettings( const hubConfig = api.gaiaHubConfig const url = `${hubConfig.url_prefix}${ownerAddress}/${DEFAULT_IDENTITY_SETTINGS_FILE_NAME}` return fetch(url) - .then(response => response.text()) - .then(encryptedSettingsData => decryptContent(encryptedSettingsData, { privateKey })) - .then(decryptedSettingsData => JSON.parse(decryptedSettingsData)) + .then(response => { + if (response.ok) { + return response.text() + .then(encryptedSettingsData => decryptContent(encryptedSettingsData, { privateKey })) + .then(decryptedSettingsData => JSON.parse(decryptedSettingsData)) + } else if (response.status == 404) { + return {} + } else { + return Promise.reject('Could not fetch identity settings') + } + }) } diff --git a/app/js/profiles/DefaultProfilePage.js b/app/js/profiles/DefaultProfilePage.js index e7eb105bc..a516b8cf3 100644 --- a/app/js/profiles/DefaultProfilePage.js +++ b/app/js/profiles/DefaultProfilePage.js @@ -112,6 +112,13 @@ export class DefaultProfilePage extends Component { componentWillMount() { logger.info('componentWillMount') this.props.refreshIdentities(this.props.api, this.props.identityAddresses) + .then(() => { + this.props.refreshAllIdentitySettings( + this.props.api, + this.props.identityAddresses, + this.props.identityKeypairs + ) + }) } componentWillReceiveProps(nextProps) { From ce1c4a83c8d8e50ca24d37d3dc5f367b1b1fcfa0 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 11 Jul 2019 16:55:56 -0400 Subject: [PATCH 5/6] Fix tests for DefaultProfilePage --- app/js/profiles/DefaultProfilePage.js | 17 ++++++++--------- test/profiles/DefaultProfilePage.test.js | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/js/profiles/DefaultProfilePage.js b/app/js/profiles/DefaultProfilePage.js index a516b8cf3..c6267c061 100644 --- a/app/js/profiles/DefaultProfilePage.js +++ b/app/js/profiles/DefaultProfilePage.js @@ -7,7 +7,7 @@ import { Person } from 'blockstack' import Modal from 'react-modal' import Image from '@components/Image' import { IdentityActions } from './store/identity' -import { AccountActions } from '../account/store/account' +import AccountActions from '../account/store/account' import SecondaryNavBar from '@components/SecondaryNavBar' import SocialAccountItem from './components/SocialAccountItem' import PGPAccountItem from './components/PGPAccountItem' @@ -66,7 +66,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return bindActionCreators( - Object.assign({}, IdentityActions, AccountActions), + Object.assign({}, AccountActions, IdentityActions), dispatch ) } @@ -79,6 +79,7 @@ export class DefaultProfilePage extends Component { createNewProfile: PropTypes.func.isRequired, updateProfile: PropTypes.func.isRequired, refreshIdentities: PropTypes.func.isRequired, + refreshAllIdentitySettings: PropTypes.func.isRequired, refreshSocialProofVerifications: PropTypes.func.isRequired, api: PropTypes.object.isRequired, identityAddresses: PropTypes.array.isRequired, @@ -112,13 +113,11 @@ export class DefaultProfilePage extends Component { componentWillMount() { logger.info('componentWillMount') this.props.refreshIdentities(this.props.api, this.props.identityAddresses) - .then(() => { - this.props.refreshAllIdentitySettings( - this.props.api, - this.props.identityAddresses, - this.props.identityKeypairs - ) - }) + this.props.refreshAllIdentitySettings( + this.props.api, + this.props.identityAddresses, + this.props.identityKeypairs + ) } componentWillReceiveProps(nextProps) { diff --git a/test/profiles/DefaultProfilePage.test.js b/test/profiles/DefaultProfilePage.test.js index ba657c8bd..074a8753b 100644 --- a/test/profiles/DefaultProfilePage.test.js +++ b/test/profiles/DefaultProfilePage.test.js @@ -26,7 +26,8 @@ function setup(accounts = []) { encryptedBackupPhrase: 'onwards and upwards', setDefaultIdentity: () => {}, identityKeypairs: [], - storageConnected: false + storageConnected: false, + refreshAllIdentitySettings: () => {} } const wrapper = shallow() From 8a8b14b3d173b785ecf5326e268c1c70b26a5aeb Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 11 Jul 2019 17:56:27 -0400 Subject: [PATCH 6/6] Fix import typo. --- app/js/profiles/DefaultProfilePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/js/profiles/DefaultProfilePage.js b/app/js/profiles/DefaultProfilePage.js index c6267c061..d29a4b697 100644 --- a/app/js/profiles/DefaultProfilePage.js +++ b/app/js/profiles/DefaultProfilePage.js @@ -7,7 +7,7 @@ import { Person } from 'blockstack' import Modal from 'react-modal' import Image from '@components/Image' import { IdentityActions } from './store/identity' -import AccountActions from '../account/store/account' +import { AccountActions } from '../account/store/account' import SecondaryNavBar from '@components/SecondaryNavBar' import SocialAccountItem from './components/SocialAccountItem' import PGPAccountItem from './components/PGPAccountItem'