diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2019-09-05 10:53:06 +0100 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2019-09-05 10:53:06 +0100 |
| commit | 0bccc7ec2d619c6133e78465bc4c1a9afffb7638 (patch) | |
| tree | 07c0a3be4e17961736c36c60eb6fea994ca6c08b | |
| parent | 14be4d9a9a79f37e6f27340f4d9f6aed3bd46a67 (diff) | |
| parent | 6ebbe56e051c465273ce8f2c16851eb57d222cd4 (diff) | |
| download | mullvadvpn-0bccc7ec2d619c6133e78465bc4c1a9afffb7638.tar.xz mullvadvpn-0bccc7ec2d619c6133e78465bc4c1a9afffb7638.zip | |
Merge branch 'add-key-rotation-rpc'
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 15 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 5 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 24 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeys.tsx | 156 | ||||
| -rw-r--r-- | gui/src/renderer/components/WireguardKeysStyles.tsx | 1 | ||||
| -rw-r--r-- | gui/src/renderer/containers/WireguardKeysPage.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 41 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 72 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 10 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 9 | ||||
| -rw-r--r-- | mullvad-daemon/src/account_history.rs | 6 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 24 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 8 | ||||
| -rw-r--r-- | mullvad-daemon/src/wireguard.rs | 55 | ||||
| -rw-r--r-- | mullvad-jni/src/daemon_interface.rs | 3 | ||||
| -rw-r--r-- | mullvad-jni/src/into_java.rs | 2 | ||||
| -rw-r--r-- | mullvad-jni/src/lib.rs | 2 | ||||
| -rw-r--r-- | mullvad-rpc/src/lib.rs | 6 | ||||
| -rw-r--r-- | mullvad-types/src/wireguard.rs | 24 |
20 files changed, 365 insertions, 113 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b9446955..c18b32b679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,13 +23,19 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +- Add ability to replace the WireGuard key with a new one. Allows manual key rotation. +- Show age of currently set WireGuard key. + ### Changed - Decreased default MTU for WireGuard to 1380 to improve performance over 4G - WireGuard key page now shows a label explaining why buttons are disabled when in a blocked state +- WireGuard key generation will try to replace old key if one exists. ### Fixed - Fix old settings deserialization to allow migrating settings from versions older than 2019.6. - Fix various small issues in GUI<->daemon communication. +- Make GUI WireGuard key verification resilient to failure. #### macOS - Unregister the app properly from the OS when running the bundled `uninstall.sh` script. diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 0a55833731..2d4141304e 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -8,6 +8,7 @@ import { ILocation, IRelayList, ISettings, + IWireguardPublicKey, KeygenEvent, RelaySettingsUpdate, TunnelState, @@ -337,10 +338,18 @@ const settingsSchema = partialObject({ tunnel_options: tunnelOptionsSchema, }); +const wireguardPublicKey = object({ + key: string, + created: string, +}); + const keygenEventSchema = oneOf( enumeration('too_many_keys', 'generation_failure'), object({ - new_key: string, + new_key: object({ + key: string, + created: string, + }), }), ); @@ -567,10 +576,10 @@ export class DaemonRpc { } } - public async getWireguardKey(): Promise<string | undefined> { + public async getWireguardKey(): Promise<IWireguardPublicKey | undefined> { const response = await this.transport.send('get_wireguard_key'); try { - return validate(maybe(string), response) || undefined; + return validate(maybe(wireguardPublicKey), response) || undefined; } catch (error) { throw new ResponseParseError('Invalid response from get_wireguard_key'); } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index efe76bdfab..a0694e814c 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -14,6 +14,7 @@ import { IRelayList, IRelayListHostname, ISettings, + IWireguardPublicKey, KeygenEvent, RelaySettings, RelaySettingsUpdate, @@ -143,7 +144,7 @@ class ApplicationMain { // The UI locale which is set once from onReady handler private locale = 'en'; - private wireguardPublicKey?: string; + private wireguardPublicKey?: IWireguardPublicKey; private accountDataCache = new AccountDataCache( (accountToken) => { @@ -560,7 +561,7 @@ class ApplicationMain { } } - private setWireguardKey(wireguardKey?: string) { + private setWireguardKey(wireguardKey?: IWireguardPublicKey) { this.wireguardPublicKey = wireguardKey; if (this.windowController) { IpcMainEventChannel.wireguardKeys.notify(this.windowController.webContents, wireguardKey); diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 094b4f376d..b08743dab4 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -16,6 +16,7 @@ import AppRoutes from './routes'; import accountActions from './redux/account/actions'; import connectionActions from './redux/connection/actions'; import settingsActions from './redux/settings/actions'; +import { IWgKey } from './redux/settings/reducers'; import configureStore from './redux/store'; import userInterfaceActions from './redux/userinterface/actions'; import versionActions from './redux/version/actions'; @@ -33,6 +34,7 @@ import { ILocation, IRelayList, ISettings, + IWireguardPublicKey, KeygenEvent, liftConstraint, RelaySettings, @@ -129,7 +131,7 @@ export default class AppRenderer { this.storeAutoStart(autoStart); }); - IpcRendererEventChannel.wireguardKeys.listen((publicKey?: string) => { + IpcRendererEventChannel.wireguardKeys.listen((publicKey?: IWireguardPublicKey) => { this.setWireguardPublicKey(publicKey); }); @@ -303,11 +305,16 @@ export default class AppRenderer { IpcRendererEventChannel.guiSettings.setMonochromaticIcon(monochromaticIcon); } - public async verifyWireguardKey(publicKey: string) { + public async verifyWireguardKey(publicKey: IWgKey) { const actions = this.reduxActions; actions.settings.verifyWireguardKey(publicKey); - const valid = await IpcRendererEventChannel.wireguardKeys.verifyKey(); - actions.settings.completeWireguardKeyVerification(valid); + try { + const valid = await IpcRendererEventChannel.wireguardKeys.verifyKey(); + actions.settings.completeWireguardKeyVerification(valid); + } catch (error) { + log.error(`Failed to verify WireGuard key - ${error.message}`); + actions.settings.completeWireguardKeyVerification(undefined); + } } public async generateWireguardKey() { @@ -317,6 +324,13 @@ export default class AppRenderer { actions.settings.setWireguardKeygenEvent(keygenEvent); } + public async replaceWireguardKey(oldKey: IWgKey) { + const actions = this.reduxActions; + actions.settings.replaceWireguardKey(oldKey); + const keygenEvent = await IpcRendererEventChannel.wireguardKeys.generateKey(); + actions.settings.setWireguardKeygenEvent(keygenEvent); + } + private setRelaySettings(relaySettings: RelaySettings) { const actions = this.reduxActions; @@ -554,7 +568,7 @@ export default class AppRenderer { this.reduxActions.settings.updateAutoStart(autoStart); } - private setWireguardPublicKey(publicKey?: string) { + private setWireguardPublicKey(publicKey?: IWireguardPublicKey) { this.reduxActions.settings.setWireguardKey(publicKey); } } diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx index d66ef68cf2..9ea386ec91 100644 --- a/gui/src/renderer/components/WireguardKeys.tsx +++ b/gui/src/renderer/components/WireguardKeys.tsx @@ -1,7 +1,10 @@ +import log from 'electron-log'; +import moment from 'moment'; import * as React from 'react'; import { Component, Text, View } from 'reactxp'; +import { sprintf } from 'sprintf-js'; import { messages } from '../../shared/gettext'; -import { WgKeyState } from '../redux/settings/reducers'; +import { IWgKey, WgKeyState } from '../redux/settings/reducers'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; import { Container, Layout } from './Layout'; @@ -12,11 +15,13 @@ import styles from './WireguardKeysStyles'; export interface IProps { keyState: WgKeyState; isOffline: boolean; + locale: string; + onClose: () => void; onGenerateKey: () => void; - onVerifyKey: (publicKey: string) => void; + onReplaceKey: (old: IWgKey) => void; + onVerifyKey: (publicKey: IWgKey) => void; onVisitWebsiteKey: () => void; - onClose: () => void; } export default class WireguardKeys extends Component<IProps> { @@ -46,11 +51,24 @@ export default class WireguardKeys extends Component<IProps> { <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__row_value}>{this.getKeyText()}</View> + <Text style={styles.wgkeys__row_label}> + {messages.pgettext('wireguard-keys', 'Key generated')} + </Text> + <Text style={styles.wgkeys__row_value}>{this.ageOfKeyString()}</Text> <View style={styles.wgkeys__validity_row}>{this.keyValidityLabel()}</View> </View> - <View style={styles.wgkeys__row}>{this.getActionButton()}</View> + <View style={styles.wgkeys__row}>{this.getGenerateButton()}</View> + <View style={styles.wgkeys__row}> + <AppButton.GreenButton + disabled={this.isVerifyButtonDisabled()} + onPress={this.getOnVerifyKeyCb()}> + <AppButton.Label> + {messages.pgettext('wireguard-key-view', 'Verify key')} + </AppButton.Label> + </AppButton.GreenButton> + </View> <View style={styles.wgkeys__row}> <AppButton.GreenButton disabled={this.props.isOffline} @@ -68,34 +86,50 @@ export default class WireguardKeys extends Component<IProps> { ); } - /// Action button can either generate or verify a key - private getActionButton() { + private isVerifyButtonDisabled(): boolean { 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; - } + return false || this.props.isOffline; + default: + return true; + } + } - const verificationCallback = () => this.props.onVerifyKey(publicKey); + private getOnVerifyKeyCb() { + return () => { + switch (this.props.keyState.type) { + case 'key-set': + const key = this.props.keyState.key; + this.props.onVerifyKey(key); + break; + default: + log.error(`onVerifyKey called from invalid state - ${this.props.keyState.type}`); + } + }; + } - return ( - <AppButton.GreenButton disabled={this.props.isOffline} onPress={verificationCallback}> - <AppButton.Label> - {messages.pgettext('wireguard-key-view', 'Verify key')} - </AppButton.Label> - </AppButton.GreenButton> - ); + /// Action button can either generate or verify a key + private getGenerateButton() { + const generateText = messages.pgettext('wireguard-key-view', 'Generate key'); + const regenerateText = messages.pgettext('wireguard-key-view', 'Regenerate key'); + let buttonText = generateText; + let generateKey = this.props.onGenerateKey; + switch (this.props.keyState.type) { + case 'key-set': + buttonText = regenerateText; + const key = this.props.keyState.key; + generateKey = () => this.props.onReplaceKey(key); + break; case 'being-verified': - return this.busyButton(messages.pgettext('wireguard-key-view', 'Verifying key')); + return this.busyButton(regenerateText); + case 'being-replaced': 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 disabled={this.props.isOffline} onPress={generateKey}> + <AppButton.Label>{buttonText}</AppButton.Label> </AppButton.GreenButton> ); } @@ -109,30 +143,26 @@ export default class WireguardKeys extends Component<IProps> { ); } - private getKeyRow() { + private getKeyText() { 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}> + <View title={this.props.keyState.key.publicKey}> <Text style={styles.wgkeys__row_value}> - {this.props.keyState.publicKey.substring(0, 20) + '...'} + {this.props.keyState.key.publicKey.substring(0, 20) + '...'} </Text> </View> ); + case 'being-replaced': case 'being-generated': - return <ImageView source="icon-spinner" height={25} width={25} />; + return <ImageView source="icon-spinner" height={19} width={19} />; 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')} + {this.formatKeygenFailure(this.props.keyState.type)} </Text> ); default: @@ -147,23 +177,75 @@ export default class WireguardKeys extends Component<IProps> { private keyValidityLabel() { switch (this.props.keyState.type) { case 'being-verified': - return <ImageView source="icon-spinner" height={25} width={25} />; + return <ImageView source="icon-spinner" height={20} width={20} />; case 'key-set': - if (this.props.keyState.valid === true) { + const key = this.props.keyState.key; + if (key.valid === true) { return ( <Text style={styles.wgkeys__valid_key}> {messages.pgettext('account-view', 'Key is valid')} </Text> ); - } else if (this.props.keyState.valid === false) { + } else if (key.valid === false) { return ( <Text style={styles.wgkeys__invalid_key}> {messages.pgettext('wireguard-key-view', 'Key is invalid')} </Text> ); + } else if (key.replacementFailure) { + let failure = ''; + switch (key.replacementFailure) { + case 'too_many_keys': + failure = this.formatKeygenFailure('too-many-keys'); + break; + case 'generation_failure': + failure = this.formatKeygenFailure('generation-failure'); + break; + } + + const failureMessage = sprintf( + messages.pgettext('wireguard-key-view', 'Failed to replace key - %(failure)s'), + { failure }, + ); + return <Text style={styles.wgkeys__invalid_key}>{failureMessage}</Text>; + } else if (key.verificationFailed) { + return ( + <Text style={styles.wgkeys__invalid_key}> + {messages.pgettext('wireguard-key-view', 'Key verification failed')} + </Text> + ); } + + default: + return ( + // Placeholder to take up the same amount of space as the validity text/spinner + <View style={{ marginBottom: 20 }} /> + ); + } + } + + private ageOfKeyString(): string { + let keyCreatedSince = '-'; + switch (this.props.keyState.type) { + case 'key-set': + case 'being-verified': + keyCreatedSince = moment(this.props.keyState.key.created) + .locale(this.props.locale) + .fromNow(); + break; + } + + return keyCreatedSince; + } + + private formatKeygenFailure(failure: 'too-many-keys' | 'generation-failure'): string { + switch (failure) { + case 'too-many-keys': + return messages.pgettext('wireguard-key-view', 'Account has too many keys already'); + case 'generation-failure': + return messages.pgettext('wireguard-key-view', 'Failed to generate a key'); default: - return ''; + return failure; } } diff --git a/gui/src/renderer/components/WireguardKeysStyles.tsx b/gui/src/renderer/components/WireguardKeysStyles.tsx index 2172b79dae..ec3fd6cf2f 100644 --- a/gui/src/renderer/components/WireguardKeysStyles.tsx +++ b/gui/src/renderer/components/WireguardKeysStyles.tsx @@ -24,7 +24,6 @@ export default { lineHeight: 20, letterSpacing: -0.2, color: colors.white60, - marginBottom: 9, }), wgkeys__validity_row: Styles.createViewStyle({ paddingTop: 5, diff --git a/gui/src/renderer/containers/WireguardKeysPage.tsx b/gui/src/renderer/containers/WireguardKeysPage.tsx index 2fb185cb1d..813526ff29 100644 --- a/gui/src/renderer/containers/WireguardKeysPage.tsx +++ b/gui/src/renderer/containers/WireguardKeysPage.tsx @@ -4,20 +4,23 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { links } from '../../config.json'; import WireguardKeys from '../components/WireguardKeys'; +import { IWgKey } from '../redux/settings/reducers'; import { IReduxState, ReduxDispatch } from '../redux/store'; import { ISharedRouteProps } from '../routes'; -const mapStateToProps = (state: IReduxState, _props: ISharedRouteProps) => ({ +const mapStateToProps = (state: IReduxState, props: ISharedRouteProps) => ({ keyState: state.settings.wireguardKeyState, isOffline: state.connection.isBlocked, + locale: props.locale, }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: ISharedRouteProps) => { const history = bindActionCreators({ push, goBack }, dispatch); return { + onClose: () => history.goBack(), onGenerateKey: () => props.app.generateWireguardKey(), - onVerifyKey: (publicKey: string) => props.app.verifyWireguardKey(publicKey), + onReplaceKey: (oldKey: IWgKey) => props.app.replaceWireguardKey(oldKey), + onVerifyKey: (publicKey: IWgKey) => props.app.verifyWireguardKey(publicKey), onVisitWebsiteKey: () => shell.openExternal(links.manageKeys), - onClose: () => history.goBack(), }; }; diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index 6c87c18fa9..a7c766e7fd 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,6 +1,6 @@ -import { BridgeState, KeygenEvent } from '../../../shared/daemon-rpc-types'; +import { BridgeState, IWireguardPublicKey, KeygenEvent } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; -import { IRelayLocationRedux, RelaySettingsRedux } from './reducers'; +import { IRelayLocationRedux, IWgKey, RelaySettingsRedux } from './reducers'; export interface IUpdateGuiSettingsAction { type: 'UPDATE_GUI_SETTINGS'; @@ -50,16 +50,21 @@ export interface IUpdateAutoStartAction { // Used to set wireguard key when accounts are changed. export interface IWireguardSetKey { type: 'SET_WIREGUARD_KEY'; - publicKey?: string; + key?: IWgKey; } export interface IWireguardGenerateKey { type: 'GENERATE_WIREGUARD_KEY'; } +export interface IWireguardReplaceKey { + type: 'REPLACE_WIREGUARD_KEY'; + oldKey: IWgKey; +} + export interface IWireguardVerifyKey { type: 'VERIFY_WIREGUARD_KEY'; - publicKey: string; + key: IWgKey; } export interface IWireguardKeygenEvent { @@ -69,7 +74,7 @@ export interface IWireguardKeygenEvent { export interface IWireguardKeyVerifiedAction { type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE'; - verified: boolean; + verified?: boolean; } export type SettingsAction = @@ -85,6 +90,7 @@ export type SettingsAction = | IWireguardSetKey | IWireguardVerifyKey | IWireguardGenerateKey + | IWireguardReplaceKey | IWireguardKeygenEvent | IWireguardKeyVerifiedAction; @@ -153,10 +159,17 @@ function updateAutoStart(autoStart: boolean): IUpdateAutoStartAction { }; } -function setWireguardKey(publicKey?: string): IWireguardSetKey { +function setWireguardKey(publicKey?: IWireguardPublicKey): IWireguardSetKey { + const key = publicKey + ? { + publicKey: publicKey.key, + created: publicKey.created, + valid: undefined, + } + : undefined; return { type: 'SET_WIREGUARD_KEY', - publicKey, + key, }; } @@ -173,14 +186,21 @@ function generateWireguardKey(): IWireguardGenerateKey { }; } -function verifyWireguardKey(publicKey: string): IWireguardVerifyKey { +function replaceWireguardKey(oldKey: IWgKey): IWireguardReplaceKey { + return { + type: 'REPLACE_WIREGUARD_KEY', + oldKey, + }; +} + +function verifyWireguardKey(key: IWgKey): IWireguardVerifyKey { return { type: 'VERIFY_WIREGUARD_KEY', - publicKey, + key, }; } -function completeWireguardKeyVerification(verified: boolean): IWireguardKeyVerifiedAction { +function completeWireguardKeyVerification(verified?: boolean): IWireguardKeyVerifiedAction { return { type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE', verified, @@ -200,6 +220,7 @@ export default { setWireguardKey, setWireguardKeygenEvent, generateWireguardKey, + replaceWireguardKey, verifyWireguardKey, completeWireguardKeyVerification, }; diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index e90022062c..5d7f37fb50 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -54,10 +54,17 @@ export interface IRelayLocationRedux { cities: IRelayLocationCityRedux[]; } -interface IWgKeySet { - type: 'key-set'; +export interface IWgKey { publicKey: string; + created: string; valid?: boolean; + replacementFailure?: KeygenEvent; + verificationFailed?: boolean; +} + +interface IWgKeySet { + type: 'key-set'; + key: IWgKey; } interface IWgKeyNotSet { @@ -76,9 +83,14 @@ interface IWgKeyBeingGenerated { type: 'being-generated'; } +interface IWgKeyBeingReplaced { + type: 'being-replaced'; + oldKey: IWgKey; +} + interface IWgKeyBeingVerified { type: 'being-verified'; - publicKey: string; + key: IWgKey; } export type WgKeyState = @@ -87,6 +99,7 @@ export type WgKeyState = | IWgKeyGenerationFailure | IWgTooManyKeys | IWgKeyBeingVerified + | IWgKeyBeingReplaced | IWgKeyBeingGenerated; export interface ISettingsReduxState { @@ -199,12 +212,12 @@ export default function( case 'SET_WIREGUARD_KEY': return { ...state, - wireguardKeyState: setWireguardKey(action.publicKey), + wireguardKeyState: setWireguardKey(action.key), }; case 'WIREGUARD_KEYGEN_EVENT': return { ...state, - wireguardKeyState: setWireguardKeygenEvent(action.event), + wireguardKeyState: setWireguardKeygenEvent(state, action.event), }; case 'WIREGUARD_KEY_VERIFICATION_COMPLETE': return { @@ -214,7 +227,7 @@ export default function( case 'VERIFY_WIREGUARD_KEY': return { ...state, - wireguardKeyState: { type: 'being-verified', publicKey: action.publicKey }, + wireguardKeyState: { type: 'being-verified', key: action.key }, }; case 'GENERATE_WIREGUARD_KEY': @@ -223,16 +236,22 @@ export default function( wireguardKeyState: { type: 'being-generated' }, }; + case 'REPLACE_WIREGUARD_KEY': + return { + ...state, + wireguardKeyState: { type: 'being-replaced', oldKey: action.oldKey }, + }; + default: return state; } } -function setWireguardKey(publicKey?: string): WgKeyState { - if (publicKey) { +function setWireguardKey(key?: IWgKey): WgKeyState { + if (key) { return { type: 'key-set', - publicKey, + key, }; } else { return { @@ -241,7 +260,23 @@ function setWireguardKey(publicKey?: string): WgKeyState { } } -function setWireguardKeygenEvent(keygenEvent: KeygenEvent): WgKeyState { +function setWireguardKeygenEvent(state: ISettingsReduxState, keygenEvent: KeygenEvent): WgKeyState { + const oldKeyState = state.wireguardKeyState; + if (oldKeyState.type === 'being-replaced') { + switch (keygenEvent) { + case 'too_many_keys': + case 'generation_failure': + return { + type: 'key-set', + key: { + ...oldKeyState.oldKey, + replacementFailure: keygenEvent, + }, + }; + default: + break; + } + } switch (keygenEvent) { case 'too_many_keys': return { type: 'too-many-keys' }; @@ -250,19 +285,26 @@ function setWireguardKeygenEvent(keygenEvent: KeygenEvent): WgKeyState { default: return { type: 'key-set', - publicKey: keygenEvent.newKey, - valid: true, + key: { + publicKey: keygenEvent.newKey.key, + created: keygenEvent.newKey.created, + valid: undefined, + }, }; } } -function applyKeyVerification(state: WgKeyState, verified: boolean): WgKeyState { +function applyKeyVerification(state: WgKeyState, verified?: boolean): WgKeyState { + const verificationFailed = verified === undefined ? true : undefined; switch (state.type) { case 'being-verified': return { - ...state, type: 'key-set', - valid: verified, + key: { + ...state.key, + valid: verified, + verificationFailed, + }, }; // drop the verification event if the key wasn't being verified. default: diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index f88cd29404..0f73586275 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -295,10 +295,16 @@ export interface ISettings { bridgeState: BridgeState; } -export type KeygenEvent = INewWireguardKey | 'too_many_keys' | 'generation_failure'; +export type KeygenEvent = INewWireguardKey | KeygenFailure; +export type KeygenFailure = 'too_many_keys' | 'generation_failure'; export interface INewWireguardKey { - newKey: string; + newKey: IWireguardPublicKey; +} + +export interface IWireguardPublicKey { + key: string; + created: string; } export type BridgeState = 'auto' | 'on' | 'off'; diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 8283cf43dc..7bdeb6e759 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -13,6 +13,7 @@ import { ILocation, IRelayList, ISettings, + IWireguardPublicKey, KeygenEvent, RelaySettingsUpdate, TunnelState, @@ -31,7 +32,7 @@ export interface IAppStateSnapshot { currentVersion: ICurrentAppVersionInfo; upgradeVersion: IAppUpgradeInfo; guiSettings: IGuiSettingsState; - wireguardPublicKey?: string; + wireguardPublicKey?: IWireguardPublicKey; } interface ISender<T> { @@ -114,13 +115,13 @@ interface IAutoStartHandlers extends ISender<boolean> { handleSet(fn: (value: boolean) => Promise<void>): void; } -interface IWireguardKeyMethods extends IReceiver<string | undefined> { +interface IWireguardKeyMethods extends IReceiver<IWireguardPublicKey | undefined> { listenKeygenEvents(fn: (event: KeygenEvent) => void): void; generateKey(): Promise<KeygenEvent>; verifyKey(): Promise<boolean>; } -interface IWireguardKeyHandlers extends ISender<string | undefined> { +interface IWireguardKeyHandlers extends ISender<IWireguardPublicKey | undefined> { notifyKeygenEvent(webContents: WebContents, event: KeygenEvent): void; handleGenerateKey(fn: () => Promise<KeygenEvent>): void; handleVerifyKey(fn: () => Promise<boolean>): void; @@ -340,7 +341,7 @@ export class IpcMainEventChannel { }; public static wireguardKeys: IWireguardKeyHandlers = { - notify: sender<string | undefined>(WIREGUARD_KEY_SET), + notify: sender<IWireguardPublicKey | undefined>(WIREGUARD_KEY_SET), notifyKeygenEvent: sender<KeygenEvent>(WIREGUARD_KEYGEN_EVENT), handleGenerateKey: requestHandler(GENERATE_WIREGUARD_KEY), handleVerifyKey: requestHandler(VERIFY_WIREGUARD_KEY), diff --git a/mullvad-daemon/src/account_history.rs b/mullvad-daemon/src/account_history.rs index 8455c060d2..7ba5894190 100644 --- a/mullvad-daemon/src/account_history.rs +++ b/mullvad-daemon/src/account_history.rs @@ -72,7 +72,11 @@ impl AccountHistory { Ok(accounts) => accounts, }; let file = io::BufWriter::new(reader.into_inner()); - Ok(AccountHistory { file, accounts }) + let mut history = AccountHistory { file, accounts }; + if let Err(e) = history.save_to_disk() { + log::error!("Failed to save account cache after opening it: {}", e); + } + Ok(history) } fn try_old_format(reader: &mut io::BufReader<fs::File>) -> Result<Vec<AccountToken>> { diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index eed31fa2fc..12b7919fd0 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -795,7 +795,7 @@ where match result { Ok(data) => { - let public_key = data.private_key.public_key(); + let public_key = data.get_public_key(); let mut account_entry = self .account_history .get(&account) @@ -1251,12 +1251,18 @@ where }) })?; - match self - .wireguard_key_manager - .generate_key_sync(account_token.clone()) - { + let gen_result = match &account_entry.wireguard { + Some(wireguard_data) => self + .wireguard_key_manager + .replace_key(account_token.clone(), wireguard_data.get_public_key()), + None => self + .wireguard_key_manager + .generate_key_sync(account_token.clone()), + }; + + match gen_result { Ok(new_data) => { - let public_key = new_data.private_key.public_key(); + let public_key = new_data.get_public_key(); account_entry.wireguard = Some(new_data.clone()); self.account_history.insert(account_entry).map_err(|e| { format!("Failed to add new wireguard key to account data: {}", e) @@ -1285,11 +1291,7 @@ where .settings .get_account_token() .and_then(|account| self.account_history.get(&account).ok()?) - .and_then(|account_entry| { - account_entry - .wireguard - .map(|wg| wg.private_key.public_key()) - }); + .and_then(|account_entry| account_entry.wireguard.map(|wg| wg.get_public_key())); Self::oneshot_send(tx, key, "get_wireguard_key response"); } diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 0940780ea3..e1b74f6185 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -19,7 +19,7 @@ use mullvad_types::{ relay_list::RelayList, settings::{self, Settings}, states::{TargetState, TunnelState}, - version, DaemonEvent, + version, wireguard, DaemonEvent, }; use parking_lot::{Mutex, RwLock}; use std::{ @@ -28,7 +28,7 @@ use std::{ }; use talpid_core::mpsc::IntoSender; use talpid_ipc; -use talpid_types::{net::wireguard, ErrorExt}; +use talpid_types::ErrorExt; use uuid; /// FIXME(linus): This is here just because the futures crate has deprecated it and jsonrpc_core @@ -133,7 +133,7 @@ build_rpc_trait! { /// Generates new wireguard key for current account #[rpc(meta, name = "generate_wireguard_key")] - fn generate_wireguard_key(&self, Self::Metadata) -> BoxFuture<mullvad_types::wireguard::KeygenEvent, Error>; + fn generate_wireguard_key(&self, Self::Metadata) -> BoxFuture<wireguard::KeygenEvent, Error>; /// Retrieve a public key for current account if the account has one. #[rpc(meta, name = "get_wireguard_key")] @@ -217,7 +217,7 @@ pub enum ManagementCommand { /// Get the daemon settings GetSettings(OneshotSender<Settings>), /// Generate new wireguard key - GenerateWireguardKey(OneshotSender<mullvad_types::wireguard::KeygenEvent>), + GenerateWireguardKey(OneshotSender<wireguard::KeygenEvent>), /// Return a public key of the currently set wireguard private key, if there is one GetWireguardKey(OneshotSender<Option<wireguard::PublicKey>>), /// Verify if the currently set wireguard key is valid. diff --git a/mullvad-daemon/src/wireguard.rs b/mullvad-daemon/src/wireguard.rs index 0b8157f712..623d2538fd 100644 --- a/mullvad-daemon/src/wireguard.rs +++ b/mullvad-daemon/src/wireguard.rs @@ -1,9 +1,13 @@ use crate::InternalDaemonEvent; +use chrono::offset::Utc; use futures::{future::Executor, sync::oneshot, Async, Future, Poll}; use jsonrpc_client_core::Error as JsonRpcError; -use mullvad_types::{account::AccountToken, wireguard::WireguardData}; +use mullvad_types::account::AccountToken; +pub use mullvad_types::wireguard::*; use std::{sync::mpsc, time::Duration}; -pub use talpid_types::net::wireguard::*; +pub use talpid_types::net::wireguard::{ + ConnectionConfig, PrivateKey, TunnelConfig, TunnelParameters, +}; use talpid_types::ErrorExt; use tokio_core::reactor::Remote; use tokio_retry::{ @@ -60,17 +64,33 @@ impl KeyManager { pub fn generate_key_sync(&mut self, account: AccountToken) -> Result<WireguardData> { self.reset(); let private_key = PrivateKey::new_from_random().map_err(Error::GenerationError)?; + + self.run_future_sync(self.push_future_generator(account, private_key)()) + .map_err(Self::map_rpc_error) + } + + pub fn run_future_sync<T: Send + 'static, E: Send + 'static>( + &mut self, + fut: impl Future<Item = T, Error = E> + Send + 'static, + ) -> std::result::Result<T, E> { + self.reset(); let (tx, rx) = oneshot::channel(); - let fut = self.push_future_generator(account, private_key)().then(|result| { + + let _ = self.tokio_remote.execute(fut.then(|result| { let _ = tx.send(result); Ok(()) - }); - self.tokio_remote - .execute(fut) - .map_err(|_e| Error::ExectuionError)?; + })); + rx.wait().unwrap() + } - rx.wait() - .map_err(|_| Error::ExectuionError)? + pub fn replace_key( + &mut self, + account: AccountToken, + old_key: PublicKey, + ) -> Result<WireguardData> { + self.reset(); + let new_key = PrivateKey::new_from_random().map_err(Error::GenerationError)?; + self.run_future_sync(self.replace_key_rpc(account, old_key, new_key)) .map_err(Self::map_rpc_error) } @@ -165,12 +185,29 @@ impl KeyManager { move |addresses| WireguardData { private_key: key, addresses, + created: Utc::now(), }, )) }; Box::new(push_future) } + fn replace_key_rpc( + &self, + account: AccountToken, + old_key: PublicKey, + new_key: PrivateKey, + ) -> impl Future<Item = WireguardData, Error = JsonRpcError> + Send { + let mut rpc = mullvad_rpc::WireguardKeyProxy::new(self.http_handle.clone()); + let new_public_key = new_key.public_key(); + rpc.replace_wg_key(account.clone(), old_key.key, new_public_key) + .map(move |addresses| WireguardData { + private_key: new_key, + addresses, + created: Utc::now(), + }) + } + fn map_rpc_error(err: jsonrpc_client_core::Error) -> Error { match err.kind() { // TODO: Consider handling the invalid account case too. diff --git a/mullvad-jni/src/daemon_interface.rs b/mullvad-jni/src/daemon_interface.rs index f80c645bb6..69d16d4034 100644 --- a/mullvad-jni/src/daemon_interface.rs +++ b/mullvad-jni/src/daemon_interface.rs @@ -8,10 +8,9 @@ use mullvad_types::{ settings::Settings, states::{TargetState, TunnelState}, version::AppVersionInfo, - wireguard::KeygenEvent, + wireguard::{self, KeygenEvent}, }; use parking_lot::Mutex; -use talpid_types::net::wireguard; #[derive(Debug, err_derive::Error)] pub enum Error { diff --git a/mullvad-jni/src/into_java.rs b/mullvad-jni/src/into_java.rs index b927014178..28a45c3630 100644 --- a/mullvad-jni/src/into_java.rs +++ b/mullvad-jni/src/into_java.rs @@ -546,7 +546,7 @@ impl<'env> IntoJava<'env> for KeygenEvent { match self { KeygenEvent::NewKey(public_key) => { let class = get_class("net/mullvad/mullvadvpn/model/KeygenEvent$NewKey"); - let java_public_key = env.auto_local(public_key.into_java(env)); + let java_public_key = env.auto_local(public_key.key.into_java(env)); let parameters = [ JValue::Object(java_public_key.as_obj()), diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index c8d267bb8b..91da16f484 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -428,7 +428,7 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_MullvadDaemon_getWireguardKey _: JObject<'this>, ) -> JObject<'env> { match DAEMON_INTERFACE.get_wireguard_key() { - Ok(public_key) => public_key.into_java(&env), + Ok(key) => key.map(|k| k.key).into_java(&env), Err(error) => { log::error!( "{}", diff --git a/mullvad-rpc/src/lib.rs b/mullvad-rpc/src/lib.rs index 6261ef9c10..a68dfea0fa 100644 --- a/mullvad-rpc/src/lib.rs +++ b/mullvad-rpc/src/lib.rs @@ -131,6 +131,12 @@ jsonrpc_client!(pub struct WireguardKeyProxy { account_token: AccountToken, public_key: wireguard::PublicKey ) -> RpcRequest<mullvad_types::wireguard::AssociatedAddresses>; + pub fn replace_wg_key( + &mut self, + account_token: AccountToken, + old_key: wireguard::PublicKey, + new_key: wireguard::PublicKey + ) -> RpcRequest<mullvad_types::wireguard::AssociatedAddresses>; pub fn check_wg_key( &mut self, account_token: AccountToken, diff --git a/mullvad-types/src/wireguard.rs b/mullvad-types/src/wireguard.rs index 635bb7d7da..8675a6e845 100644 --- a/mullvad-types/src/wireguard.rs +++ b/mullvad-types/src/wireguard.rs @@ -1,3 +1,4 @@ +use chrono::{offset::Utc, DateTime}; use serde::{Deserialize, Serialize}; use std::fmt; use talpid_types::net::wireguard; @@ -7,6 +8,25 @@ use talpid_types::net::wireguard; pub struct WireguardData { pub private_key: wireguard::PrivateKey, pub addresses: AssociatedAddresses, + #[serde(default = "Utc::now")] + pub created: DateTime<Utc>, +} + +impl WireguardData { + /// Create a public key + pub fn get_public_key(&self) -> PublicKey { + PublicKey { + key: self.private_key.public_key(), + created: self.created, + } + } +} + +/// Represents a published public key +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PublicKey { + pub key: wireguard::PublicKey, + pub created: DateTime<Utc>, } /// Contains a pair of local link addresses that are paired with a specific wireguard @@ -21,7 +41,7 @@ pub struct AssociatedAddresses { #[derive(Clone, Debug, Deserialize, Serialize)] /// Event that is emitted when the daemon has finished generating a key. pub enum KeygenEvent { - NewKey(wireguard::PublicKey), + NewKey(PublicKey), TooManyKeys, GenerationFailure, } @@ -29,7 +49,7 @@ pub enum KeygenEvent { impl fmt::Display for KeygenEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { - KeygenEvent::NewKey(public_key) => write!(f, "New wireguard key {}", public_key), + KeygenEvent::NewKey(new_key) => write!(f, "New wireguard key {}", new_key.key), KeygenEvent::TooManyKeys => write!(f, "Account has too many keys already"), KeygenEvent::GenerationFailure => write!(f, "Failed to generate new wireguard key"), } |
