diff --git a/src/app/organisms/settings/CrossSigning.jsx b/src/app/organisms/settings/CrossSigning.jsx index 644c0b97..13d5bbbe 100644 --- a/src/app/organisms/settings/CrossSigning.jsx +++ b/src/app/organisms/settings/CrossSigning.jsx @@ -1,38 +1,81 @@ -import React from 'react'; +import React, { useState } from 'react'; import './CrossSigning.scss'; +import FileSaver from 'file-saver'; import { Formik } from 'formik'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import { openReusableDialog } from '../../../client/action/navigation'; import { hasCrossSigningAccountData } from '../../../util/matrixUtil'; +import { copyToClipboard } from '../../../util/common'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import Input from '../../atoms/input/Input'; +import Spinner from '../../atoms/spinner/Spinner'; import SettingTile from '../../molecules/setting-tile/SettingTile'; +const securityKeyDialog = (key) => { + const downloadKey = () => { + const blob = new Blob([key.encodedPrivateKey], { + type: 'text/plain;charset=us-ascii', + }); + FileSaver.saveAs(blob, 'security-key.txt'); + }; + const copyKey = () => { + copyToClipboard(key.encodedPrivateKey); + }; + + const renderSecurityKey = () => ( +
+ Please save this security key somewhere safe. + + {key.encodedPrivateKey} + +
+ + +
+
+ ); + + // Download automatically. + downloadKey(); + + openReusableDialog( + Security Key, + () => renderSecurityKey(), + ); +}; + function CrossSigningSetup() { const initialValues = { phrase: '', confirmPhrase: '' }; + const [genWithPhrase, setGenWithPhrase] = useState(undefined); - const setup = (securityPhrase = undefined) => { + const setup = async (securityPhrase = undefined) => { const mx = initMatrix.matrixClient; - const crossSigning = mx.getStoredCrossSigningForUser(mx.getUserId()); + setGenWithPhrase(typeof securityPhrase === 'string'); + const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase); - if (!crossSigning) { - // TODO: bootstrap crossSigning. - } - if (hasCrossSigningAccountData()) { - // TODO: user is doing reset. - // delete current cross signing keys - // delete current message backup - } + const bootstrapSSOpts = { + createSecretStorageKey: async () => recoveryKey, + setupNewKeyBackup: true, + setupNewSecretStorage: true, + }; - // TODO: prompt user security key. + await mx.bootstrapSecretStorage(bootstrapSSOpts); + + securityKeyDialog(recoveryKey); }; const validator = (values) => { const errors = {}; + if (values.phrase === '12345678') { + errors.phrase = 'How about 87654321 ?'; + } + if (values.phrase === '87654321') { + errors.phrase = 'Your are playing with 🔥'; + } const PHRASE_REGEX = /^([^\s]){8,127}$/; if (values.phrase.length > 0 && !PHRASE_REGEX.test(values.phrase)) { errors.phrase = 'Phrase must contain 8-127 characters with no space.'; @@ -43,10 +86,6 @@ function CrossSigningSetup() { return errors; }; - const submitter = (values) => { - setup(values.phrase); - }; - return (
@@ -54,12 +93,13 @@ function CrossSigningSetup() { We will generate a Security Key, which you can use to manage messages backup and session verification. - + {genWithPhrase !== false && } + {genWithPhrase === false && }
OR setup(values.phrase)} validate={validator} > {({ @@ -68,17 +108,35 @@ function CrossSigningSetup() {
Alternatively you can also set a Security Phrase so you don't have to remember long Security Key, and optionally save the Key as backup. - + {errors.phrase && {errors.phrase}} - + {errors.confirmPhrase && {errors.confirmPhrase}} - + {genWithPhrase !== true && } + {genWithPhrase === true && } )}
diff --git a/src/app/organisms/settings/CrossSigning.scss b/src/app/organisms/settings/CrossSigning.scss index 86df4c5d..494fa977 100644 --- a/src/app/organisms/settings/CrossSigning.scss +++ b/src/app/organisms/settings/CrossSigning.scss @@ -27,6 +27,24 @@ } } } + +.cross-signing__key { + padding: var(--sp-normal); + + &-text { + margin: var(--sp-normal) 0; + padding: var(--sp-extra-tight); + background-color: var(--bg-surface-low); + border-radius: var(--bo-radius); + } + &-btn { + display: flex; + & > button:last-child { + margin: 0 var(--sp-normal); + } + } +} + .cross-signing__reset { padding: var(--sp-normal); padding-top: var(--sp-extra-loose); diff --git a/src/util/common.js b/src/util/common.js index 941f34cf..3d5383ad 100644 --- a/src/util/common.js +++ b/src/util/common.js @@ -114,3 +114,21 @@ export function getScrollInfo(target) { export function avatarInitials(text) { return [...text][0]; } + +export function copyToClipboard(text) { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + const host = document.body; + const copyInput = document.createElement('input'); + copyInput.style.position = 'fixed'; + copyInput.style.opacity = '0'; + copyInput.value = text; + host.append(copyInput); + + copyInput.select(); + copyInput.setSelectionRange(0, 99999); + document.execCommand('Copy'); + copyInput.remove(); + } +} diff --git a/webpack.common.js b/webpack.common.js index 9ff58daf..db5eedd9 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,6 +1,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); const CopyPlugin = require("copy-webpack-plugin"); +const webpack = require('webpack'); module.exports = { entry: { @@ -17,6 +18,7 @@ module.exports = { 'util': require.resolve('util/'), 'assert': require.resolve('assert/'), 'url': require.resolve('url/'), + 'buffer': require.resolve('buffer'), } }, node: { @@ -73,5 +75,8 @@ module.exports = { { from: 'config.json' }, ], }), + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + }), ], };