diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2019-07-05 17:19:21 +0100 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2019-07-05 17:19:21 +0100 |
| commit | 7e4c53472a42bc91cad8c74ac638a36faf03793d (patch) | |
| tree | 54cc27ace293e0936ab4b9a022bcf1b3dca12ea2 /gui/src | |
| parent | f272ffc8a5e4e3f78b61860dcfed5aa07b843719 (diff) | |
| parent | 4ce55b897d81baab00a16b07a28187ba423fd2e1 (diff) | |
| download | mullvadvpn-7e4c53472a42bc91cad8c74ac638a36faf03793d.tar.xz mullvadvpn-7e4c53472a42bc91cad8c74ac638a36faf03793d.zip | |
Merge branch 'manage-wg-keys-in-gui'
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/config.json | 1 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 49 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 48 | ||||
| -rw-r--r-- | gui/src/main/notification-controller.ts | 10 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 28 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 18 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 168 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeysStyles.tsx | 48 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 7 | ||||
| -rw-r--r-- | gui/src/renderer/containers/WireguardKeysPage.tsx | 27 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 73 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 117 | ||||
| -rw-r--r-- | gui/src/renderer/routes.tsx | 6 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 33 |
14 files changed, 619 insertions, 14 deletions
diff --git a/gui/src/config.json b/gui/src/config.json index 54eca6000b..9e11eea254 100644 --- a/gui/src/config.json +++ b/gui/src/config.json @@ -2,6 +2,7 @@ "links": { "createAccount": "https://mullvad.net/account/create/", "purchase": "https://mullvad.net/account/login/", + "manageKeys": "https://mullvad.net/account/login/", "faq": "https://mullvad.net/faq/", "download": "https://mullvad.net/download/", "supportEmail": "support@mullvad.net" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 91f9e8771c..e317ffb794 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -7,6 +7,7 @@ import { ILocation, IRelayList, ISettings, + KeygenEvent, RelaySettingsUpdate, TunnelState, } from '../shared/daemon-rpc-types'; @@ -330,6 +331,13 @@ const settingsSchema = partialObject({ tunnel_options: tunnelOptionsSchema, }); +const keygenEventSchema = oneOf( + enumeration('too_many_keys', 'generation_failure'), + object({ + new_key: string, + }), +); + const daemonEventSchema = oneOf( object({ tunnel_state: tunnelStateSchema, @@ -341,12 +349,7 @@ const daemonEventSchema = oneOf( relay_list: relayListSchema, }), object({ - wireguard_key: oneOf( - enumeration('too_many_keys', 'generation_failure'), - object({ - new_key: string, - }), - ), + wireguard_key: keygenEventSchema, }), ); @@ -524,6 +527,40 @@ export class DaemonRpc { } } + public async generateWireguardKey(): Promise<KeygenEvent> { + const response = await this.transport.send('generate_wireguard_key'); + try { + const validatedResponse: any = validate(keygenEventSchema, response); + switch (validatedResponse) { + case 'too_many_keys': + case 'generation_failure': + return validatedResponse; + default: + return camelCaseObjectKeys(validatedResponse as object) as KeygenEvent; + } + } catch (error) { + throw new ResponseParseError(`Invalid response from generate_wireguard_key ${error}`); + } + } + + public async getWireguardKey(): Promise<string | undefined> { + const response = await this.transport.send('get_wireguard_key'); + try { + return validate(maybe(string), response) || undefined; + } catch (error) { + throw new ResponseParseError('Invalid response from get_wireguard_key'); + } + } + + public async verifyWireguardKey(): Promise<boolean> { + const response = await this.transport.send('verify_wireguard_key'); + try { + return validate(boolean, response); + } catch (error) { + throw new ResponseParseError('Invalid response from verify_wireguard_key'); + } + } + public async getVersionInfo(): Promise<IAppVersionInfo> { const response = await this.transport.send('get_version_info', [], NETWORK_CALL_TIMEOUT); try { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index bdc1c4f5ec..a0dcbeb50b 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -13,6 +13,7 @@ import { IRelayList, IRelayListHostname, ISettings, + KeygenEvent, RelaySettings, RelaySettingsUpdate, TunnelState, @@ -126,6 +127,8 @@ class ApplicationMain { // The UI locale which is set once from onReady handler private locale = 'en'; + private wireguardPublicKey?: string; + public run() { // Since electron's GPU blacklists are broken, GPU acceleration won't work on older distros if (process.platform === 'linux') { @@ -487,9 +490,7 @@ class ApplicationMain { } else if ('relayList' in daemonEvent) { this.setRelays(daemonEvent.relayList, this.settings.relaySettings); } else if ('wireguardKey' in daemonEvent) { - /// TODO: handle wireguard key events properly. - log.info(`Received new key event`); - log.info(daemonEvent); + this.handleWireguardKeygenEvent(daemonEvent.wireguardKey); } }, (error: Error) => { @@ -512,6 +513,28 @@ class ApplicationMain { } } + private setWireguardKey(wireguardKey?: string) { + this.wireguardPublicKey = wireguardKey; + if (this.windowController) { + IpcMainEventChannel.wireguardKeys.notify(this.windowController.webContents, wireguardKey); + } + } + + private handleWireguardKeygenEvent(event: KeygenEvent) { + switch (event) { + case 'too_many_keys': + case 'generation_failure': + this.notificationController.notifyKeyGenerationFailed(); + this.wireguardPublicKey = undefined; + break; + default: + this.wireguardPublicKey = event.newKey; + } + if (this.windowController) { + IpcMainEventChannel.wireguardKeys.notifyKeygenEvent(this.windowController.webContents, event); + } + } + private setTunnelState(newState: TunnelState) { this.tunnelState = newState; this.updateTrayIcon(newState, this.settings.blockWhenDisconnected); @@ -534,6 +557,7 @@ class ApplicationMain { if (oldSettings.accountToken !== newSettings.accountToken) { this.updateAccountHistory(); + this.fetchWireguardKey(); } if (this.windowController) { @@ -831,6 +855,7 @@ class ApplicationMain { currentVersion: this.currentVersion, upgradeVersion: this.upgradeVersion, guiSettings: this.guiSettings.state, + wireguardPublicKey: this.wireguardPublicKey, })); IpcMainEventChannel.settings.handleAllowLan((allowLan: boolean) => @@ -888,6 +913,15 @@ class ApplicationMain { this.updateAccountHistory(); }); + IpcMainEventChannel.wireguardKeys.handleGenerateKey(async () => { + try { + return await this.daemonRpc.generateWireguardKey(); + } catch { + return 'generation_failure'; + } + }); + IpcMainEventChannel.wireguardKeys.handleVerifyKey(() => this.daemonRpc.verifyWireguardKey()); + ipcMain.on('show-window', () => { const windowController = this.windowController; if (windowController) { @@ -970,6 +1004,14 @@ class ApplicationMain { } } + private async fetchWireguardKey(): Promise<void> { + try { + this.setWireguardKey(await this.daemonRpc.getWireguardKey()); + } catch (error) { + log.error(`Failed to fetch wireguard key: ${error.message}`); + } + } + private updateDaemonsAutoConnect() { const daemonAutoConnect = this.guiSettings.autoConnect && getOpenAtLogin(); if (daemonAutoConnect !== this.settings.autoConnect) { diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 771e788722..4b0f59fbf6 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -109,6 +109,16 @@ export default class NotificationController { }); } + public notifyKeyGenerationFailed() { + const notification = new Notification({ + title: this.notificationTitle, + body: messages.pgettext('notifications', 'Wireguard key generation failed'), + silent: true, + icon: this.notificationIcon, + }); + this.scheduleNotification(notification); + } + public cancelPendingNotifications() { for (const notification of this.pendingNotifications) { notification.close(); diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 3d5a6eb0e5..6d5d9ae5ab 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -36,6 +36,7 @@ import { ILocation, IRelayList, ISettings, + KeygenEvent, RelaySettings, RelaySettingsUpdate, TunnelState, @@ -150,6 +151,14 @@ export default class AppRenderer { this.storeAutoStart(autoStart); }); + IpcRendererEventChannel.wireguardKeys.listen((publicKey?: string) => { + this.setWireguardPublicKey(publicKey); + }); + + IpcRendererEventChannel.wireguardKeys.listenKeygenEvents((event: KeygenEvent) => { + this.reduxActions.settings.setWireguardKeygenEvent(event); + }); + // Request the initial state from the main process const initialState = IpcRendererEventChannel.state.get(); @@ -172,6 +181,7 @@ export default class AppRenderer { this.setUpgradeVersion(initialState.upgradeVersion); this.setGuiSettings(initialState.guiSettings); this.storeAutoStart(initialState.autoStart); + this.setWireguardPublicKey(initialState.wireguardPublicKey); if (initialState.isConnected) { this.onDaemonConnected(); @@ -346,6 +356,20 @@ export default class AppRenderer { IpcRendererEventChannel.guiSettings.setMonochromaticIcon(monochromaticIcon); } + public async verifyWireguardKey(publicKey: string) { + const actions = this.reduxActions; + actions.settings.verifyWireguardKey(publicKey); + const valid = await IpcRendererEventChannel.wireguardKeys.verifyKey(); + actions.settings.completeWireguardKeyVerification(valid); + } + + public async generateWireguardKey() { + const actions = this.reduxActions; + actions.settings.generateWireguardKey(); + const keygenEvent = await IpcRendererEventChannel.wireguardKeys.generateKey(); + actions.settings.setWireguardKeygenEvent(keygenEvent); + } + private setRelaySettings(relaySettings: RelaySettings) { const actions = this.reduxActions; @@ -615,4 +639,8 @@ export default class AppRenderer { private storeAutoStart(autoStart: boolean) { this.reduxActions.settings.updateAutoStart(autoStart); } + + private setWireguardPublicKey(publicKey?: string) { + this.reduxActions.settings.setWireguardKey(publicKey); + } } diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index f5394a2520..1805e07bae 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -36,11 +36,13 @@ interface IProps { mssfix?: number; port?: number; bridgeState: BridgeState; + enableWireguardKeysPage: boolean; setBridgeState: (value: BridgeState) => void; setEnableIpv6: (value: boolean) => void; setBlockWhenDisconnected: (value: boolean) => void; setOpenVpnMssfix: (value: number | undefined) => void; setRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; + onViewWireguardKeys: () => void; onClose: () => void; } @@ -255,6 +257,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { }, )} </Cell.Footer> + {this.wireguardKeysButton()} </NavigationScrollbars> </View> </NavigationContainer> @@ -264,6 +267,21 @@ export default class AdvancedSettings extends Component<IProps, IState> { ); } + private wireguardKeysButton() { + if (this.props.enableWireguardKeysPage) { + return ( + <View> + <Cell.CellButton onPress={this.props.onViewWireguardKeys}> + <Cell.Label>{messages.pgettext('advanced-settings-view', 'WireGuard keys')}</Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </View> + ); + } else { + return null; + } + } + private onSelectProtocol = (protocol?: RelayProtocol) => { this.props.setRelayProtocolAndPort(protocol); }; diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx new file mode 100644 index 0000000000..dadaca4b51 --- /dev/null +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -0,0 +1,168 @@ +import * as React from 'react'; +import { Component, Text, View } from 'reactxp'; +import { messages } from '../../shared/gettext'; +import { WgKeyState } from '../redux/settings/reducers'; +import * as AppButton from './AppButton'; +import ImageView from './ImageView'; +import { Container, Layout } from './Layout'; +import { BackBarItem, NavigationBar, NavigationContainer } from './NavigationBar'; +import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import styles from './WireguardKeysStyles'; + +export interface IProps { + keyState: WgKeyState; + isOffline: boolean; + + onGenerateKey: () => void; + onVerifyKey: (publicKey: string) => void; + onVisitWebsiteKey: () => void; + onClose: () => void; +} + +export default class WireguardKeys extends Component<IProps> { + public render() { + return ( + <Layout> + <Container> + <View> + <NavigationContainer> + <NavigationBar> + <BackBarItem action={this.props.onClose}> + {// TRANSLATORS: Back button in navigation bar + messages.pgettext('wireguard-keys-nav', 'Advanced')} + </BackBarItem> + </NavigationBar> + </NavigationContainer> + + <View style={styles.wgkeys__container}> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('wireguard-keys-nav', 'WireGuard keys')} + </HeaderTitle> + </SettingsHeader> + + <View style={styles.wgkeys__row}> + <Text style={styles.wgkeys__row_label}> + {messages.pgettext('wireguard-keys', 'Public key')} + </Text> + <View style={styles.wgkeys__row_value}>{this.getKeyRow()}</View> + <View style={styles.wgkeys__validity_row}>{this.keyValidityLabel()}</View> + </View> + + <View style={styles.wgkeys__row}>{this.getActionButton()}</View> + <View style={styles.wgkeys__row}> + <AppButton.GreenButton + disabled={this.props.isOffline} + onPress={this.props.onVisitWebsiteKey}> + <AppButton.Label> + {messages.pgettext('wireguard-key-view', 'Manage keys in website')} + </AppButton.Label> + <AppButton.Icon source="icon-extLink" height={16} width={16} /> + </AppButton.GreenButton> + </View> + </View> + </View> + </Container> + </Layout> + ); + } + + /// Action button can either generate or verify a key + private getActionButton() { + switch (this.props.keyState.type) { + case 'key-set': + const publicKey = this.props.keyState.publicKey; + // if the key is known to be invalid, allow the user to generate a new one + if (this.props.keyState.valid === false) { + break; + } + + const verificationCallback = () => this.props.onVerifyKey(publicKey); + + return ( + <AppButton.GreenButton disabled={this.props.isOffline} onPress={verificationCallback}> + <AppButton.Label> + {messages.pgettext('wireguard-key-view', 'Verify key')} + </AppButton.Label> + </AppButton.GreenButton> + ); + + case 'being-verified': + return this.busyButton(messages.pgettext('wireguard-key-view', 'Verifying key')); + case 'being-generated': + return this.busyButton(messages.pgettext('wireguard-key-view', 'Generating key')); + } + return ( + <AppButton.GreenButton disabled={this.props.isOffline} onPress={this.props.onGenerateKey}> + <AppButton.Label>{messages.pgettext('wireguard-key-view', 'Generate key')}</AppButton.Label> + </AppButton.GreenButton> + ); + } + + private busyButton(message: string) { + return ( + <AppButton.GreenButton disabled={true}> + <AppButton.Label>{message}</AppButton.Label> + <AppButton.Icon source="icon-spinner" height={16} width={16} /> + </AppButton.GreenButton> + ); + } + + private getKeyRow() { + switch (this.props.keyState.type) { + case 'being-verified': + case 'key-set': + // mimicking the truncating of the key from website + return ( + <View title={this.props.keyState.publicKey}> + <Text style={styles.wgkeys__row_value}> + {this.props.keyState.publicKey.substring(0, 20) + '...'} + </Text> + </View> + ); + case 'being-generated': + return <ImageView source="icon-spinner" height={25} width={25} />; + case 'too-many-keys': + return ( + <Text style={styles.wgkeys__invalid_key}> + {messages.pgettext('wireguard-key-view', 'Account has too many keys already')} + </Text> + ); + case 'generation-failure': + return ( + <Text style={styles.wgkeys__invalid_key}> + {messages.pgettext('wireguard-key-view', 'Failed to generate key')} + </Text> + ); + default: + return ( + <Text style={styles.wgkeys__row_value}> + {messages.pgettext('wireguard-key-view', 'No key set')} + </Text> + ); + } + } + + private keyValidityLabel() { + switch (this.props.keyState.type) { + case 'being-verified': + return <ImageView source="icon-spinner" height={25} width={25} />; + case 'key-set': + if (this.props.keyState.valid === true) { + return ( + <Text style={styles.wgkeys__valid_key}> + {messages.pgettext('account-view', 'Key is valid')} + </Text> + ); + } else if (this.props.keyState.valid === false) { + return ( + <Text style={styles.wgkeys__invalid_key}> + {messages.pgettext('wireguard-key-view', 'Key is invalid')} + </Text> + ); + } + default: + return ''; + } + } +} diff --git a/gui/src/renderer/components/WireguardKeysStyles.tsx b/gui/src/renderer/components/WireguardKeysStyles.tsx new file mode 100644 index 0000000000..8f94bdcca6 --- /dev/null +++ b/gui/src/renderer/components/WireguardKeysStyles.tsx @@ -0,0 +1,48 @@ +import { Styles } from 'reactxp'; +import { colors } from '../../config.json'; + +export default { + wgkeys__container: Styles.createViewStyle({ + flexDirection: 'column', + flex: 1, + }), + wgkeys__row: Styles.createViewStyle({ + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 24, + paddingRight: 24, + marginBottom: 24, + }), + wgkeys__row_label: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + lineHeight: 20, + letterSpacing: -0.2, + color: colors.white60, + marginBottom: 9, + }), + wgkeys__validity_row: Styles.createViewStyle({ + paddingTop: 5, + }), + wgkeys__row_value: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 16, + lineHeight: 19, + fontWeight: '800', + color: colors.white, + }), + wgkeys__invalid_key: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + color: colors.red, + }), + wgkeys__valid_key: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '600', + lineHeight: 20, + color: colors.green, + }), +}; diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index a8784a62e2..4dcb91b6b5 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -1,4 +1,4 @@ -import { goBack } from 'connected-react-router'; +import { goBack, push } from 'connected-react-router'; import log from 'electron-log'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -12,12 +12,14 @@ import { ISharedRouteProps } from '../routes'; const mapStateToProps = (state: IReduxState) => { const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); + const enableWireguardKeysPage = process.platform === 'linux' || process.platform === 'darwin'; return { enableIpv6: state.settings.enableIpv6, blockWhenDisconnected: state.settings.blockWhenDisconnected, mssfix: state.settings.openVpn.mssfix, bridgeState: state.settings.bridgeState, + enableWireguardKeysPage, ...protocolAndPort, }; }; @@ -38,7 +40,7 @@ const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => }; const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { - const history = bindActionCreators({ goBack }, dispatch); + const history = bindActionCreators({ push, goBack }, dispatch); return { onClose: () => { history.goBack(); @@ -98,6 +100,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) = log.error('Failed to update mssfix value', e.message); } }, + onViewWireguardKeys: () => history.push('/settings/advanced/wireguard-keys'), }; }; diff --git a/gui/src/renderer/containers/WireguardKeysPage.tsx b/gui/src/renderer/containers/WireguardKeysPage.tsx new file mode 100644 index 0000000000..2fb185cb1d --- /dev/null +++ b/gui/src/renderer/containers/WireguardKeysPage.tsx @@ -0,0 +1,27 @@ +import { goBack, push } from 'connected-react-router'; +import { shell } from 'electron'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { links } from '../../config.json'; +import WireguardKeys from '../components/WireguardKeys'; +import { IReduxState, ReduxDispatch } from '../redux/store'; +import { ISharedRouteProps } from '../routes'; + +const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => ({ + keyState: state.settings.wireguardKeyState, + isOffline: state.connection.isBlocked, +}); +const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { + const history = bindActionCreators({ push, goBack }, dispatch); + return { + onGenerateKey: () => props.app.generateWireguardKey(), + onVerifyKey: (publicKey: string) => props.app.verifyWireguardKey(publicKey), + onVisitWebsiteKey: () => shell.openExternal(links.manageKeys), + onClose: () => history.goBack(), + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(WireguardKeys); diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index 742923276d..6c87c18fa9 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,4 +1,4 @@ -import { BridgeState } from '../../../shared/daemon-rpc-types'; +import { BridgeState, KeygenEvent } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { IRelayLocationRedux, RelaySettingsRedux } from './reducers'; @@ -47,6 +47,31 @@ export interface IUpdateAutoStartAction { autoStart: boolean; } +// Used to set wireguard key when accounts are changed. +export interface IWireguardSetKey { + type: 'SET_WIREGUARD_KEY'; + publicKey?: string; +} + +export interface IWireguardGenerateKey { + type: 'GENERATE_WIREGUARD_KEY'; +} + +export interface IWireguardVerifyKey { + type: 'VERIFY_WIREGUARD_KEY'; + publicKey: string; +} + +export interface IWireguardKeygenEvent { + type: 'WIREGUARD_KEYGEN_EVENT'; + event: KeygenEvent; +} + +export interface IWireguardKeyVerifiedAction { + type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE'; + verified: boolean; +} + export type SettingsAction = | IUpdateGuiSettingsAction | IUpdateRelayAction @@ -56,7 +81,12 @@ export type SettingsAction = | IUpdateBlockWhenDisconnectedAction | IUpdateBridgeStateAction | IUpdateOpenVpnMssfixAction - | IUpdateAutoStartAction; + | IUpdateAutoStartAction + | IWireguardSetKey + | IWireguardVerifyKey + | IWireguardGenerateKey + | IWireguardKeygenEvent + | IWireguardKeyVerifiedAction; function updateGuiSettings(guiSettings: IGuiSettingsState): IUpdateGuiSettingsAction { return { @@ -123,6 +153,40 @@ function updateAutoStart(autoStart: boolean): IUpdateAutoStartAction { }; } +function setWireguardKey(publicKey?: string): IWireguardSetKey { + return { + type: 'SET_WIREGUARD_KEY', + publicKey, + }; +} + +function setWireguardKeygenEvent(event: KeygenEvent): IWireguardKeygenEvent { + return { + type: 'WIREGUARD_KEYGEN_EVENT', + event, + }; +} + +function generateWireguardKey(): IWireguardGenerateKey { + return { + type: 'GENERATE_WIREGUARD_KEY', + }; +} + +function verifyWireguardKey(publicKey: string): IWireguardVerifyKey { + return { + type: 'VERIFY_WIREGUARD_KEY', + publicKey, + }; +} + +function completeWireguardKeyVerification(verified: boolean): IWireguardKeyVerifiedAction { + return { + type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE', + verified, + }; +} + export default { updateGuiSettings, updateRelay, @@ -133,4 +197,9 @@ export default { updateBridgeState, updateOpenVpnMssfix, updateAutoStart, + setWireguardKey, + setWireguardKeygenEvent, + generateWireguardKey, + verifyWireguardKey, + completeWireguardKeyVerification, }; diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 6c652f887d..97a6a21ee4 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -1,4 +1,10 @@ -import { BridgeState, RelayLocation, RelayProtocol } from '../../../shared/daemon-rpc-types'; +import log from 'electron-log'; +import { + BridgeState, + KeygenEvent, + RelayLocation, + RelayProtocol, +} from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; import { ReduxAction } from '../store'; @@ -41,6 +47,41 @@ export interface IRelayLocationRedux { cities: IRelayLocationCityRedux[]; } +interface IWgKeySet { + type: 'key-set'; + publicKey: string; + valid?: boolean; +} + +interface IWgKeyNotSet { + type: 'key-not-set'; +} + +interface IWgTooManyKeys { + type: 'too-many-keys'; +} + +interface IWgKeyGenerationFailure { + type: 'generation-failure'; +} + +interface IWgKeyBeingGenerated { + type: 'being-generated'; +} + +interface IWgKeyBeingVerified { + type: 'being-verified'; + publicKey: string; +} + +export type WgKeyState = + | IWgKeySet + | IWgKeyNotSet + | IWgKeyGenerationFailure + | IWgTooManyKeys + | IWgKeyBeingVerified + | IWgKeyBeingGenerated; + export interface ISettingsReduxState { autoStart: boolean; guiSettings: IGuiSettingsState; @@ -53,6 +94,7 @@ export interface ISettingsReduxState { openVpn: { mssfix?: number; }; + wireguardKeyState: WgKeyState; } const initialState: ISettingsReduxState = { @@ -76,6 +118,9 @@ const initialState: ISettingsReduxState = { bridgeState: 'auto', blockWhenDisconnected: false, openVpn: {}, + wireguardKeyState: { + type: 'key-not-set', + }, }; export default function( @@ -140,7 +185,77 @@ export default function( bridgeState: action.bridgeState, }; + case 'SET_WIREGUARD_KEY': + return { + ...state, + wireguardKeyState: setWireguardKey(action.publicKey), + }; + case 'WIREGUARD_KEYGEN_EVENT': + return { + ...state, + wireguardKeyState: setWireguardKeygenEvent(action.event), + }; + case 'WIREGUARD_KEY_VERIFICATION_COMPLETE': + return { + ...state, + wireguardKeyState: applyKeyVerification(state.wireguardKeyState, action.verified), + }; + case 'VERIFY_WIREGUARD_KEY': + return { + ...state, + wireguardKeyState: { type: 'being-verified', publicKey: action.publicKey }, + }; + + case 'GENERATE_WIREGUARD_KEY': + return { + ...state, + wireguardKeyState: { type: 'being-generated' }, + }; + + default: + return state; + } +} + +function setWireguardKey(publicKey?: string): WgKeyState { + if (publicKey) { + return { + type: 'key-set', + publicKey, + }; + } else { + return { + type: 'key-not-set', + }; + } +} + +function setWireguardKeygenEvent(keygenEvent: KeygenEvent): WgKeyState { + switch (keygenEvent) { + case 'too_many_keys': + return { type: 'too-many-keys' }; + case 'generation_failure': + return { type: 'generation-failure' }; + default: + return { + type: 'key-set', + publicKey: keygenEvent.newKey, + valid: true, + }; + } +} + +function applyKeyVerification(state: WgKeyState, verified: boolean): WgKeyState { + switch (state.type) { + case 'being-verified': + return { + ...state, + type: 'key-set', + valid: verified, + }; + // drop the verification event if the key wasn't being verified. default: + log.error(`Received key verification event when key wasn't being verified`); return state; } } diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx index ae694cefbc..814eeae31e 100644 --- a/gui/src/renderer/routes.tsx +++ b/gui/src/renderer/routes.tsx @@ -12,6 +12,7 @@ import PreferencesPage from './containers/PreferencesPage'; import SelectLocationPage from './containers/SelectLocationPage'; import SettingsPage from './containers/SettingsPage'; import SupportPage from './containers/SupportPage'; +import WireguardKeysPage from './containers/WireguardKeysPage'; import { getTransitionProps } from './transitions'; export interface ISharedRouteProps { @@ -90,6 +91,11 @@ class AppRoutes extends React.Component<IAppRoutesProps, IAppRoutesState> { path="/settings/advanced" component={AdvancedSettingsPage} /> + <CustomRoute + exact={true} + path="/settings/advanced/wireguard-keys" + component={WireguardKeysPage} + /> <CustomRoute exact={true} path="/settings/support" component={SupportPage} /> <CustomRoute exact={true} path="/select-location" component={SelectLocationPage} /> </Switch> diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 207e509779..2bb594f5e6 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -12,6 +12,7 @@ import { ILocation, IRelayList, ISettings, + KeygenEvent, RelaySettingsUpdate, TunnelState, } from './daemon-rpc-types'; @@ -28,6 +29,7 @@ export interface IAppStateSnapshot { currentVersion: ICurrentAppVersionInfo; upgradeVersion: IAppUpgradeInfo; guiSettings: IGuiSettingsState; + wireguardPublicKey?: string; } interface ISender<T> { @@ -112,6 +114,18 @@ interface IAutoStartHandlers extends ISender<boolean> { handleSet(fn: (value: boolean) => Promise<void>): void; } +interface IWireguardKeyMethods extends IReceiver<string | undefined> { + listenKeygenEvents(fn: (event: KeygenEvent) => void): void; + generateKey(): Promise<KeygenEvent>; + verifyKey(): Promise<boolean>; +} + +interface IWireguardKeyHandlers extends ISender<string | undefined> { + notifyKeygenEvent(webContents: WebContents, event: KeygenEvent): void; + handleGenerateKey(fn: () => Promise<KeygenEvent>): void; + handleVerifyKey(fn: () => Promise<boolean>): void; +} + /// Events names const DAEMON_CONNECTED = 'daemon-connected'; @@ -152,6 +166,11 @@ const GET_ACCOUNT_DATA = 'get-account-data'; const AUTO_START_CHANGED = 'auto-start-changed'; const SET_AUTO_START = 'set-auto-start'; +const WIREGUARD_KEY_SET = 'wireguard-key-change-event'; +const WIREGUARD_KEYGEN_EVENT = 'wireguard-keygen-event'; +const GENERATE_WIREGUARD_KEY = 'generate-wireguard-key'; +const VERIFY_WIREGUARD_KEY = 'verify-wireguard-key'; + /// Typed IPC event channel /// /// Static methods are meant to be provide the way to send the events from a renderer process, while @@ -228,6 +247,13 @@ export class IpcRendererEventChannel { listen: listen(ACCOUNT_HISTORY_CHANGED), removeItem: requestSender(REMOVE_ACCOUNT_HISTORY_ITEM), }; + + public static wireguardKeys: IWireguardKeyMethods = { + listen: listen(WIREGUARD_KEY_SET), + listenKeygenEvents: listen(WIREGUARD_KEYGEN_EVENT), + generateKey: requestSender(GENERATE_WIREGUARD_KEY), + verifyKey: requestSender(VERIFY_WIREGUARD_KEY), + }; } export class IpcMainEventChannel { @@ -302,6 +328,13 @@ export class IpcMainEventChannel { notify: sender<AccountToken[]>(ACCOUNT_HISTORY_CHANGED), handleRemoveItem: requestHandler(REMOVE_ACCOUNT_HISTORY_ITEM), }; + + public static wireguardKeys: IWireguardKeyHandlers = { + notify: sender<string | undefined>(WIREGUARD_KEY_SET), + notifyKeygenEvent: sender<KeygenEvent>(WIREGUARD_KEYGEN_EVENT), + handleGenerateKey: requestHandler(GENERATE_WIREGUARD_KEY), + handleVerifyKey: requestHandler(VERIFY_WIREGUARD_KEY), + }; } function listen<T>(event: string): (fn: (value: T) => void) => void { |
