import moment from 'moment'; import * as React from 'react'; import { sprintf } from 'sprintf-js'; import { TunnelState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { IWgKey, WgKeyState } from '../redux/settings/reducers'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import ClipboardLabel from './ClipboardLabel'; import ImageView from './ImageView'; import { Layout } from './Layout'; import { BackBarItem, NavigationBar, NavigationContainer, NavigationItems, TitleBarItem, } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import { StyledButtonRow, StyledContainer, StyledContent, StyledLastButtonRow, StyledMessage, StyledMessages, StyledNavigationScrollbars, StyledRow, StyledRowLabel, StyledRowLabelSpacer, StyledRowValue, } from './WireguardKeysStyles'; export interface IProps { keyState: WgKeyState; isOffline: boolean; locale: string; tunnelState: TunnelState; windowFocused: boolean; onClose: () => void; onGenerateKey: () => void; onReplaceKey: (old: IWgKey) => void; onVerifyKey: (publicKey: IWgKey) => void; onVisitWebsiteKey: () => Promise; } export interface IState { recentlyGeneratedKey: boolean; userHasInitiatedVerification: boolean; ageOfKeyString: string; } export default class WireguardKeys extends React.Component { public state = { recentlyGeneratedKey: false, userHasInitiatedVerification: false, ageOfKeyString: WireguardKeys.ageOfKeyString(this.props.keyState, this.props.locale), }; private keyAgeUpdateInterval?: number; public static getDerivedStateFromProps(props: IProps) { return { ageOfKeyString: WireguardKeys.ageOfKeyString(props.keyState, props.locale), }; } public componentDidMount() { this.verifyKey(); this.keyAgeUpdateInterval = setInterval(this.setAgeOfKeyStringState, 60 * 1000); } public componentWillUnmount() { clearInterval(this.keyAgeUpdateInterval); } public componentDidUpdate(prevProps: IProps) { const prevKey = prevProps.keyState.type === 'key-set' ? prevProps.keyState.key.publicKey : undefined; const key = this.props.keyState.type === 'key-set' ? this.props.keyState.key.publicKey : undefined; if (this.props.tunnelState.state === 'connected' && key !== undefined && key != prevKey) { this.setState({ recentlyGeneratedKey: true }); } if ( this.state.recentlyGeneratedKey && prevProps.tunnelState.state !== 'connected' && this.props.tunnelState.state === 'connected' ) { this.setState({ recentlyGeneratedKey: false }); } } public render() { return ( { // TRANSLATORS: Back button in navigation bar messages.pgettext('wireguard-keys-nav', 'Advanced') } { // TRANSLATORS: Title label in navigation bar messages.pgettext('wireguard-keys-nav', 'WireGuard key') } {messages.pgettext('wireguard-keys-nav', 'WireGuard key')} {messages.pgettext('wireguard-key-view', 'Public key')} {this.keyValidityLabel()} {this.getKeyText()} {messages.pgettext('wireguard-key-view', 'Key generated')} {this.state.ageOfKeyString} {this.getStatusMessage()} {this.getGenerateButton()} {messages.pgettext('wireguard-key-view', 'Verify key')} {messages.pgettext('wireguard-key-view', 'Manage keys')} ); } private isVerifyButtonDisabled(): boolean { return this.props.keyState.type !== 'key-set'; } private handleVerifyKeyPress = () => { this.setState({ userHasInitiatedVerification: true }); this.verifyKey(); }; private verifyKey() { 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}`); } } /// Action button can either generate or verify a key private getGenerateButton() { let buttonText = messages.pgettext('wireguard-key-view', 'Generate key'); const regenerateText = messages.pgettext('wireguard-key-view', 'Regenerate key'); let disabled = false; 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': disabled = true; buttonText = regenerateText; break; case 'being-replaced': case 'being-generated': disabled = true; buttonText = messages.pgettext('wireguard-key-view', 'Generating key'); } return ( {buttonText} ); } private getKeyText() { switch (this.props.keyState.type) { case 'being-verified': case 'key-set': { // mimicking the truncating of the key from website const publicKey = this.props.keyState.key.publicKey; return ( ); } case 'being-replaced': case 'being-generated': return ; default: return ( {messages.pgettext('wireguard-key-view', 'No key set')} ); } } private keyValidityLabel() { const keyStateType = this.props.keyState.type; if (keyStateType === 'being-verified' && this.state.userHasInitiatedVerification) { return ; } else if (this.props.keyState.type === 'key-set') { const valid = this.props.keyState.key.valid; const show = this.state.userHasInitiatedVerification || valid === false; return show && valid !== undefined ? ( {valid ? messages.pgettext('wireguard-key-view', 'Key is valid') : messages.pgettext('wireguard-key-view', 'Key is invalid')} ) : null; } else { return null; } } private static ageOfKeyString(keyState: WgKeyState, locale: string): string { switch (keyState.type) { case 'key-set': case 'being-verified': return moment(keyState.key.created).locale(locale).fromNow(); default: return '-'; } } private setAgeOfKeyStringState = () => { this.setState({ ageOfKeyString: WireguardKeys.ageOfKeyString(this.props.keyState, this.props.locale), }); }; private getStatusMessage() { if (this.props.isOffline && this.state.recentlyGeneratedKey) { return ( {messages.pgettext('wireguard-key-view', 'Reconnecting with new WireGuard key...')} ); } else { let message = ''; switch (this.props.keyState.type) { case 'key-set': { const key = this.props.keyState.key; if (key.replacementFailure) { switch (key.replacementFailure) { case 'too_many_keys': message = this.formatKeygenFailure('too-many-keys'); break; case 'generation_failure': message = this.formatKeygenFailure('generation-failure'); break; } } else if (key.verificationFailed) { message = messages.pgettext('wireguard-key-view', 'Key verification failed'); } break; } case 'too-many-keys': case 'generation-failure': message = this.formatKeygenFailure(this.props.keyState.type); break; } return {message}; } } private formatKeygenFailure(failure: 'too-many-keys' | 'generation-failure'): string { switch (failure) { case 'too-many-keys': // TRANSLATORS: "%(manage)" is replaced with the text in the "Manage keys" button. return sprintf( messages.pgettext( 'wireguard-key-view', 'Unable to regenerate key: you already have the maximum number of keys. To generate a new key, you first need to revoke one under “Manage keys.”', ), { manage: messages.pgettext('wireguard-key-view', 'Manage keys') }, ); case 'generation-failure': return messages.pgettext('wireguard-key-view', 'Failed to generate a key'); default: return failure; } } }