From 6ce8584337cb74695d4d8d711301b136468f323d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 23 Jun 2020 15:04:39 +0100 Subject: [PATCH 01/37] Implement first screen (recovery key / passphrase choice) --- .../_CreateSecretStorageDialog.scss | 18 ++++ .../views/elements/_StyledRadioButton.scss | 13 ++- res/img/feather-customised/secure-backup.svg | 11 ++ res/img/feather-customised/secure-phrase.svg | 11 ++ .../CreateSecretStorageDialog.js | 102 ++++++++++++++++-- .../views/elements/StyledRadioButton.tsx | 2 +- src/i18n/strings/en_EN.json | 6 ++ 7 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 res/img/feather-customised/secure-backup.svg create mode 100644 res/img/feather-customised/secure-phrase.svg diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 63e5a3de09..28df5039f1 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -59,6 +59,24 @@ limitations under the License. display: block; } +.mx_CreateSecretStorageDialog_primaryContainer .mx_RadioButton { + margin-bottom: 16px; + padding: 11px; +} + +.mx_CreateSecretStorageDialog_optionTitle { + color: $dialog-title-fg-color; + font-weight: 600; + font-size: $font-18px; +} + +.mx_CreateSecretStorageDialog_optionIcon { + width: 24px; + margin-right: 8px; + position: relative; + top: 5px; +} + .mx_CreateSecretStorageDialog_passPhraseContainer { display: flex; align-items: flex-start; diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss index c2edb359dc..eb73cec5b5 100644 --- a/res/css/views/elements/_StyledRadioButton.scss +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -25,16 +25,21 @@ limitations under the License. position: relative; display: flex; - align-items: center; + align-items: baseline; flex-grow: 1; - > span { + border: 1px solid $input-darker-bg-color; + border-radius: 8px; + + > .mx_RadioButton_content { flex-grow: 1; display: flex; margin-left: 8px; margin-right: 8px; + + flex-direction: column; } .mx_RadioButton_spacer { @@ -105,3 +110,7 @@ limitations under the License. } } } + +.mx_RadioButton_checked { + border-color: $accent-color; +} diff --git a/res/img/feather-customised/secure-backup.svg b/res/img/feather-customised/secure-backup.svg new file mode 100644 index 0000000000..c06f93c1fe --- /dev/null +++ b/res/img/feather-customised/secure-backup.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/feather-customised/secure-phrase.svg b/res/img/feather-customised/secure-phrase.svg new file mode 100644 index 0000000000..eb13d3f048 --- /dev/null +++ b/res/img/feather-customised/secure-phrase.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index d7b79c2cfa..08a1dc5d9e 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -26,20 +26,26 @@ import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; +import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton'; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; -const PHASE_MIGRATE = 2; -const PHASE_PASSPHRASE = 3; -const PHASE_PASSPHRASE_CONFIRM = 4; -const PHASE_SHOWKEY = 5; -const PHASE_KEEPITSAFE = 6; -const PHASE_STORING = 7; -const PHASE_DONE = 8; -const PHASE_CONFIRM_SKIP = 9; +const PHASE_CHOOSE_KEY_PASSPHRASE = 2; +const PHASE_MIGRATE = 3; +const PHASE_PASSPHRASE = 4; +const PHASE_PASSPHRASE_CONFIRM = 5; +const PHASE_SHOWKEY = 6; +const PHASE_KEEPITSAFE = 7; +const PHASE_STORING = 8; +const PHASE_DONE = 9; +const PHASE_CONFIRM_SKIP = 10; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. +// these end up as strings from being values in the radio buttons, so just use strings +const CREATESTORAGE_OPTION_KEY = 'key'; +const CREATESTORAGE_OPTION_PASSPHRASE = 'passphrase'; + /* * Walks the user through the process of creating a passphrase to guard Secure * Secret Storage in account data. @@ -79,6 +85,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { accountPasswordCorrect: null, // status of the key backup toggle switch useKeyBackup: true, + + passPhraseKeySelected: CREATESTORAGE_OPTION_KEY, }; this._passphraseField = createRef(); @@ -110,7 +118,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ); const { force } = this.props; - const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; this.setState({ phase, @@ -152,6 +160,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } + _onKeyPassphraseChange = e => { + this.setState({ + passPhraseKeySelected: e.target.value, + }); + } + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } @@ -162,6 +176,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _onChooseKeyPassphraseFormSubmit = async () => { + if (this.state.passPhraseKeySelected === CREATESTORAGE_OPTION_KEY) { + this._recoveryKey = + await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_SHOWKEY, + }); + } else { + this.setState({ + copied: false, + downloaded: false, + phase: PHASE_PASSPHRASE, + }); + } + } + _onMigrateFormSubmit = (e) => { e.preventDefault(); if (this.state.backupSigStatus.usable) { @@ -427,6 +459,53 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _renderPhaseChooseKeyPassphrase() { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Safeguard against losing access to encrypted messages & data by " + + "backing up encryption keys on your server.", + )}

+
+ +
+ + {_t("Generate a Security Key")} +
+
{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}
+
+ +
+ + {_t("Enter a Security Phrase")} +
+
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
+
+
+ + ; + } + _renderPhaseMigrate() { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. @@ -716,6 +795,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { + case PHASE_CHOOSE_KEY_PASSPHRASE: + return _t('Set up Secure backup'); case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: @@ -760,6 +841,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADERROR: content = this._renderPhaseLoadError(); break; + case PHASE_CHOOSE_KEY_PASSPHRASE: + content = this._renderPhaseChooseKeyPassphrase(); + break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index d7ae4d5af8..3b83666048 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -42,7 +42,7 @@ export default class StyledRadioButton extends React.PureComponent {/* Used to render the radio button circle */}
- {children} +
{children}
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 646f43af33..c7b05eafe4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2171,6 +2171,11 @@ "Import": "Import", "Confirm encryption setup": "Confirm encryption setup", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", + "Generate a Security Key": "Generate a Security Key", + "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.", + "Enter a Security Phrase": "Enter a Security Phrase", + "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Use a secret phrase only you know, and optionally save a Security Key to use for backup.", "Enter your account password to confirm the upgrade:": "Enter your account password to confirm the upgrade:", "Restore your key backup to upgrade your encryption": "Restore your key backup to upgrade your encryption", "Restore": "Restore", @@ -2200,6 +2205,7 @@ "Unable to query secret storage status": "Unable to query secret storage status", "Retry": "Retry", "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", + "Set up Secure backup": "Set up Secure backup", "Upgrade your encryption": "Upgrade your encryption", "Confirm recovery passphrase": "Confirm recovery passphrase", "Make a copy of your recovery key": "Make a copy of your recovery key", From 0694776b255da1a1da8cf6644273e590dee47e3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 23 Jun 2020 16:27:41 +0100 Subject: [PATCH 02/37] Update the 'save your security key' screen --- .../_CreateSecretStorageDialog.scss | 35 ++++--- .../CreateSecretStorageDialog.js | 99 ++++++------------- src/i18n/strings/en_EN.json | 22 +++-- 3 files changed, 66 insertions(+), 90 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 28df5039f1..0d06d503b0 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -91,33 +91,42 @@ limitations under the License. margin-left: 20px; } -.mx_CreateSecretStorageDialog_recoveryKeyHeader { - margin-bottom: 1em; -} - .mx_CreateSecretStorageDialog_recoveryKeyContainer { - display: flex; + width: 380px; + margin-left: auto; + margin-right: auto; } .mx_CreateSecretStorageDialog_recoveryKey { - width: 262px; + font-weight: bold; + text-align: center; padding: 20px; color: $info-plinth-fg-color; background-color: $info-plinth-bg-color; - margin-right: 12px; + border-radius: 6px; + word-spacing: 1em; + margin-bottom: 20px; } .mx_CreateSecretStorageDialog_recoveryKeyButtons { - flex: 1; display: flex; + justify-content: space-between; align-items: center; } .mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { - margin-right: 10px; -} - -.mx_CreateSecretStorageDialog_recoveryKeyButtons button { - flex: 1; + width: 160px; + padding-left: 0px; + padding-right: 0px; white-space: nowrap; } + +.mx_CreateSecretStorageDialog_continueSpinner { + margin-top: 33px; + text-align: right; +} + +.mx_CreateSecretStorageDialog_continueSpinner img { + width: 20px; + height: 20px; +} diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 08a1dc5d9e..7175f27e9f 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -27,6 +27,9 @@ import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; import PassphraseField from "../../../../components/views/auth/PassphraseField"; import StyledRadioButton from '../../../../components/views/elements/StyledRadioButton'; +import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; +import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -35,7 +38,6 @@ const PHASE_MIGRATE = 3; const PHASE_PASSPHRASE = 4; const PHASE_PASSPHRASE_CONFIRM = 5; const PHASE_SHOWKEY = 6; -const PHASE_KEEPITSAFE = 7; const PHASE_STORING = 8; const PHASE_DONE = 9; const PHASE_CONFIRM_SKIP = 10; @@ -208,7 +210,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (successful) { this.setState({ copied: true, - phase: PHASE_KEEPITSAFE, }); } } @@ -221,7 +222,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ downloaded: true, - phase: PHASE_KEEPITSAFE, }); } @@ -374,6 +374,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._fetchBackupInfo(); } + _onShowKeyContinueClick = () => { + this._bootstrapSecretStorage(); + } + _onSkipSetupClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } @@ -429,12 +433,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _onKeepItSafeBackClick = () => { - this.setState({ - phase: PHASE_SHOWKEY, - }); - } - _onPassPhraseValidate = (result) => { this.setState({ passPhraseValid: result.valid, @@ -460,8 +458,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseChooseKeyPassphrase() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "Safeguard against losing access to encrypted messages & data by " + @@ -512,7 +508,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // Once we're confident enough in this (and it's supported enough) we can do // it automatically. // https://github.com/vector-im/riot-web/issues/11696 - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Field = sdk.getComponent('views.elements.Field'); let authPrompt; @@ -561,8 +556,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhasePassPhrase() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); return @@ -614,7 +607,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhasePassPhraseConfirm() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const Field = sdk.getComponent('views.elements.Field'); let matchText; @@ -645,7 +637,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

; } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "Enter your recovery passphrase a second time to confirm it.", @@ -679,66 +670,48 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseShowKey() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + let continueButton; + if (this.state.phase === PHASE_SHOWKEY) { + continueButton = ; + } else { + continueButton =

+ +
; + } return

{_t( - "Your recovery key is a safety net - you can use it to restore " + - "access to your encrypted messages if you forget your recovery passphrase.", - )}

-

{_t( - "Keep a copy of it somewhere secure, like a password manager or even a safe.", + "Store your Security Key somewhere safe, like a password manager or a safe, " + + "as it’s used to safeguard your encrypted data.", )}

-
- {_t("Your recovery key")} -
{this._recoveryKey.encodedPrivateKey}
+ + {_t("Download")} + + {_t("or")} - {_t("Copy")} - - - {_t("Download")} + {this.state.copied ? _t("Copied!") : _t("Copy")}
-
; - } - - _renderPhaseKeepItSafe() { - let introText; - if (this.state.copied) { - introText = _t( - "Your recovery key has been copied to your clipboard, paste it to:", - {}, {b: s => {s}}, - ); - } else if (this.state.downloaded) { - introText = _t( - "Your recovery key is in your Downloads folder.", - {}, {b: s => {s}}, - ); - } - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
- {introText} -
    -
  • {_t("Print it and store it somewhere safe", {}, {b: s => {s}})}
  • -
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • -
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • -
- - - + {continueButton}
; } @@ -750,7 +723,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseLoadError() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t("Unable to query secret storage status")}

