From 28c16ccae561236a10d2dbf7d20cad38745f2bb9 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 28 May 2019 14:11:09 +0100 Subject: [PATCH] Journal members: implement giving access --- src/Journals/JournalMemberAddDialog.tsx | 132 ++++++++++++++++++++++++ src/Journals/JournalMembers.tsx | 84 ++++++++++++--- src/Journals/index.tsx | 3 +- 3 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 src/Journals/JournalMemberAddDialog.tsx diff --git a/src/Journals/JournalMemberAddDialog.tsx b/src/Journals/JournalMemberAddDialog.tsx new file mode 100644 index 0000000..500babb --- /dev/null +++ b/src/Journals/JournalMemberAddDialog.tsx @@ -0,0 +1,132 @@ +import * as React from 'react'; + +import TextField from '@material-ui/core/TextField'; + +import LoadingIndicator from '../widgets/LoadingIndicator'; +import ConfirmationDialog from '../widgets/ConfirmationDialog'; +import PrettyFingerprint from '../widgets/PrettyFingerprint'; + +import * as EteSync from '../api/EteSync'; +import { CredentialsData } from '../store'; + +import { handleInputChange } from '../helpers'; + +interface PropsType { + etesync: CredentialsData; + info: EteSync.CollectionInfo; + onOk: (user: string, publicKey: string) => void; + onClose: () => void; +} + +class JournalMemberAddDialog extends React.PureComponent { + public state = { + addUser: '', + publicKey: '', + userChosen: false, + error: undefined as Error | undefined, + }; + + private handleInputChange: any; + + constructor(props: PropsType) { + super(props); + + this.handleInputChange = handleInputChange(this); + this.onAddRequest = this.onAddRequest.bind(this); + this.onOk = this.onOk.bind(this); + } + + public render() { + const { onClose } = this.props; + const { addUser, userChosen, publicKey, error } = this.state; + + if (error) { + return ( + <> + + User ({addUser}) not found, or has journal sharing disabled. + + + ); + } + + if (publicKey) { + return ( + <> + +

+ Verify {addUser}'s security fingerprint to ensure the encryption is secure. +

+
+ +
+
+ + ); + } else { + return ( + <> + + { userChosen ? + + : + + } + + + ); + } + } + + private onAddRequest(user: string) { + this.setState({ + userChosen: true, + }); + + const { etesync } = this.props; + const { addUser } = this.state; + + const creds = etesync.credentials; + const apiBase = etesync.serviceApiUrl; + const userInfoManager = new EteSync.UserInfoManager(creds, apiBase); + userInfoManager.fetch(addUser).then((userInfo) => { + this.setState({ + publicKey: userInfo.publicKey, + }); + }).catch((error) => { + this.setState({error}); + }); + } + + private onOk() { + const { addUser, publicKey } = this.state; + this.props.onOk(addUser, publicKey); + } +} + +export default JournalMemberAddDialog; diff --git a/src/Journals/JournalMembers.tsx b/src/Journals/JournalMembers.tsx index bfcc4cd..41c9d36 100644 --- a/src/Journals/JournalMembers.tsx +++ b/src/Journals/JournalMembers.tsx @@ -1,20 +1,27 @@ import * as React from 'react'; +import * as sjcl from 'sjcl'; import { List, ListItem } from '../widgets/List'; import { Theme, withTheme } from '@material-ui/core/styles'; +import IconMemberAdd from '@material-ui/icons/PersonAdd'; import AppBarOverride from '../widgets/AppBarOverride'; import Container from '../widgets/Container'; import LoadingIndicator from '../widgets/LoadingIndicator'; import ConfirmationDialog from '../widgets/ConfirmationDialog'; +import JournalMemberAddDialog from './JournalMemberAddDialog'; + import * as EteSync from '../api/EteSync'; -import { CredentialsData } from '../store'; +import { CredentialsData, UserInfoData } from '../store'; + +import { SyncInfoJournal } from '../SyncGate'; interface PropsType { etesync: CredentialsData; - info: EteSync.CollectionInfo; + syncJournal: SyncInfoJournal; + userInfo: UserInfoData; } interface PropsTypeInner extends PropsType { @@ -25,6 +32,7 @@ class JournalMembers extends React.PureComponent { public state = { members: null as EteSync.JournalMemberJson[] | null, revokeUser: null as string | null, + addMemberOpen: false, }; constructor(props: PropsTypeInner) { @@ -32,30 +40,38 @@ class JournalMembers extends React.PureComponent { this.onRevokeRequest = this.onRevokeRequest.bind(this); this.onRevokeDo = this.onRevokeDo.bind(this); + this.onMemberAdd = this.onMemberAdd.bind(this); } public render() { - const { info } = this.props; - const { members, revokeUser } = this.state; + const { syncJournal } = this.props; + const { members, revokeUser, addMemberOpen } = this.state; + + const info = syncJournal.collection; return ( <> { members ? - (members.length > 0 ? - - { members.map((member) => ( + + } onClick={() => this.setState({ addMemberOpen: true })}> + Add member + + {(members.length > 0 ? + members.map((member) => ( this.onRevokeRequest(member.user)}> {member.user} - ))} + )) + : + + No members + + )} - : -
No members
- ) : - + }
{ Would you like to revoke {revokeUser}'s access?
Please be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.
+ + { addMemberOpen && + this.setState({ addMemberOpen: false })} + /> + } ); } @@ -77,7 +102,8 @@ class JournalMembers extends React.PureComponent { } private fetchMembers() { - const { etesync, info } = this.props; + const { etesync, syncJournal } = this.props; + const info = syncJournal.collection; const creds = etesync.credentials; const apiBase = etesync.serviceApiUrl; @@ -96,8 +122,9 @@ class JournalMembers extends React.PureComponent { } private onRevokeDo() { - const { etesync, info } = this.props; + const { etesync, syncJournal } = this.props; const { revokeUser } = this.state; + const info = syncJournal.collection; const creds = etesync.credentials; const apiBase = etesync.serviceApiUrl; @@ -109,6 +136,35 @@ class JournalMembers extends React.PureComponent { revokeUser: null, }); } + + private onMemberAdd(user: string, publicKey: string) { + const { etesync, syncJournal, userInfo } = this.props; + const journal = syncJournal.journal; + const derived = this.props.etesync.encryptionKey; + + const keyPair = userInfo.getKeyPair(new EteSync.CryptoManager(derived, 'userInfo', userInfo.version)); + let cryptoManager: EteSync.CryptoManager; + if (journal.key) { + const asymmetricCryptoManager = new EteSync.AsymmetricCryptoManager(keyPair); + const derivedJournalKey = asymmetricCryptoManager.decryptBytes(journal.key); + cryptoManager = EteSync.CryptoManager.fromDerivedKey(derivedJournalKey, journal.version); + } else { + cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); + } + + const pubkeyBytes = sjcl.codec.bytes.fromBits(sjcl.codec.base64.toBits(publicKey)); + const encryptedKey = sjcl.codec.base64.fromBits(sjcl.codec.bytes.toBits(cryptoManager.getEncryptedKey(keyPair, pubkeyBytes))); + + const creds = etesync.credentials; + const apiBase = etesync.serviceApiUrl; + const journalMembersManager = new EteSync.JournalMembersManager(creds, apiBase, journal.uid); + journalMembersManager.create({ user, key: encryptedKey }).then(() => { + this.fetchMembers(); + }); + this.setState({ + addMemberOpen: false, + }); + } } export default withTheme()(JournalMembers); diff --git a/src/Journals/index.tsx b/src/Journals/index.tsx index 67acaf5..9fe7f83 100644 --- a/src/Journals/index.tsx +++ b/src/Journals/index.tsx @@ -92,7 +92,8 @@ class Journals extends React.PureComponent { render={() => ( )} />