@@ -764,7 +736,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseDone() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "You can now verify your other devices, " + @@ -778,7 +749,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseSkipConfirm() { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( "Without completing security on this session, it won’t have " + @@ -806,8 +776,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: - case PHASE_KEEPITSAFE: - return _t('Make a copy of your recovery key'); + return _t('Save your Security Key'); case PHASE_STORING: return _t('Setting up keys'); case PHASE_DONE: @@ -822,7 +791,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let content; if (this.state.error) { - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); content =

{_t("Unable to set up secret storage")}

@@ -856,9 +824,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_SHOWKEY: content = this._renderPhaseShowKey(); break; - case PHASE_KEEPITSAFE: - content = this._renderPhaseKeepItSafe(); - break; case PHASE_STORING: content = this._renderBusyPhase(); break; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c7b05eafe4..385125a33d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2192,33 +2192,35 @@ "Go back to set it again.": "Go back to set it again.", "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", "Confirm your recovery passphrase": "Confirm your recovery passphrase", - "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", - "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", - "Your recovery key": "Your recovery key", - "Copy": "Copy", + "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.", "Download": "Download", - "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", - "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", - "Print it and store it somewhere safe": "Print it and store it somewhere safe", - "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", - "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", + "Copy": "Copy", "Unable to query secret storage status": "Unable to query secret storage status", "Retry": "Retry", "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", "Set up Secure backup": "Set up Secure backup", "Upgrade your encryption": "Upgrade your encryption", "Confirm recovery passphrase": "Confirm recovery passphrase", - "Make a copy of your recovery key": "Make a copy of your recovery key", + "Save your Security Key": "Save your Security Key", "You're done!": "You're done!", "Unable to set up secret storage": "Unable to set up secret storage", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", + "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", + "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", + "Your recovery key": "Your recovery key", + "Your recovery key has been copied to your clipboard, paste it to:": "Your recovery key has been copied to your clipboard, paste it to:", + "Your recovery key is in your Downloads folder.": "Your recovery key is in your Downloads folder.", + "Print it and store it somewhere safe": "Print it and store it somewhere safe", + "Save it on a USB key or backup drive": "Save it on a USB key or backup drive", + "Copy it to your personal cloud storage": "Copy it to your personal cloud storage", "Your keys are being backed up (the first backup could take a few minutes).": "Your keys are being backed up (the first backup could take a few minutes).", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.", "Set up Secure Message Recovery": "Set up Secure Message Recovery", "Secure your backup with a recovery passphrase": "Secure your backup with a recovery passphrase", + "Make a copy of your recovery key": "Make a copy of your recovery key", "Starting backup...": "Starting backup...", "Success!": "Success!", "Create key backup": "Create key backup", From 3716f9d82c95a01639a9162d3674573c17768b1a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 23 Jun 2020 16:43:52 +0100 Subject: [PATCH 03/37] Fix cancel button / prompt --- .../CreateSecretStorageDialog.js | 22 ++++++++++--------- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 7175f27e9f..e81d017d7f 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -378,12 +378,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._bootstrapSecretStorage(); } - _onSkipSetupClick = () => { + _onCancelClick = () => { this.setState({phase: PHASE_CONFIRM_SKIP}); } - _onSetUpClick = () => { - this.setState({phase: PHASE_PASSPHRASE}); + _onGoBackClick = () => { + this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE}); } _onSkipPassPhraseClick = async () => { @@ -496,7 +496,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ; @@ -750,15 +750,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhaseSkipConfirm() { return
- {_t( - "Without completing security on this session, it won’t have " + - "access to encrypted messages.", - )} +

{_t( + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", + )}

+

{_t( + "You can also set up Secure Backup & manage your keys in Settings.", + )}

- +
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 385125a33d..0b7a261e45 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2198,6 +2198,8 @@ "Unable to query secret storage status": "Unable to query secret storage status", "Retry": "Retry", "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", + "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", + "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", "Set up Secure backup": "Set up Secure backup", "Upgrade your encryption": "Upgrade your encryption", "Confirm recovery passphrase": "Confirm recovery passphrase", From bf15e96a6a98764b2c8c12b92e82f4c332074171 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Jun 2020 12:43:56 +0100 Subject: [PATCH 04/37] Make pasphrase screen look more like designs Although passphrase / passphrase confirm is still split between two screens because that's more work to change and probably is not a pivotal part of the UI that needs to change in step with everything else. --- .../CreateSecretStorageDialog.js | 59 ++++--------------- src/i18n/strings/en_EN.json | 8 +-- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index e81d017d7f..bc27efea46 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -85,8 +85,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { canUploadKeysWithPasswordOnly: null, accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - // status of the key backup toggle switch - useKeyBackup: true, passPhraseKeySelected: CREATESTORAGE_OPTION_KEY, }; @@ -172,12 +170,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._recoveryKeyNode = n; } - _onUseKeyBackupChange = (enabled) => { - this.setState({ - useKeyBackup: enabled, - }); - } - _onChooseKeyPassphraseFormSubmit = async () => { if (this.state.passPhraseKeySelected === CREATESTORAGE_OPTION_KEY) { this._recoveryKey = @@ -291,22 +283,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, - setupNewKeyBackup: this.state.useKeyBackup, + setupNewKeyBackup: true, setupNewSecretStorage: true, }); - if (!this.state.useKeyBackup && this.state.backupInfo) { - // If the user is resetting their cross-signing keys and doesn't want - // key backup (but had it enabled before), delete the key backup as it's - // no longer valid. - console.log("Deleting invalid key backup (secrets have been reset; key backup not requested)"); - await cli.deleteKeyBackupVersion(this.state.backupInfo.version); - } } else { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, - setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + setupNewKeyBackup: !this.state.backupInfo, getKeyBackupPassphrase: () => { // We may already have the backup key if we earlier went // through the restore backup path, so pass it along @@ -386,16 +371,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({phase: PHASE_CHOOSE_KEY_PASSPHRASE}); } - _onSkipPassPhraseClick = async () => { - this._recoveryKey = - await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this.setState({ - copied: false, - downloaded: false, - phase: PHASE_SHOWKEY, - }); - } - _onPassPhraseNextClick = async (e) => { e.preventDefault(); if (!this._passphraseField.current) return; // unmounting @@ -548,7 +523,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={false} primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword} > -
@@ -556,12 +531,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhasePassPhrase() { - const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - return

{_t( - "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + - "This should be different to your account password:", + "Enter a security phrase only you know, as it’s used to safeguard your data. " + + "To be secure, you shouldn’t re-use your account password.", )}

@@ -580,11 +553,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { />
- - + >{_t("Cancel")} - -
- {_t("Advanced")} - - {_t("Set up with a recovery key")} - -
; } @@ -662,7 +623,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { disabled={this.state.passPhrase !== this.state.passPhraseConfirm} > @@ -772,9 +733,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_MIGRATE: return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: - return _t('Set up encryption'); + return _t('Set a Security Phrase'); case PHASE_PASSPHRASE_CONFIRM: - return _t('Confirm recovery passphrase'); + return _t('Confirm Security Phrase'); case PHASE_CONFIRM_SKIP: return _t('Are you sure?'); case PHASE_SHOWKEY: diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0b7a261e45..7d67fc488a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2181,11 +2181,9 @@ "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", - "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:": "Set a recovery passphrase to secure encrypted information and recover it if you log out. This should be different to your account password:", + "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.", "Enter a recovery passphrase": "Enter a recovery passphrase", "Great! This recovery passphrase looks strong enough.": "Great! This recovery passphrase looks strong enough.", - "Back up encrypted message keys": "Back up encrypted message keys", - "Set up with a recovery key": "Set up with a recovery key", "That matches!": "That matches!", "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", @@ -2202,12 +2200,14 @@ "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", "Set up Secure backup": "Set up Secure backup", "Upgrade your encryption": "Upgrade your encryption", - "Confirm recovery passphrase": "Confirm recovery passphrase", + "Set a Security Phrase": "Set a Security Phrase", + "Confirm Security Phrase": "Confirm Security Phrase", "Save your Security Key": "Save your Security Key", "You're done!": "You're done!", "Unable to set up secret storage": "Unable to set up secret storage", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", + "Set up with a recovery key": "Set up with a recovery key", "Please enter your recovery passphrase a second time to confirm.": "Please enter your recovery passphrase a second time to confirm.", "Repeat your recovery passphrase...": "Repeat your recovery passphrase...", "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.": "Your recovery key is a safety net - you can use it to restore access to your encrypted messages if you forget your recovery passphrase.", From a23b784e005bd8098edee6dca76af368d69b6bd1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Jun 2020 15:21:09 +0100 Subject: [PATCH 05/37] Enable continue button if a passphrase has been set --- .../views/dialogs/secretstorage/CreateSecretStorageDialog.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index bc27efea46..14f5764927 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -78,6 +78,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { passPhraseConfirm: '', copied: false, downloaded: false, + setPassphrase: false, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -177,6 +178,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ copied: false, downloaded: false, + setPassphrase: false, phase: PHASE_SHOWKEY, }); } else { @@ -395,6 +397,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ copied: false, downloaded: false, + setPassphrase: true, phase: PHASE_SHOWKEY, }); } @@ -634,7 +637,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let continueButton; if (this.state.phase === PHASE_SHOWKEY) { continueButton = ; From 966837232c7b969e5cccabcb9d6e056de840013f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Jun 2020 16:12:46 +0100 Subject: [PATCH 06/37] Add header icons & justification --- res/css/_common.scss | 2 +- .../_CreateSecretStorageDialog.scss | 4 ++++ .../CreateSecretStorageDialog.js | 20 +++++++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index e83c6aaeda..5f5a6d6999 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -319,7 +319,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_Dialog_titleImage { - vertical-align: middle; + vertical-align: sub; width: 25px; height: 25px; margin-left: -2px; diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 0d06d503b0..c591973c94 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -48,6 +48,10 @@ limitations under the License. margin-bottom: 1em; } +.mx_CreateSecretStorageDialog_centeredTitle, .mx_CreateSecretStorageDialog_centeredBody { + text-align: center; +} + .mx_CreateSecretStorageDialog_primaryContainer { /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ padding-top: 20px; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 14f5764927..936043b770 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -437,7 +437,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhaseChooseKeyPassphrase() { return
-

{_t( +

{_t( "Safeguard against losing access to encrypted messages & data by " + "backing up encryption keys on your server.", )}

@@ -802,15 +802,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - let headerImage; - if (this._titleForPhase(this.state.phase)) { - headerImage = require("../../../../../res/img/e2e/normal.svg"); + let headerImage = null; + switch (this.state.phase) { + case PHASE_PASSPHRASE: + case PHASE_PASSPHRASE_CONFIRM: + headerImage = require("../../../../../res/img/feather-customised/secure-phrase.svg"); + break; + case PHASE_SHOWKEY: + headerImage = require("../../../../../res/img/feather-customised/secure-backup.svg"); + break; + } + + let titleClass = null; + if (this.state.phase === PHASE_CHOOSE_KEY_PASSPHRASE) { + titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; } return ( Date: Wed, 24 Jun 2020 16:55:35 +0100 Subject: [PATCH 07/37] Remove the "You're done" screen --- .../CreateSecretStorageDialog.js | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 936043b770..b4b99f2205 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -39,7 +39,6 @@ const PHASE_PASSPHRASE = 4; const PHASE_PASSPHRASE_CONFIRM = 5; const PHASE_SHOWKEY = 6; const PHASE_STORING = 8; -const PHASE_DONE = 9; const PHASE_CONFIRM_SKIP = 10; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. @@ -305,9 +304,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }, }); } - this.setState({ - phase: PHASE_DONE, - }); + this.props.onFinished(true); } catch (e) { if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { this.setState({ @@ -699,19 +696,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } - _renderPhaseDone() { - return
-

{_t( - "You can now verify your other devices, " + - "and other users to keep your chats safe.", - )}

- -
; - } - _renderPhaseSkipConfirm() { return

{_t( @@ -745,8 +729,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return _t('Save your Security Key'); case PHASE_STORING: return _t('Setting up keys'); - case PHASE_DONE: - return _t("You're done!"); default: return ''; } @@ -793,9 +775,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_STORING: content = this._renderBusyPhase(); break; - case PHASE_DONE: - content = this._renderPhaseDone(); - break; case PHASE_CONFIRM_SKIP: content = this._renderPhaseSkipConfirm(); break; From 2b144a846a91db048f1d31d4102ed4ffc37b7787 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 12:44:15 +0100 Subject: [PATCH 08/37] Apply some of the newer styling to passphrase / recovery key entry --- .../_AccessSecretStorageDialog.scss | 5 -- .../AccessSecretStorageDialog.js | 66 +++++-------------- src/i18n/strings/en_EN.json | 14 ++-- 3 files changed, 24 insertions(+), 61 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss index db11e91bdb..785eb85374 100644 --- a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -19,11 +19,6 @@ limitations under the License. height: 30px; } -.mx_AccessSecretStorageDialog_primaryContainer { - /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; -} - .mx_AccessSecretStorageDialog_passPhraseInput, .mx_AccessSecretStorageDialog_recoveryKeyInput { width: 300px; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index e2ceadfbb9..9d443b49ab 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -118,10 +118,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent { let content; let title; + let headerImage; if (hasPassphrase && !this.state.forceRecoveryKey) { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - title = _t("Enter recovery passphrase"); + title = _t("Security Phrase"); + headerImage = require("../../../../../res/img/feather-customised/secure-phrase.svg"); let keyStatus; if (this.state.keyMatches === false) { @@ -137,12 +139,15 @@ export default class AccessSecretStorageDialog extends React.PureComponent { content =

{_t( - "Warning: You should only do this on a trusted computer.", {}, - { b: sub => {sub} }, - )}

-

{_t( - "Access your secure message history and your cross-signing " + - "identity for verifying other sessions by entering your recovery passphrase.", + "Enter your Security Phrase or to continue.", {}, + { + button: s => + {s} + , + }, )}

@@ -156,7 +161,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { /> {keyStatus} - {_t( - "If you've forgotten your recovery passphrase you can "+ - "use your recovery key or " + - "set up new recovery options." - , {}, { - button1: s => - {s} - , - button2: s => - {s} - , - })}
; } else { - title = _t("Enter recovery key"); + title = _t("Security Key"); + headerImage = require("../../../../../res/img/feather-customised/secure-backup.svg"); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let keyStatus; if (this.state.recoveryKey.length === 0) { @@ -209,14 +196,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } content =
-

{_t( - "Warning: You should only do this on a trusted computer.", {}, - { b: sub => {sub} }, - )}

-

{_t( - "Access your secure message history and your cross-signing " + - "identity for verifying other sessions by entering your recovery key.", - )}

+

{_t("Use your Security Key to continue.")}

{keyStatus} - {_t( - "If you've forgotten your recovery key you can "+ - "." - , {}, { - button: s => - {s} - , - })}
; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d67fc488a..d551772e77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1782,16 +1782,14 @@ "Remember my selection for this widget": "Remember my selection for this widget", "Allow": "Allow", "Deny": "Deny", - "Enter recovery passphrase": "Enter recovery passphrase", + "Security Phrase": "Security Phrase", "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.", - "Warning: You should only do this on a trusted computer.": "Warning: You should only do this on a trusted computer.", - "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.", - "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options.": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options.", - "Enter recovery key": "Enter recovery key", + "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", + "Security Key": "Security Key", "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.", "This looks like a valid recovery key!": "This looks like a valid recovery key!", "Not a valid recovery key": "Not a valid recovery key", - "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.": "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery key.", + "Use your Security Key to continue.": "Use your Security Key to continue.", "If you've forgotten your recovery key you can .": "If you've forgotten your recovery key you can .", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", @@ -1806,9 +1804,11 @@ "Keys restored": "Keys restored", "Failed to decrypt %(failedCount)s sessions!": "Failed to decrypt %(failedCount)s sessions!", "Successfully restored %(sessionCount)s keys": "Successfully restored %(sessionCount)s keys", + "Enter recovery passphrase": "Enter recovery passphrase", "Warning: you should only set up key backup from a trusted computer.": "Warning: you should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", + "Enter recovery key": "Enter recovery key", "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.", "If you've forgotten your recovery key you can ": "If you've forgotten your recovery key you can ", @@ -2195,7 +2195,6 @@ "Copy": "Copy", "Unable to query secret storage status": "Unable to query secret storage status", "Retry": "Retry", - "You can now verify your other devices, and other users to keep your chats safe.": "You can now verify your other devices, and other users to keep your chats safe.", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", "Set up Secure backup": "Set up Secure backup", @@ -2203,7 +2202,6 @@ "Set a Security Phrase": "Set a Security Phrase", "Confirm Security Phrase": "Confirm Security Phrase", "Save your Security Key": "Save your Security Key", - "You're done!": "You're done!", "Unable to set up secret storage": "Unable to set up secret storage", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.", "For maximum security, this should be different from your account password.": "For maximum security, this should be different from your account password.", From 29cdebb611124c14e8a80d79169f76778e974ab8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 13:26:32 +0100 Subject: [PATCH 09/37] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d551772e77..1c3c61113a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1790,7 +1790,6 @@ "This looks like a valid recovery key!": "This looks like a valid recovery key!", "Not a valid recovery key": "Not a valid recovery key", "Use your Security Key to continue.": "Use your Security Key to continue.", - "If you've forgotten your recovery key you can .": "If you've forgotten your recovery key you can .", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", From 0acb35dc23b2d14d6782bce624017ee6f1b0f13b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 13:48:11 +0100 Subject: [PATCH 10/37] Update end to end tests --- test/end-to-end-tests/src/usecases/signup.js | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index aa9f6b7efa..2772e89fb8 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -79,35 +79,19 @@ module.exports = async function signup(session, username, password, homeserver) const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit'); await acceptButton.click(); - //plow through cross-signing setup by entering arbitrary details - //TODO: It's probably important for the tests to know the passphrase - const xsigningPassphrase = 'a7eaXcjpa9!Yl7#V^h$B^%dovHUVX'; // https://xkcd.com/221/ - let passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); - await session.replaceInputText(passphraseField, xsigningPassphrase); - await session.delay(1000); // give it a second to analyze our passphrase for security + // Continue with the default (generate a security key) let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await xsignContButton.click(); - //repeat passphrase entry - passphraseField = await session.query('.mx_CreateSecretStorageDialog_passPhraseField input'); - await session.replaceInputText(passphraseField, xsigningPassphrase); - await session.delay(1000); // give it a second to analyze our passphrase for security - xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); - await xsignContButton.click(); - //ignore the recovery key //TODO: It's probably important for the tests to know the recovery key const copyButton = await session.query('.mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn'); await copyButton.click(); //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); + const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await copyContinueButton.click(); - //acknowledge that we're done cross-signing setup and our keys are safe - const doneOkButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_primary'); - await doneOkButton.click(); - //wait for registration to finish so the hash gets set //onhashchange better? From 65febd24eb5a706948244c13670d94cebe63c546 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 13:52:38 +0100 Subject: [PATCH 11/37] lint --- test/end-to-end-tests/src/usecases/signup.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 2772e89fb8..fd41ef1a71 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -80,7 +80,7 @@ module.exports = async function signup(session, username, password, homeserver) await acceptButton.click(); // Continue with the default (generate a security key) - let xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); await xsignContButton.click(); //ignore the recovery key @@ -89,7 +89,9 @@ module.exports = async function signup(session, username, password, homeserver) await copyButton.click(); //acknowledge that we copied the recovery key to a safe place - const copyContinueButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary'); + const copyContinueButton = await session.query( + '.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary', + ); await copyContinueButton.click(); //wait for registration to finish so the hash gets set From 648c0c28c2b78521e1b3d9d0987b03cb49a4429c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Jun 2020 15:36:06 +0100 Subject: [PATCH 12/37] Add placeholder to security phrase input --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 9d443b49ab..4196256617 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -158,6 +158,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { value={this.state.passPhrase} autoFocus={true} autoComplete="new-password" + placeholder={_t("Security Phrase")} /> {keyStatus} Date: Thu, 25 Jun 2020 16:33:07 +0100 Subject: [PATCH 13/37] Remove unused code No reset option here anymore --- .../dialogs/secretstorage/AccessSecretStorageDialog.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 4196256617..b76c62323b 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -21,7 +21,6 @@ import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import { _t } from '../../../../languageHandler'; -import { accessSecretStorage } from '../../../../CrossSigningManager'; /* * Access Secure Secret Storage by requesting the user's passphrase. @@ -55,12 +54,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } - _onResetRecoveryClick = () => { - // Re-enter the access flow, but resetting storage this time around. - this.props.onFinished(false); - accessSecretStorage(() => {}, /* forceReset = */ true); - } - _onRecoveryKeyChange = (e) => { this.setState({ recoveryKey: e.target.value, From bf45cb05880f0da6f56ba08cf43bb026dfffd0f3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 11:24:07 +0100 Subject: [PATCH 14/37] PR feedback: re-order CSS & add underscore --- .../views/elements/_StyledRadioButton.scss | 3 +-- .../CreateSecretStorageDialog.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/res/css/views/elements/_StyledRadioButton.scss b/res/css/views/elements/_StyledRadioButton.scss index eb73cec5b5..17a063593f 100644 --- a/res/css/views/elements/_StyledRadioButton.scss +++ b/res/css/views/elements/_StyledRadioButton.scss @@ -35,11 +35,10 @@ limitations under the License. flex-grow: 1; display: flex; + flex-direction: column; margin-left: 8px; margin-right: 8px; - - flex-direction: column; } .mx_RadioButton_spacer { diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index b4b99f2205..bd751f7e74 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -44,8 +44,8 @@ const PHASE_CONFIRM_SKIP = 10; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. // these end up as strings from being values in the radio buttons, so just use strings -const CREATESTORAGE_OPTION_KEY = 'key'; -const CREATESTORAGE_OPTION_PASSPHRASE = 'passphrase'; +const CREATE_STORAGE_OPTION_KEY = 'key'; +const CREATE_STORAGE_OPTION_PASSPHRASE = 'passphrase'; /* * Walks the user through the process of creating a passphrase to guard Secure @@ -86,7 +86,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { accountPassword: props.accountPassword || "", accountPasswordCorrect: null, - passPhraseKeySelected: CREATESTORAGE_OPTION_KEY, + passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY, }; this._passphraseField = createRef(); @@ -171,7 +171,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onChooseKeyPassphraseFormSubmit = async () => { - if (this.state.passPhraseKeySelected === CREATESTORAGE_OPTION_KEY) { + if (this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY) { this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); this.setState({ @@ -440,10 +440,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { )}

{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}
Date: Fri, 26 Jun 2020 12:41:24 +0100 Subject: [PATCH 15/37] Convert icons to masks so they're a sensible colour in other themes --- .../_CreateSecretStorageDialog.scss | 30 +++++++++++++++++++ .../CreateSecretStorageDialog.js | 23 +++++--------- src/components/views/dialogs/BaseDialog.js | 8 +++-- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index c591973c94..b073bac93c 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -48,6 +48,25 @@ limitations under the License. margin-bottom: 1em; } +.mx_CreateSecretStorageDialog_titleWithIcon::before { + content: ''; + display: inline-block; + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + top: 5px; + background-color: $primary-fg-color; +} + +.mx_CreateSecretStorageDialog_secureBackupTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); +} + +.mx_CreateSecretStorageDialog_securePhraseTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + .mx_CreateSecretStorageDialog_centeredTitle, .mx_CreateSecretStorageDialog_centeredBody { text-align: center; } @@ -75,10 +94,21 @@ limitations under the License. } .mx_CreateSecretStorageDialog_optionIcon { + display: inline-block; width: 24px; + height: 24px; margin-right: 8px; position: relative; top: 5px; + background-color: $primary-fg-color; +} + +.mx_CreateSecretStorageDialog_optionIcon_securePhrase { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + +.mx_CreateSecretStorageDialog_optionIcon_secureBackup { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); } .mx_CreateSecretStorageDialog_passPhraseContainer { diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index bd751f7e74..ebef30ad1f 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -446,9 +446,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY} >
- + {_t("Generate a Security Key")}
{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}
@@ -460,9 +458,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE} >
- + {_t("Enter a Security Phrase")}
{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}
@@ -781,20 +777,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } - let headerImage = null; + let titleClass = null; switch (this.state.phase) { case PHASE_PASSPHRASE: case PHASE_PASSPHRASE_CONFIRM: - headerImage = require("../../../../../res/img/feather-customised/secure-phrase.svg"); + titleClass = ['mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_securePhraseTitle']; break; case PHASE_SHOWKEY: - headerImage = require("../../../../../res/img/feather-customised/secure-backup.svg"); + titleClass = ['mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_secureBackupTitle']; + break; + case PHASE_CHOOSE_KEY_PASSPHRASE: + titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; break; - } - - let titleClass = null; - if (this.state.phase === PHASE_CHOOSE_KEY_PASSPHRASE) { - titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; } return ( @@ -802,7 +796,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { onFinished={this.props.onFinished} title={this._titleForPhase(this.state.phase)} titleClass={titleClass} - headerImage={headerImage} hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} > diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index e59b6bbaf5..353298032c 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -75,8 +75,12 @@ export default createReactClass({ // If provided, this is used to add a aria-describedby attribute contentId: PropTypes.string, - // optional additional class for the title element - titleClass: PropTypes.string, + // optional additional class for the title element (basically anything that can be passed to classnames) + titleClass: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.string), + ]), }, getDefaultProps: function() { From 919c3bd360b37f61221cf299672a4e956e567c6d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 12:43:28 +0100 Subject: [PATCH 16/37] lint --- .../dialogs/secretstorage/CreateSecretStorageDialog.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index ebef30ad1f..58b5b57354 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -781,10 +781,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { switch (this.state.phase) { case PHASE_PASSPHRASE: case PHASE_PASSPHRASE_CONFIRM: - titleClass = ['mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_securePhraseTitle']; + titleClass = [ + 'mx_CreateSecretStorageDialog_titleWithIcon', + 'mx_CreateSecretStorageDialog_securePhraseTitle', + ]; break; case PHASE_SHOWKEY: - titleClass = ['mx_CreateSecretStorageDialog_titleWithIcon', 'mx_CreateSecretStorageDialog_secureBackupTitle']; + titleClass = [ + 'mx_CreateSecretStorageDialog_titleWithIcon', + 'mx_CreateSecretStorageDialog_secureBackupTitle', + ]; break; case PHASE_CHOOSE_KEY_PASSPHRASE: titleClass = 'mx_CreateSecretStorageDialog_centeredTitle'; From 178cbca934198ad5f37c440786a7970be9bda8db Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 12:56:41 +0100 Subject: [PATCH 17/37] Use mask images in key entry dialogs --- .../_AccessSecretStorageDialog.scss | 19 +++++++++++++++++++ .../AccessSecretStorageDialog.js | 8 ++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss index 785eb85374..f15d43b199 100644 --- a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -15,6 +15,25 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_AccessSecretStorageDialog_titleWithIcon::before { + content: ''; + display: inline-block; + width: 24px; + height: 24px; + margin-right: 8px; + position: relative; + top: 5px; + background-color: $primary-fg-color; +} + +.mx_AccessSecretStorageDialog_secureBackupTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-backup.svg'); +} + +.mx_AccessSecretStorageDialog_securePhraseTitle::before { + mask-image: url('$(res)/img/feather-customised/secure-phrase.svg'); +} + .mx_AccessSecretStorageDialog_keyStatus { height: 30px; } diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index b76c62323b..bb9937c429 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -111,12 +111,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent { let content; let title; - let headerImage; + let titleClass; if (hasPassphrase && !this.state.forceRecoveryKey) { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); title = _t("Security Phrase"); - headerImage = require("../../../../../res/img/feather-customised/secure-phrase.svg"); + titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle']; let keyStatus; if (this.state.keyMatches === false) { @@ -166,7 +166,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
; } else { title = _t("Security Key"); - headerImage = require("../../../../../res/img/feather-customised/secure-backup.svg"); + titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let keyStatus; @@ -213,9 +213,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent { return (
{content} From 46058a17f8718743db3cc29fdde7cea1615ad4ed Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 14:18:38 +0100 Subject: [PATCH 18/37] Fix Room Custom Sounds regression Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/settings/tabs/room/NotificationSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index c521e228e0..257f4a5d23 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -28,6 +28,8 @@ export default class NotificationsSettingsTab extends React.Component { roomId: PropTypes.string.isRequired, }; + _soundUpload = createRef(); + constructor() { super(); @@ -44,8 +46,6 @@ export default class NotificationsSettingsTab extends React.Component { return; } this.setState({currentSound: soundData.name || soundData.url}); - - this._soundUpload = createRef(); } async _triggerUploader(e) { From 72035c807804b3741d8a8372b6b6c857bf5b8081 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 14:02:36 +0100 Subject: [PATCH 19/37] Make relevant again Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 21 ++++++++++ res/css/views/auth/_PassphraseField.scss | 19 +-------- res/css/views/elements/_ProgressBar.scss | 30 ++++++++++---- src/components/views/elements/ProgressBar.js | 39 ------------------- src/components/views/elements/ProgressBar.tsx | 28 +++++++++++++ 5 files changed, 72 insertions(+), 65 deletions(-) delete mode 100644 src/components/views/elements/ProgressBar.js create mode 100644 src/components/views/elements/ProgressBar.tsx diff --git a/res/css/_common.scss b/res/css/_common.scss index e83c6aaeda..e079292bdc 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -696,3 +696,24 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } } } + +@define-mixin ProgressBarColour $colour { + color: $colour; + &::-moz-progress-bar { + background-color: $colour; + } + &::-webkit-progress-value { + background-color: $colour; + } +} + +@define-mixin ProgressBarBorderRadius $radius { + border-radius: $radius; + &::-moz-progress-bar { + border-radius: $radius; + } + &::-webkit-progress-bar, + &::-webkit-progress-value { + border-radius: $radius; + } +} diff --git a/res/css/views/auth/_PassphraseField.scss b/res/css/views/auth/_PassphraseField.scss index d1b8c47d00..2354c597ae 100644 --- a/res/css/views/auth/_PassphraseField.scss +++ b/res/css/views/auth/_PassphraseField.scss @@ -18,16 +18,6 @@ $PassphraseStrengthHigh: $accent-color; $PassphraseStrengthMedium: $username-variant5-color; $PassphraseStrengthLow: $notice-primary-color; -@define-mixin ProgressBarColour $colour { - color: $colour; - &::-moz-progress-bar { - background-color: $colour; - } - &::-webkit-progress-value { - background-color: $colour; - } -} - progress.mx_PassphraseField_progress { appearance: none; width: 100%; @@ -36,14 +26,7 @@ progress.mx_PassphraseField_progress { position: absolute; top: -12px; - border-radius: 2px; - &::-moz-progress-bar { - border-radius: 2px; - } - &::-webkit-progress-bar, - &::-webkit-progress-value { - border-radius: 2px; - } + @mixin ProgressBarBorderRadius "2px"; @mixin ProgressBarColour $PassphraseStrengthLow; &[value="2"], &[value="3"] { diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index a3fee232d0..e49d85af04 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ProgressBar { - height: 5px; - border: 1px solid $progressbar-color; -} +progress.mx_ProgressBar { + height: 4px; + width: 60px; + border-radius: 10px; + overflow: hidden; + appearance: none; + border: 0; -.mx_ProgressBar_fill { - height: 100%; - background-color: $progressbar-color; + @mixin ProgressBarBorderRadius "10px"; + @mixin ProgressBarColour $accent-color; + ::-webkit-progress-value { + transition: width 1s; + } + ::-moz-progress-bar { + transition: padding-bottom 1s; + padding-bottom: var(--value); + transform-origin: 0 0; + transform: rotate(-90deg) translateX(-15px); + padding-left: 15px; + + height: 0; + } } diff --git a/src/components/views/elements/ProgressBar.js b/src/components/views/elements/ProgressBar.js deleted file mode 100644 index 045731ba38..0000000000 --- a/src/components/views/elements/ProgressBar.js +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; - -export default createReactClass({ - displayName: 'ProgressBar', - propTypes: { - value: PropTypes.number, - max: PropTypes.number, - }, - - render: function() { - // Would use an HTML5 progress tag but if that doesn't animate if you - // use the HTML attributes rather than styles - const progressStyle = { - width: ((this.props.value / this.props.max) * 100)+"%", - }; - return ( -
- ); - }, -}); diff --git a/src/components/views/elements/ProgressBar.tsx b/src/components/views/elements/ProgressBar.tsx new file mode 100644 index 0000000000..90832e5006 --- /dev/null +++ b/src/components/views/elements/ProgressBar.tsx @@ -0,0 +1,28 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +interface IProps { + value: number; + max: number; +} + +const ProgressBar: React.FC = ({value, max}) => { + return ; +}; + +export default ProgressBar; From f830a4b7fcb8f3cf19b31a0b5a84901d3303efd6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 26 Jun 2020 14:22:59 +0100 Subject: [PATCH 20/37] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/auth/_PassphraseField.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/auth/_PassphraseField.scss b/res/css/views/auth/_PassphraseField.scss index 2354c597ae..bf8e7f4438 100644 --- a/res/css/views/auth/_PassphraseField.scss +++ b/res/css/views/auth/_PassphraseField.scss @@ -27,7 +27,6 @@ progress.mx_PassphraseField_progress { top: -12px; @mixin ProgressBarBorderRadius "2px"; - @mixin ProgressBarColour $PassphraseStrengthLow; &[value="2"], &[value="3"] { @mixin ProgressBarColour $PassphraseStrengthMedium; From 15ebaa14704c08c5a8875a33ffac96109b14efaf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 15:22:04 +0100 Subject: [PATCH 21/37] Port recovery key upload button to new designs --- .../_AccessSecretStorageDialog.scss | 49 ++++- .../AccessSecretStorageDialog.js | 172 +++++++++++++++--- src/i18n/strings/en_EN.json | 10 +- 3 files changed, 198 insertions(+), 33 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss index f15d43b199..63d0ca555d 100644 --- a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss @@ -38,11 +38,56 @@ limitations under the License. height: 30px; } -.mx_AccessSecretStorageDialog_passPhraseInput, -.mx_AccessSecretStorageDialog_recoveryKeyInput { +.mx_AccessSecretStorageDialog_passPhraseInput { width: 300px; border: 1px solid $accent-color; border-radius: 5px; padding: 10px; } +.mx_AccessSecretStorageDialog_recoveryKeyEntry { + display: flex; + align-items: center; +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_textInput { + flex-grow: 1; +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText { + margin: 16px; +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback { + &::before { + content: ""; + display: inline-block; + vertical-align: bottom; + width: 20px; + height: 20px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 20px; + margin-right: 5px; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid { + color: $input-valid-border-color; + &::before { + mask-image: url('$(res)/img/feather-customised/check.svg'); + background-color: $input-valid-border-color; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid { + color: $input-invalid-border-color; + &::before { + mask-image: url('$(res)/img/feather-customised/x.svg'); + background-color: $input-invalid-border-color; + } +} + +.mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput { + display: none; +} diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index bb9937c429..7713f07115 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -15,13 +15,26 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { debounce } from 'lodash'; +import classNames from 'classnames'; import React from 'react'; import PropTypes from "prop-types"; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; +import Field from '../../elements/Field'; +import AccessibleButton from '../../elements/AccessibleButton'; +import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from '../../../../languageHandler'; +// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode, +// so this should be plenty and allow for people putting extra whitespace in the file because +// maybe that's a thing people would do? +const KEY_FILE_MAX_SIZE = 128; + +// Don't shout at the user that their key is invalid every time they type a key: wait a short time +const VALIDATION_THROTTLE_MS = 200; + /* * Access Secure Secret Storage by requesting the user's passphrase. */ @@ -35,9 +48,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent { constructor(props) { super(props); + + this._fileUpload = React.createRef(); + this.state = { recoveryKey: "", - recoveryKeyValid: false, + recoveryKeyValid: null, + recoveryKeyCorrect: null, + recoveryKeyFileError: null, forceRecoveryKey: false, passPhrase: '', keyMatches: null, @@ -54,12 +72,89 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } + _validateRecoveryKeyOnChange = debounce(() => { + this._validateRecoveryKey(); + }, VALIDATION_THROTTLE_MS); + + async _validateRecoveryKey() { + if (this.state.recoveryKey === '') { + this.setState({ + recoveryKeyValid: null, + recoveryKeyCorrect: null, + }); + return; + } + + try { + const decodedKey = decodeRecoveryKey(this.state.recoveryKey); + const correct = await MatrixClientPeg.get().checkSecretStorageKey( + decodedKey, this.props.keyInfo, + ); + this.setState({ + recoveryKeyValid: true, + recoveryKeyCorrect: correct, + }); + } catch (e) { + this.setState({ + recoveryKeyValid: false, + recoveryKeyCorrect: false, + }); + } + } + _onRecoveryKeyChange = (e) => { this.setState({ recoveryKey: e.target.value, - recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), - keyMatches: null, + recoveryKeyFileError: null, }); + + // also clear the file upload control so that the user can upload the same file + // the did before (otherwise the onchange wouldn't fire) + this._fileUpload.current.value = null; + + + // We don't use Field's validation here because a) we want it in a separate place rather + // than in a tooltip and b) we want it to display feedback based on the uploaded file + // as well as the text box. Ideally we would refactor Field's validation logic so we could + // re-use some of it. + this._validateRecoveryKeyOnChange(); + } + + _onRecoveryKeyFileChange = async e => { + if (e.target.files.length === 0) return; + + const f = e.target.files[0]; + + if (f.size > KEY_FILE_MAX_SIZE) { + this.setState({ + recoveryKeyFileError: true, + recoveryKeyCorrect: false, + recoveryKeyValid: false, + }); + } else { + const contents = await f.text(); + // test it's within the base58 alphabet. We could be more strict here, eg. require the + // right number of characters, but it's really just to make sure that what we're reading is + // text because we'll put it in the text field. + if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) { + this.setState({ + recoveryKeyFileError: null, + recoveryKey: contents.trim(), + }); + this._validateRecoveryKey(); + } else { + this.setState({ + recoveryKeyFileError: true, + recoveryKeyCorrect: false, + recoveryKeyValid: false, + recoveryKey: '', + }); + } + } + } + + _onRecoveryKeyFileUploadClick = () => { + this._fileUpload.current.click(); } _onPassPhraseNext = async (e) => { @@ -99,6 +194,20 @@ export default class AccessSecretStorageDialog extends React.PureComponent { }); } + getKeyValidationText() { + if (this.state.recoveryKeyFileError) { + return _t("Wrong file type"); + } else if (this.state.recoveryKeyCorrect) { + return _t("Looks good!"); + } else if (this.state.recoveryKeyValid) { + return _t("Wrong Recovery Key"); + } else if (this.state.recoveryKeyValid === null) { + return ''; + } else { + return _t("Invalid Recovery Key"); + } + } + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); @@ -169,36 +278,43 @@ export default class AccessSecretStorageDialog extends React.PureComponent { titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle']; const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - let keyStatus; - if (this.state.recoveryKey.length === 0) { - keyStatus =
; - } else if (this.state.keyMatches === false) { - keyStatus =
- {"\uD83D\uDC4E "}{_t( - "Unable to access secret storage. " + - "Please verify that you entered the correct recovery key.", - )} -
; - } else if (this.state.recoveryKeyValid) { - keyStatus =
- {"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")} -
; - } else { - keyStatus =
- {"\uD83D\uDC4E "}{_t("Not a valid recovery key")} -
; - } + const feedbackClasses = classNames({ + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true, + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true, + 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false, + }); + const recoveryKeyFeedback =
+ {this.getKeyValidationText()} +
; content =

{_t("Use your Security Key to continue.")}

- - {keyStatus} +
+
+ +
+ + {_t("or")} + +
+ + + {_t("Upload")} + +
+
+ {recoveryKeyFeedback} Use your Security Key to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", - "Unable to access secret storage. Please verify that you entered the correct recovery key.": "Unable to access secret storage. Please verify that you entered the correct recovery key.", - "This looks like a valid recovery key!": "This looks like a valid recovery key!", - "Not a valid recovery key": "Not a valid recovery key", "Use your Security Key to continue.": "Use your Security Key to continue.", + "Recovery Key": "Recovery Key", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", @@ -1813,6 +1815,8 @@ "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Access your secure message history and set up secure messaging by entering your recovery passphrase.", "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options": "If you've forgotten your recovery passphrase you can use your recovery key or set up new recovery options", "Enter recovery key": "Enter recovery key", + "This looks like a valid recovery key!": "This looks like a valid recovery key!", + "Not a valid recovery key": "Not a valid recovery key", "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.", "If you've forgotten your recovery key you can ": "If you've forgotten your recovery key you can ", From b74674ced8b9a947fdb089fb471a5fb2c865cf06 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 18:04:06 +0100 Subject: [PATCH 22/37] Right name for security key and fix cancel button --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 4 +++- src/i18n/strings/en_EN.json | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 7713f07115..3141cfb33b 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -295,7 +295,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
@@ -319,6 +319,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent { primaryButton={_t('Continue')} onPrimaryButtonClick={this._onRecoveryKeyNext} hasCancel={true} + cancelButton={_t("Go Back")} + cancelButtonClass='danger' onCancel={this._onCancel} focus={false} primaryDisabled={!this.state.recoveryKeyValid} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7a2fb12867..9160d48f48 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1796,7 +1796,6 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", - "Recovery Key": "Recovery Key", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", From 24baf19d6528cae719cedde35d3588b178f15421 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 18:50:05 +0100 Subject: [PATCH 23/37] Set field validity (ie. border colour) correctly Changes flagInvalid to forceValidity which can force valid as well as invalid. --- .../dialogs/secretstorage/CreateSecretStorageDialog.js | 2 +- .../dialogs/secretstorage/AccessSecretStorageDialog.js | 1 + src/components/views/elements/Field.tsx | 10 +++++----- src/components/views/settings/SetIdServer.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 58b5b57354..984158c7a2 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -491,7 +491,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { label={_t("Password")} value={this.state.accountPassword} onChange={this._onAccountPasswordChange} - flagInvalid={this.state.accountPasswordCorrect === false} + forceValidity={this.state.accountPasswordCorrect === false ? false : null} autoFocus={true} />
; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 3141cfb33b..5029856f26 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -298,6 +298,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { label={_t('Security Key')} value={this.state.recoveryKey} onChange={this._onRecoveryKeyChange} + forceValidity={this.state.recoveryKeyCorrect} />
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 834edff7df..9a889a0351 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -50,7 +50,7 @@ interface IProps { // to the user. onValidate?: (input: IFieldState) => Promise; // If specified, overrides the value returned by onValidate. - flagInvalid?: boolean; + forceValidity?: boolean; // If specified, contents will appear as a tooltip on the element and // validation feedback tooltips will be suppressed. tooltipContent?: React.ReactNode; @@ -203,7 +203,7 @@ export default class Field extends React.PureComponent { public render() { const { element, prefixComponent, postfixComponent, className, onValidate, children, - tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props; + tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props; // Set some defaults for the element const ref = input => this.input = input; @@ -228,15 +228,15 @@ export default class Field extends React.PureComponent { postfixContainer = {postfixComponent}; } - const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined; + const hasValidationFlag = forceValidity !== null && forceValidity !== undefined; const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, { // If we have a prefix element, leave the label always at the top left and // don't animate it, as it looks a bit clunky and would add complexity to do // properly. mx_Field_labelAlwaysTopLeft: prefixComponent, - mx_Field_valid: onValidate && this.state.valid === true, + mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true, mx_Field_invalid: hasValidationFlag - ? flagInvalid + ? !forceValidity : onValidate && this.state.valid === false, }); diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js index 23e72e2352..e05fe4f1c3 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.js @@ -413,7 +413,7 @@ export default class SetIdServer extends React.Component { tooltipContent={this._getTooltip()} tooltipClassName="mx_SetIdServer_tooltip" disabled={this.state.busy} - flagInvalid={!!this.state.error} + forceValidity={this.state.error ? false : null} /> Date: Fri, 26 Jun 2020 18:55:23 +0100 Subject: [PATCH 24/37] Disable spellcheck on the recovery key entry --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 5029856f26..8b61af8886 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -290,7 +290,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { content =

{_t("Use your Security Key to continue.")}

- +
Date: Fri, 26 Jun 2020 18:58:12 +0100 Subject: [PATCH 25/37] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9160d48f48..4e5fd63d6c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1796,6 +1796,7 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", + "Go Back": "Go Back", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", @@ -2138,7 +2139,6 @@ "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", - "Go Back": "Go Back", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Incorrect password": "Incorrect password", "Failed to re-authenticate": "Failed to re-authenticate", From 916f60687298e75f2a7e51043b9bead20cc542a7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 19:07:39 +0100 Subject: [PATCH 26/37] Apparently we need to null check here --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 8b61af8886..868eeb2218 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -110,7 +110,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { // also clear the file upload control so that the user can upload the same file // the did before (otherwise the onchange wouldn't fire) - this._fileUpload.current.value = null; + if (this._fileUpload.current) this._fileUpload.current.value = null; // We don't use Field's validation here because a) we want it in a separate place rather From 0579c9f748b052c4e7ad547457d8d46946da8c31 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 20:25:38 +0100 Subject: [PATCH 27/37] Fix tests --- .../AccessSecretStorageDialog.js | 6 ++-- .../dialogs/AccessSecretStorageDialog-test.js | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 868eeb2218..e5b75f4dfc 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -23,7 +23,6 @@ import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import Field from '../../elements/Field'; import AccessibleButton from '../../elements/AccessibleButton'; -import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from '../../../../languageHandler'; @@ -86,8 +85,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent { } try { - const decodedKey = decodeRecoveryKey(this.state.recoveryKey); - const correct = await MatrixClientPeg.get().checkSecretStorageKey( + const cli = MatrixClientPeg.get(); + const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey); + const correct = await cli.checkSecretStorageKey( decodedKey, this.props.keyInfo, ); this.setState({ diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js index c754a4b607..71413a2978 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.js @@ -40,19 +40,20 @@ describe("AccessSecretStorageDialog", function() { testInstance.getInstance()._onRecoveryKeyNext(e); }); - it("Considers a valid key to be valid", function() { + it("Considers a valid key to be valid", async function() { const testInstance = TestRenderer.create( true} />, ); - const v = "asfd"; + const v = "asdf"; const e = { target: { value: v } }; stubClient(); - MatrixClientPeg.get().isValidRecoveryKey = function(k) { - return k == v; - }; + MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key'; + MatrixClientPeg.get().checkSecretStorageKey = () => true; testInstance.getInstance()._onRecoveryKeyChange(e); + // force a validation now because it debounces + await testInstance.getInstance()._validateRecoveryKey(); const { recoveryKeyValid } = testInstance.getInstance().state; expect(recoveryKeyValid).toBe(true); }); @@ -65,17 +66,20 @@ describe("AccessSecretStorageDialog", function() { ); const e = { target: { value: "a" } }; stubClient(); - MatrixClientPeg.get().isValidRecoveryKey = () => true; + MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => { + throw new Error("that's no key"); + }; testInstance.getInstance()._onRecoveryKeyChange(e); - await testInstance.getInstance()._onRecoveryKeyNext({ preventDefault: () => {} }); - const { keyMatches } = testInstance.getInstance().state; - expect(keyMatches).toBe(false); + // force a validation now because it debounces + await testInstance.getInstance()._validateRecoveryKey(); + + const { recoveryKeyValid, recoveryKeyCorrect } = testInstance.getInstance().state; + expect(recoveryKeyValid).toBe(false); + expect(recoveryKeyCorrect).toBe(false); const notification = testInstance.root.findByProps({ - className: "mx_AccessSecretStorageDialog_keyStatus", + className: "mx_AccessSecretStorageDialog_recoveryKeyFeedback mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid", }); - expect(notification.props.children).toEqual( - ["\uD83D\uDC4E ", "Unable to access secret storage. Please verify that you " + - "entered the correct recovery key."]); + expect(notification.props.children).toEqual("Invalid Recovery Key"); done(); }); From 2969820371b60976a995d3a159f3f740610d1955 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Jun 2020 20:31:22 +0100 Subject: [PATCH 28/37] LINT --- .../components/views/dialogs/AccessSecretStorageDialog-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js index 71413a2978..5a8dcbf763 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.js @@ -77,7 +77,8 @@ describe("AccessSecretStorageDialog", function() { expect(recoveryKeyValid).toBe(false); expect(recoveryKeyCorrect).toBe(false); const notification = testInstance.root.findByProps({ - className: "mx_AccessSecretStorageDialog_recoveryKeyFeedback mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid", + className: "mx_AccessSecretStorageDialog_recoveryKeyFeedback " + + "mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid", }); expect(notification.props.children).toEqual("Invalid Recovery Key"); done(); From 10492fe72f4721b6acac7610b3068bc6c296cf63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 27 Jun 2020 18:30:15 +0100 Subject: [PATCH 29/37] fix StyledRadioGroup React key warning Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/StyledRadioGroup.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index 050a8b7adb..ded1342462 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -41,9 +41,8 @@ function StyledRadioGroup({name, definitions, value, className }; return - {definitions.map(d => + {definitions.map(d => Date: Mon, 29 Jun 2020 11:34:58 +0100 Subject: [PATCH 30/37] ToastStore fix type definition Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/ToastStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 55c48c3937..7063ba541a 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -24,7 +24,7 @@ export interface IToast; + props?: Omit, "toastKey">; // toastKey is injected by ToastContainer } /** From 51b813e2500eab2257daea1af7e40834a82742ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Jun 2020 11:35:14 +0100 Subject: [PATCH 31/37] add timing/interval/expiry hooks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/hooks/useTimeout.ts | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/hooks/useTimeout.ts diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts new file mode 100644 index 0000000000..911b7bc75d --- /dev/null +++ b/src/hooks/useTimeout.ts @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {useEffect, useRef, useState} from "react"; + +type Handler = () => void; + +// Hook to simplify timeouts in functional components +export const useTimeout = (handler: Handler, timeoutMs: number) => { + // Create a ref that stores handler + const savedHandler = useRef(); + + // Update ref.current value if handler changes. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + // Set up timer + useEffect(() => { + const timeoutID = setTimeout(() => { + savedHandler.current(); + }, timeoutMs); + return () => clearTimeout(timeoutID); + }, [timeoutMs]); +}; + +// Hook to simplify intervals in functional components +export const useInterval = (handler: Handler, intervalMs: number) => { + // Create a ref that stores handler + const savedHandler = useRef(); + + // Update ref.current value if handler changes. + useEffect(() => { + savedHandler.current = handler; + }, [handler]); + + // Set up timer + useEffect(() => { + const intervalID = setInterval(() => { + savedHandler.current(); + }, intervalMs); + return () => clearInterval(intervalID); + }, [intervalMs]); +}; + +// Hook to simplify a variable counting down to 0, handler called when it reached 0 +export const useExpiringCounter = (handler: Handler, intervalMs: number, initialCount: number) => { + const [count, setCount] = useState(initialCount); + useInterval(() => setCount(c => c - 1), intervalMs); + if (count === 0) { + handler(); + } + return count; +}; From 1a1b7e5e702f9a1222bcf576b5bdbb28710d6b51 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Jun 2020 11:38:50 +0100 Subject: [PATCH 32/37] Add Generic Expiring Toast Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/toasts/GenericExpiringToast.tsx | 53 +++++++++++++++++++ src/components/views/toasts/GenericToast.tsx | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/components/views/toasts/GenericExpiringToast.tsx diff --git a/src/components/views/toasts/GenericExpiringToast.tsx b/src/components/views/toasts/GenericExpiringToast.tsx new file mode 100644 index 0000000000..83f43208c4 --- /dev/null +++ b/src/components/views/toasts/GenericExpiringToast.tsx @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import ToastStore from "../../../stores/ToastStore"; +import GenericToast, { IProps as IGenericToastProps } from "./GenericToast"; +import {useExpiringCounter} from "../../../hooks/useTimeout"; + +interface IProps extends IGenericToastProps { + toastKey: string; + numSeconds: number; + dismissLabel: string; + onDismiss?(); +} + +const SECOND = 1000; + +const GenericExpiringToast: React.FC = ({description, acceptLabel, dismissLabel, onAccept, onDismiss, toastKey, numSeconds}) => { + const onReject = () => { + if (onDismiss) onDismiss(); + ToastStore.sharedInstance().dismissToast(toastKey); + }; + const counter = useExpiringCounter(onReject, SECOND, numSeconds); + + let rejectLabel = dismissLabel; + if (counter > 0) { + rejectLabel += ` (${counter})`; + } + + return ; +}; + +export default GenericExpiringToast; diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index ea12641948..9f8885ba47 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -19,7 +19,7 @@ import React, {ReactChild} from "react"; import FormButton from "../elements/FormButton"; import {XOR} from "../../../@types/common"; -interface IProps { +export interface IProps { description: ReactChild; acceptLabel: string; From b2b909aa53b545c835de591e48ebef593cf8b219 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Jun 2020 15:40:20 +0100 Subject: [PATCH 33/37] Including start_sso and start_cas in redirect loop prevention Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/MatrixChat.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 79bdf743ce..02f08211b9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1932,11 +1932,12 @@ export default class MatrixChat extends React.PureComponent { getFragmentAfterLogin() { let fragmentAfterLogin = ""; - if (this.props.initialScreenAfterLogin && + const initialScreenAfterLogin = this.props.initialScreenAfterLogin; + if (initialScreenAfterLogin && // XXX: workaround for https://github.com/vector-im/riot-web/issues/11643 causing a login-loop - !["welcome", "login", "register"].includes(this.props.initialScreenAfterLogin.screen) + !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen) ) { - fragmentAfterLogin = `/${this.props.initialScreenAfterLogin.screen}`; + fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`; } return fragmentAfterLogin; } From 9f6893ef2b4ab2e69ebcf3c302f21e7504c26bf5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 Jun 2020 16:27:59 +0100 Subject: [PATCH 34/37] Fix /join slash command via servers including room id as a via Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 7ebdc4ee3b..f667c47b3c 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -495,8 +495,7 @@ export const Commands = [ }); return success(); } else if (params[0][0] === '!') { - const roomId = params[0]; - const viaServers = params.splice(0); + const [roomId, ...viaServers] = params; dis.dispatch({ action: 'view_room', From 1e457994f903a05bcf6136b5d2b3a7c0a7bad977 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Jun 2020 11:10:12 +0100 Subject: [PATCH 35/37] More padding between header & text in radio button --- .../views/dialogs/secretstorage/_CreateSecretStorageDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index b073bac93c..d30803b1f0 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -91,6 +91,7 @@ limitations under the License. color: $dialog-title-fg-color; font-weight: 600; font-size: $font-18px; + padding-bottom: 10px; } .mx_CreateSecretStorageDialog_optionIcon { From 7d7bafb1ea1e6c18526521f1bfe66f8d930d2f17 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 30 Jun 2020 16:23:52 +0100 Subject: [PATCH 36/37] De-duplicate rooms from the room autocomplete provider Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/RoomProvider.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 0d8aac4218..f14fa3bbfa 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -25,9 +25,9 @@ import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; import * as sdk from '../index'; -import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { uniqBy, sortBy } from 'lodash'; const ROOM_REGEX = /\B#\S*/g; @@ -91,10 +91,11 @@ export default class RoomProvider extends AutocompleteProvider { this.matcher.setObjects(matcherObjects); const matchedString = command[0]; completions = this.matcher.match(matchedString); - completions = _sortBy(completions, [ + completions = sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, ]); + completions = uniqBy(completions, (match) => match.room); completions = completions.map((room) => { return { completion: room.displayedAlias, From 7caf2d5459cf472a470dea7cda48bee7d21be827 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Jun 2020 17:56:50 +0100 Subject: [PATCH 37/37] remove rogue blank line --- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index e5b75f4dfc..5c01a6907f 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -112,7 +112,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent { // the did before (otherwise the onchange wouldn't fire) if (this._fileUpload.current) this._fileUpload.current.value = null; - // We don't use Field's validation here because a) we want it in a separate place rather // than in a tooltip and b) we want it to display feedback based on the uploaded file // as well as the text box. Ideally we would refactor Field's validation logic so we could