Merge pull request #50 from ente-io/recovery-key

Recovery key
This commit is contained in:
Abhinav-grd 2021-04-06 10:41:16 +05:30 committed by GitHub
commit 4f5f0fe378
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 908 additions and 334 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View file

@ -13,10 +13,10 @@ const CONFIRM_ACTION_VALUES = [
{ text: 'LOGOUT', type: 'danger' },
{ text: 'DELETE', type: 'danger' },
{ text: 'SESSION_EXPIRED', type: 'primary' },
{ text: 'DOWNLOAD_APP', type: 'primary' },
{ text: 'DOWNLOAD_APP', type: 'success' },
];
export interface Props {
interface Props {
callback: any;
action: CONFIRM_ACTION;
show: boolean;
@ -37,19 +37,19 @@ function ConfirmDialog({ callback, action, ...props }: Props) {
<Modal.Title id="contained-modal-title-vcenter">
{
constants[
`${CONFIRM_ACTION_VALUES[action]?.text}_MESSAGE`
`${CONFIRM_ACTION_VALUES[action]?.text}_MESSAGE`
]
}
</Modal.Title>
</Modal.Body>
<Modal.Footer style={{ borderTop: 'none' }}>
{action != CONFIRM_ACTION.SESSION_EXPIRED && (
<Button variant="secondary" onClick={props.onHide}>
<Button variant="outline-secondary" onClick={props.onHide}>
{constants.CANCEL}
</Button>
)}
<Button
variant={`${CONFIRM_ACTION_VALUES[action]?.type}`}
variant={`outline-${CONFIRM_ACTION_VALUES[action]?.type}`}
onClick={callback}
>
{constants[CONFIRM_ACTION_VALUES[action]?.text]}

View file

@ -0,0 +1,61 @@
import React from 'react';
import { Button, Modal } from 'react-bootstrap';
import constants from 'utils/strings/constants';
interface Props {
show: boolean;
children?: any;
onHide: () => void;
attributes?: {
title?: string;
ok?: boolean;
staticBackdrop?: boolean;
cancel?: { text: string; action?: any };
proceed?: { text: string; action: any };
};
}
export function MessageDialog({ attributes, children, ...props }: Props) {
return (
<Modal
{...props}
size="lg"
centered
backdrop={attributes?.staticBackdrop ? 'static' : 'true'}
>
<Modal.Body>
{attributes?.title && (
<Modal.Title>
<strong>{attributes.title}</strong>
<hr />
</Modal.Title>
)}
{children}
</Modal.Body>
{attributes && (
<Modal.Footer style={{ borderTop: 'none' }}>
{attributes.ok && (
<Button variant="secondary" onClick={props.onHide}>
{constants.OK}
</Button>
)}
{attributes.cancel && (
<Button
variant="outline-danger"
onClick={attributes.cancel.action ?? props.onHide}
>
{attributes.cancel.text}
</Button>
)}
{attributes.proceed && (
<Button
variant="outline-success"
onClick={attributes.proceed.action}
>
{attributes.proceed.text}
</Button>
)}
</Modal.Footer>
)}
</Modal>
);
}

View file

@ -0,0 +1,117 @@
import React, { useEffect, useState } from 'react';
import Container from 'components/Container';
import styled from 'styled-components';
import Card from 'react-bootstrap/Card';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import constants from 'utils/strings/constants';
import { Formik, FormikHelpers } from 'formik';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import * as Yup from 'yup';
import { KeyAttributes } from 'types';
import CryptoWorker, { setSessionKeys } from 'utils/crypto';
import { Spinner } from 'react-bootstrap';
import { propTypes } from 'react-bootstrap/esm/Image';
interface formValues {
passphrase: string;
}
interface Props {
callback: (passphrase: string, setFieldError) => void;
fieldType: string;
title: string;
placeholder: string;
buttonText: string;
alternateOption: { text: string; click: () => void };
back: () => void;
}
export default function PassPhraseForm(props: Props) {
const [loading, SetLoading] = useState(false);
const submitForm = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
SetLoading(true);
await props.callback(values.passphrase, setFieldError);
SetLoading(false);
};
return (
<Container>
<Card
style={{ minWidth: '320px', padding: '40px 30px' }}
className="text-center"
>
<Card.Body>
<Card.Title style={{ marginBottom: '24px' }}>
{props.title}
</Card.Title>
<Formik<formValues>
initialValues={{ passphrase: '' }}
onSubmit={submitForm}
validationSchema={Yup.object().shape({
passphrase: Yup.string().required(
constants.REQUIRED
),
})}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit,
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Form.Control
type={props.fieldType}
placeholder={props.placeholder}
value={values.passphrase}
onChange={handleChange('passphrase')}
onBlur={handleBlur('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase
)}
disabled={loading}
autoFocus={true}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Button block type="submit" disabled={loading}>
{loading ? (
<Spinner animation="border" />
) : (
props.buttonText
)}
</Button>
<br />
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<Button
variant="link"
onClick={props.alternateOption.click}
>
{props.alternateOption.text}
</Button>
<Button variant="link" onClick={props.back}>
{constants.GO_BACK}
</Button>
</div>
</Form>
)}
</Formik>
</Card.Body>
</Card>
</Container>
);
}

View file

@ -0,0 +1,135 @@
import React, { useState, useEffect, useContext } from 'react';
import Container from 'components/Container';
import styled from 'styled-components';
import Card from 'react-bootstrap/Card';
import Form from 'react-bootstrap/Form';
import constants from 'utils/strings/constants';
import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import Button from 'react-bootstrap/Button';
import { Spinner } from 'react-bootstrap';
interface Props {
callback: (passphrase: any, setFieldError: any) => Promise<void>;
buttonText: string;
back: () => void;
}
interface formValues {
passphrase: string;
confirm: string;
}
function SetPassword(props: Props) {
const [loading, setLoading] = useState(false);
const onSubmit = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
setLoading(true);
try {
const { passphrase, confirm } = values;
if (passphrase === confirm) {
await props.callback(passphrase, setFieldError);
} else {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
}
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
} finally {
setLoading(false);
}
};
return (
<Container>
<Card style={{ maxWidth: '540px', padding: '20px' }}>
<Card.Body>
<div
className="text-center"
style={{ marginBottom: '40px' }}
>
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
{constants.PASSPHRASE_DISCLAIMER()}
</div>
<Formik<formValues>
initialValues={{ passphrase: '', confirm: '' }}
validationSchema={Yup.object().shape({
passphrase: Yup.string().required(
constants.REQUIRED
),
confirm: Yup.string().required(constants.REQUIRED),
})}
onSubmit={onSubmit}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit,
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Form.Control
type="password"
placeholder={constants.PASSPHRASE_HINT}
value={values.passphrase}
onChange={handleChange('passphrase')}
onBlur={handleBlur('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase
)}
autoFocus={true}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
type="password"
placeholder={
constants.PASSPHRASE_CONFIRM
}
value={values.confirm}
onChange={handleChange('confirm')}
onBlur={handleBlur('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.confirm}
</Form.Control.Feedback>
</Form.Group>
<Button
type="submit"
block
disabled={loading}
style={{ marginTop: '28px' }}
>
{loading ? (
<Spinner animation="border" />
) : (
props.buttonText
)}
</Button>
</Form>
)}
</Formik>
<div className="text-center" style={{ marginTop: '20px' }}>
<Button variant="link" onClick={props.back}>
{constants.GO_BACK}
</Button>
</div>
</Card.Body>
</Card>
</Container>
);
}
export default SetPassword;

View file

@ -0,0 +1,86 @@
import React, { useEffect, useState } from 'react';
import { Spinner } from 'react-bootstrap';
import { downloadAsFile } from 'utils/common';
import { getRecoveryKey } from 'utils/crypto';
import { setJustSignedUp } from 'utils/storage';
import constants from 'utils/strings/constants';
import { MessageDialog } from './MessageDailog';
interface Props {
show: boolean;
onHide: () => void;
somethingWentWrong: any;
}
function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
const [recoveryKey, setRecoveryKey] = useState(null);
useEffect(() => {
if (!props.show) {
return;
}
const main = async () => {
const recoveryKey = await getRecoveryKey();
if (!recoveryKey) {
somethingWentWrong();
props.onHide();
}
setRecoveryKey(recoveryKey);
};
main();
}, [props.show]);
function onSaveClick() {
downloadAsFile(constants.RECOVERY_KEY_FILENAME, recoveryKey);
onSaveLaterClick();
}
function onSaveLaterClick() {
props.onHide();
setJustSignedUp(false);
}
return (
<MessageDialog
{...props}
attributes={{
title: constants.DOWNLOAD_RECOVERY_KEY,
cancel: {
text: constants.SAVE_LATER,
action: onSaveLaterClick,
},
staticBackdrop: true,
proceed: {
text: constants.SAVE,
action: onSaveClick,
},
}}
>
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#1a1919',
height: '150px',
padding: '40px',
color: 'white',
margin: '20px 0',
}}
>
{recoveryKey ? (
<div
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word',
minWidth: '30%',
}}
>
{recoveryKey}
</div>
) : (
<Spinner animation="border" />
)}
</div>
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
</MessageDialog>
);
}
export default RecoveryKeyModal;

View file

@ -14,15 +14,20 @@ import exportService from 'services/exportService';
import { file } from 'services/fileService';
import isElectron from 'is-electron';
import { collection } from 'services/collectionService';
import { useRouter } from 'next/router';
import RecoveryKeyModal from './RecoveryKeyModal';
import { justSignedUp } from 'utils/storage';
interface Props {
files: file[];
collections: collection[];
setConfirmAction: any;
somethingWentWrong: any;
}
export default function Sidebar(props: Props) {
const [usage, SetUsage] = useState<string>(null);
const subscription: Subscription = getData(LS_KEYS.SUBSCRIPTION);
const [isOpen, setIsOpen] = useState(false);
const [modalView, setModalView] = useState(justSignedUp());
useEffect(() => {
const main = async () => {
if (!isOpen) {
@ -48,6 +53,7 @@ export default function Sidebar(props: Props) {
props.setConfirmAction(CONFIRM_ACTION.DOWNLOAD_APP);
}
}
const router = useRouter();
return (
<Menu
@ -114,14 +120,47 @@ export default function Sidebar(props: Props) {
support
</a>
</h5>
<div
style={{
height: '1px',
marginTop: '40px',
background: '#242424',
width: '100%',
}}
></div>
<>
<RecoveryKeyModal
show={modalView}
onHide={() => setModalView(false)}
somethingWentWrong={props.somethingWentWrong}
/>
<h5
style={{ cursor: 'pointer', marginTop: '30px' }}
onClick={() => setModalView(true)}
>
{constants.DOWNLOAD_RECOVERY_KEY}
</h5>
</>
<h5
style={{ cursor: 'pointer', marginTop: '30px' }}
onClick={() => router.push('changePassword')}
>
{constants.CHANGE_PASSWORD}
</h5>
<h5
style={{ cursor: 'pointer', marginTop: '30px' }}
onClick={exportFiles}
>
{constants.EXPORT}
</h5>
<div
style={{
height: '1px',
marginTop: '40px',
background: '#242424',
width: '100%',
}}
></div>
<h5
style={{
cursor: 'pointer',

View file

@ -124,10 +124,20 @@ const GlobalStyles = createGlobalStyle`
.btn-outline-success {
color: #2dc262;
border-color: #2dc262;
border-width: 2px;
}
.btn-outline-success:hover {
background: #2dc262;
}
.btn-outline-danger {
border-width: 2px;
}
.btn-outline-secondary {
border-width: 2px;
}
.btn-outline-primary {
border-width: 2px;
}
.card {
background-color: #242424;
color: #fff;
@ -140,7 +150,8 @@ const GlobalStyles = createGlobalStyle`
margin-top: 50px;
}
.alert-success {
background-color: #c4ffd6;
background-color: #a9f7ff;
color: #000000;
}
.alert-primary {
background-color: #c4ffd6;

View file

@ -0,0 +1,79 @@
import React, { useState, useEffect, useContext } from 'react';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { getKey, SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
import { B64EncryptionResult } from 'services/uploadService';
import CryptoWorker, {
setSessionKeys,
generateAndSaveIntermediateKeyAttributes,
} from 'utils/crypto';
import { getActualKey } from 'utils/common/key';
import { logoutUser, setKeys, UpdatedKey } from 'services/userService';
import PasswordForm from 'components/PasswordForm';
export interface KEK {
key: string;
opsLimit: number;
memLimit: number;
}
export default function Generate() {
const [token, setToken] = useState<string>();
const router = useRouter();
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
useEffect(() => {
const user = getData(LS_KEYS.USER);
if (!user?.token) {
router.push('/');
} else {
setToken(user.token);
}
}, []);
const onSubmit = async (passphrase, setFieldError) => {
const cryptoWorker = await new CryptoWorker();
const key: string = await getActualKey();
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
let kek: KEK;
try {
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
} catch (e) {
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
return;
}
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(
key,
kek.key
);
const updatedKey: UpdatedKey = {
kekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData,
keyDecryptionNonce: encryptedKeyAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
};
await setKeys(token, updatedKey);
const updatedKeyAttributes = Object.assign(keyAttributes, updatedKey);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
updatedKeyAttributes,
key
);
setSessionKeys(key);
router.push('/gallery');
};
return (
<PasswordForm
callback={onSubmit}
buttonText={constants.CHANGE_PASSWORD}
back={() => router.push('/gallery')}
/>
);
}

View file

@ -1,35 +1,22 @@
import React, { useEffect, useState } from 'react';
import Container from 'components/Container';
import styled from 'styled-components';
import Card from 'react-bootstrap/Card';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import constants from 'utils/strings/constants';
import { Formik, FormikHelpers } from 'formik';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import * as Yup from 'yup';
import { KeyAttributes } from 'types';
import { setKey, SESSION_KEYS, getKey } from 'utils/storage/sessionStorage';
import CryptoWorker, { generateIntermediateKeyAttributes } from 'utils/crypto';
import { SESSION_KEYS, getKey } from 'utils/storage/sessionStorage';
import CryptoWorker, {
generateAndSaveIntermediateKeyAttributes,
setSessionKeys,
} from 'utils/crypto';
import { logoutUser } from 'services/userService';
import { isFirstLogin } from 'utils/storage';
import { Spinner } from 'react-bootstrap';
const Image = styled.img`
width: 200px;
margin-bottom: 20px;
max-width: 100%;
`;
interface formValues {
passphrase: string;
}
import PassPhraseForm from 'components/PassphraseForm';
export default function Credentials() {
const router = useRouter();
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
const [loading, setLoading] = useState(false);
useEffect(() => {
router.prefetch('/gallery');
@ -47,14 +34,9 @@ export default function Credentials() {
}
}, []);
const verifyPassphrase = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
setLoading(true);
const verifyPassphrase = async (passphrase, setFieldError) => {
try {
const cryptoWorker = await new CryptoWorker();
const { passphrase } = values;
const kek: string = await cryptoWorker.deriveKey(
passphrase,
keyAttributes.kekSalt,
@ -69,21 +51,13 @@ export default function Credentials() {
kek
);
if (isFirstLogin()) {
const intermediateKeyAttributes = await generateIntermediateKeyAttributes(
generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
key
);
setData(LS_KEYS.KEY_ATTRIBUTES, intermediateKeyAttributes);
}
const sessionKeyAttributes = await cryptoWorker.encryptToB64(
key
);
const sessionKey = sessionKeyAttributes.key;
const sessionNonce = sessionKeyAttributes.nonce;
const encryptionKey = sessionKeyAttributes.encryptedData;
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
setSessionKeys(key);
router.push('/gallery');
} catch (e) {
console.error(e);
@ -95,76 +69,20 @@ export default function Credentials() {
`${constants.UNKNOWN_ERROR} ${e.message}`
);
}
setLoading(false);
};
return (
<Container>
{/* <Image alt="vault" src="/vault.png" /> */}
<Card
style={{ minWidth: '320px', padding: '40px 30px' }}
className="text-center"
>
<Card.Body>
<Card.Title style={{ marginBottom: '24px' }}>
{constants.ENTER_PASSPHRASE}
</Card.Title>
<Formik<formValues>
initialValues={{ passphrase: '' }}
onSubmit={verifyPassphrase}
validationSchema={Yup.object().shape({
passphrase: Yup.string().required(
constants.REQUIRED
),
})}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit,
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Form.Control
type="password"
placeholder={
constants.RETURN_PASSPHRASE_HINT
}
value={values.passphrase}
onChange={handleChange('passphrase')}
onBlur={handleBlur('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase
)}
disabled={loading}
autoFocus={true}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Button block type="submit" disabled={loading}>
{loading ? (
<Spinner animation="border" />
) : (
constants.VERIFY_PASSPHRASE
)}
</Button>
<br />
<div>
<a href="#" onClick={logoutUser}>
{constants.LOGOUT}
</a>
</div>
</Form>
)}
</Formik>
</Card.Body>
</Card>
</Container>
<PassPhraseForm
callback={verifyPassphrase}
title={constants.ENTER_PASSPHRASE}
placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={constants.VERIFY_PASSPHRASE}
fieldType="password"
alternateOption={{
text: constants.FORGOT_PASSWORD,
click: () => router.push('/recover'),
}}
back={logoutUser}
/>
);
}

View file

@ -32,6 +32,7 @@ import UploadButton from './components/UploadButton';
import { checkConnectivity } from 'utils/common';
import { isFirstLogin, setIsFirstLogin } from 'utils/storage';
import { logoutUser } from 'services/userService';
import { MessageDialog } from 'components/MessageDailog';
const DATE_CONTAINER_HEIGHT = 45;
const IMAGE_CONTAINER_HEIGHT = 200;
const NO_OF_PAGES = 2;
@ -102,16 +103,6 @@ const ListContainer = styled.div<{ columns: number }>`
}
`;
const Image = styled.img`
width: 200px;
max-width: 100%;
display: block;
text-align: center;
margin-left: auto;
margin-right: auto;
margin-bottom: 20px;
`;
const DateContainer = styled.div`
padding-top: 15px;
`;
@ -163,6 +154,7 @@ export default function Gallery(props: Props) {
const [isFirstLoad, setIsFirstLoad] = useState(false);
const [selected, setSelected] = useState<selectedState>({ count: 0 });
const [confirmAction, setConfirmAction] = useState<CONFIRM_ACTION>(null);
const [requestFailed, setRequestFailed] = useState(false);
const loadingBar = useRef(null);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
@ -435,6 +427,14 @@ export default function Gallery(props: Props) {
callback={confirmCallbacks.get(confirmAction)}
action={confirmAction}
/>
<MessageDialog
show={requestFailed}
onHide={() => setRequestFailed(false)}
attributes={{
title: constants.UNKNOWN_ERROR,
ok: true,
}}
/>
<Collections
collections={collections}
selected={Number(router.query.collection)}
@ -452,15 +452,30 @@ export default function Gallery(props: Props) {
files={data}
collections={collections}
setConfirmAction={setConfirmAction}
somethingWentWrong={() => setRequestFailed(true)}
/>
<UploadButton openFileUploader={props.openFileUploader} />
{!isFirstLoad && data.length == 0 ? (
<Jumbotron>
<Image alt="vault" src="/vault.png" />
<Button variant="primary" onClick={props.openFileUploader}>
<div
style={{
height: '60%',
display: 'grid',
placeItems: 'center',
}}
>
<Button
variant="outline-success"
onClick={props.openFileUploader}
style={{
paddingLeft: '32px',
paddingRight: '32px',
paddingTop: '12px',
paddingBottom: '12px',
}}
>
{constants.UPLOAD_FIRST_PHOTO}
</Button>
</Jumbotron>
</div>
) : filteredData.length ? (
<Container>
<AutoSizer>

View file

@ -1,31 +1,17 @@
import React, { useState, useEffect, useContext } from 'react';
import Container from 'components/Container';
import styled from 'styled-components';
import Card from 'react-bootstrap/Card';
import Form from 'react-bootstrap/Form';
import React, { useState, useEffect } from 'react';
import constants from 'utils/strings/constants';
import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import Button from 'react-bootstrap/Button';
import { logoutUser, putAttributes } from 'services/userService';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { getKey, SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { B64EncryptionResult } from 'services/uploadService';
import CryptoWorker from 'utils/crypto';
import { generateIntermediateKeyAttributes } from 'utils/crypto';
import { Spinner } from 'react-bootstrap';
const Image = styled.img`
width: 200px;
margin-bottom: 20px;
max-width: 100%;
`;
interface formValues {
passphrase: string;
confirm: string;
}
import CryptoWorker, {
setSessionKeys,
generateAndSaveIntermediateKeyAttributes,
} from 'utils/crypto';
import PasswordForm from 'components/PasswordForm';
import { KeyAttributes } from 'types';
import { setJustSignedUp } from 'utils/storage';
export interface KEK {
key: string;
@ -34,12 +20,11 @@ export interface KEK {
}
export default function Generate() {
const [loading, setLoading] = useState(false);
const [token, setToken] = useState<string>();
const router = useRouter();
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
router.prefetch('/gallery');
const user = getData(LS_KEYS.USER);
if (!user?.token) {
@ -51,177 +36,72 @@ export default function Generate() {
}
}, []);
const onSubmit = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
setLoading(true);
const onSubmit = async (passphrase, setFieldError) => {
const cryptoWorker = await new CryptoWorker();
const masterKey: string = await cryptoWorker.generateEncryptionKey();
const recoveryKey: string = await cryptoWorker.generateEncryptionKey();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
let kek: KEK;
try {
const { passphrase, confirm } = values;
if (passphrase === confirm) {
const cryptoWorker = await new CryptoWorker();
const key: string = await cryptoWorker.generateMasterKey();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
let kek: KEK;
try {
kek = await cryptoWorker.deriveSensitiveKey(
passphrase,
kekSalt
);
} catch (e) {
setFieldError(
'confirm',
constants.PASSWORD_GENERATION_FAILED
);
return;
}
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(
key,
kek.key
);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(
keyPair.privateKey,
key
);
const keyAttributes = {
kekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData,
keyDecryptionNonce: encryptedKeyAttributes.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey:
encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
};
await putAttributes(
token,
getData(LS_KEYS.USER).name,
keyAttributes
);
setData(
LS_KEYS.KEY_ATTRIBUTES,
await generateIntermediateKeyAttributes(
passphrase,
keyAttributes,
key
)
);
const sessionKeyAttributes = await cryptoWorker.encryptToB64(
key
);
const sessionKey = sessionKeyAttributes.key;
const sessionNonce = sessionKeyAttributes.nonce;
const encryptionKey = sessionKeyAttributes.encryptedData;
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
router.push('/gallery');
} else {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
}
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
} finally {
setLoading(false);
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
return;
}
const masterKeyEncryptedWithKek: B64EncryptionResult = await cryptoWorker.encryptToB64(
masterKey,
kek.key
);
const masterKeyEncryptedWithRecoveryKey: B64EncryptionResult = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const recoveryKeyEncryptedWithMasterKey: B64EncryptionResult = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const keyPair = await cryptoWorker.generateKeyPair();
const encryptedKeyPairAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(
keyPair.privateKey,
masterKey
);
const keyAttributes: KeyAttributes = {
kekSalt,
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
publicKey: keyPair.publicKey,
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce,
opsLimit: kek.opsLimit,
memLimit: kek.memLimit,
masterKeyEncryptedWithRecoveryKey:
masterKeyEncryptedWithRecoveryKey.encryptedData,
masterKeyDecryptionNonce: masterKeyEncryptedWithRecoveryKey.nonce,
recoveryKeyEncryptedWithMasterKey:
recoveryKeyEncryptedWithMasterKey.encryptedData,
recoveryKeyDecryptionNonce: recoveryKeyEncryptedWithMasterKey.nonce,
};
await putAttributes(token, getData(LS_KEYS.USER).name, keyAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
masterKey
);
setSessionKeys(masterKey);
setJustSignedUp(true);
router.push('/gallery');
};
return (
<Container>
{/* <Image alt="vault" src="/vault.png" style={{ paddingBottom: '40px' }} /> */}
<Card style={{ maxWidth: '540px', padding: '20px' }}>
<Card.Body>
<div
className="text-center"
style={{ marginBottom: '40px' }}
>
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
{constants.PASSPHRASE_DISCLAIMER()}
</div>
<Formik<formValues>
initialValues={{ passphrase: '', confirm: '' }}
validationSchema={Yup.object().shape({
passphrase: Yup.string().required(
constants.REQUIRED
),
confirm: Yup.string().required(constants.REQUIRED),
})}
onSubmit={onSubmit}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
handleSubmit,
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Form.Control
type="password"
placeholder={constants.PASSPHRASE_HINT}
value={values.passphrase}
onChange={handleChange('passphrase')}
onBlur={handleBlur('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase
)}
autoFocus={true}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
type="password"
placeholder={
constants.PASSPHRASE_CONFIRM
}
value={values.confirm}
onChange={handleChange('confirm')}
onBlur={handleBlur('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.confirm}
</Form.Control.Feedback>
</Form.Group>
<Button
type="submit"
block
disabled={loading}
style={{ marginTop: '28px' }}
>
{loading ? (
<Spinner animation="border" />
) : (
constants.SET_PASSPHRASE
)}
</Button>
</Form>
)}
</Formik>
<div className="text-center" style={{ marginTop: '20px' }}>
<a href="#" onClick={logoutUser}>
{constants.LOGOUT}
</a>
</div>
</Card.Body>
</Card>
</Container>
<>
<PasswordForm
callback={onSubmit}
buttonText={constants.SET_PASSPHRASE}
back={logoutUser}
/>
</>
);
}

View file

@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { KeyAttributes } from 'types';
import CryptoWorker, { setSessionKeys } from 'utils/crypto';
import PassPhraseForm from 'components/PassphraseForm';
import { MessageDialog } from 'components/MessageDailog';
export default function Recover() {
const router = useRouter();
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
const [messageDialogView, SetMessageDialogView] = useState(false);
useEffect(() => {
router.prefetch('/gallery');
const user = getData(LS_KEYS.USER);
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
if (!user?.token) {
router.push('/');
} else if (!keyAttributes) {
router.push('/generate');
} else {
setKeyAttributes(keyAttributes);
}
}, []);
const recover = async (recoveryKey: string, setFieldError) => {
try {
const cryptoWorker = await new CryptoWorker();
let masterKey: string = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey,
keyAttributes.masterKeyDecryptionNonce,
await cryptoWorker.fromHex(recoveryKey)
);
setSessionKeys(masterKey);
router.push('/changePassword');
} catch (e) {
console.error(e);
setFieldError('passphrase', constants.INCORRECT_RECOVERY_KEY);
}
};
return (
<>
<PassPhraseForm
callback={recover}
fieldType="text"
title={constants.RECOVER_ACCOUNT}
placeholder={constants.RETURN_RECOVERY_KEY_HINT}
buttonText={constants.RECOVER}
alternateOption={{
text: constants.NO_RECOVERY_KEY,
click: () => SetMessageDialogView(true),
}}
back={router.back}
/>
<MessageDialog
show={messageDialogView}
onHide={() => SetMessageDialogView(false)}
attributes={{
title: constants.SORRY,
ok: true,
}}
>
{constants.NO_RECOVERY_KEY_MESSAGE}
</MessageDialog>
</>
);
}

View file

@ -215,7 +215,7 @@ export const AddCollection = async (
const worker = await new CryptoWorker();
const encryptionKey = await getActualKey();
const token = getToken();
const collectionKey: string = await worker.generateMasterKey();
const collectionKey: string = await worker.generateEncryptionKey();
const {
encryptedData: encryptedKey,
nonce: keyDecryptionNonce,

View file

@ -7,6 +7,20 @@ import { clearData } from 'utils/storage/localStorage';
import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key';
export interface UpdatedKey {
kekSalt: string;
encryptedKey: string;
keyDecryptionNonce: string;
memLimit: number;
opsLimit: number;
}
export interface RecoveryKey {
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
const ENDPOINT = getEndpoint();
export interface user {
@ -31,10 +45,26 @@ export const putAttributes = (
name: string,
keyAttributes: KeyAttributes
) => {
console.log('name ' + name);
return HTTPService.put(
`${ENDPOINT}/users/attributes`,
{ name: name, keyAttributes: keyAttributes },
{ name: name ? name : '', keyAttributes: keyAttributes },
null,
{
'X-Auth-Token': token,
}
);
};
export const setKeys = (token: string, updatedKey: UpdatedKey) => {
return HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
'X-Auth-Token': token,
});
};
export const SetRecoveryKey = (token: string, recoveryKey: RecoveryKey) => {
return HTTPService.put(
`${ENDPOINT}/users/recovery-key`,
recoveryKey,
null,
{
'X-Auth-Token': token,

View file

@ -7,6 +7,10 @@ export interface KeyAttributes {
publicKey: string;
encryptedSecretKey: string;
secretKeyDecryptionNonce: string;
masterKeyEncryptedWithRecoveryKey: string;
masterKeyDecryptionNonce: string;
recoveryKeyEncryptedWithMasterKey: string;
recoveryKeyDecryptionNonce: string;
}
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;

View file

@ -16,3 +16,19 @@ export function getFileExtension(fileName): string {
export function runningInBrowser() {
return typeof window !== 'undefined';
}
export function downloadAsFile(filename: string, content: string) {
const file = new Blob([content], {
type: 'text/plain',
});
var a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
}

View file

@ -3,16 +3,20 @@ import { B64EncryptionResult } from 'services/uploadService';
import { KeyAttributes } from 'types';
import * as Comlink from 'comlink';
import { runningInBrowser } from 'utils/common';
import { SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { getActualKey, getToken } from 'utils/common/key';
import { SetRecoveryKey } from 'services/userService';
const CryptoWorker: any =
runningInBrowser() &&
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
export async function generateIntermediateKeyAttributes(
export async function generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
existingKeyAttributes,
key
): Promise<KeyAttributes> {
) {
const cryptoWorker = await new CryptoWorker();
const intermediateKekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
const intermediateKek: KEK = await cryptoWorker.deriveIntermediateKey(
@ -23,16 +27,84 @@ export async function generateIntermediateKeyAttributes(
key,
intermediateKek.key
);
return {
const updatedKeyAttributes = Object.assign(existingKeyAttributes, {
kekSalt: intermediateKekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData,
keyDecryptionNonce: encryptedKeyAttributes.nonce,
publicKey: keyAttributes.publicKey,
encryptedSecretKey: keyAttributes.encryptedSecretKey,
secretKeyDecryptionNonce: keyAttributes.secretKeyDecryptionNonce,
opsLimit: intermediateKek.opsLimit,
memLimit: intermediateKek.memLimit,
};
});
setData(LS_KEYS.KEY_ATTRIBUTES, updatedKeyAttributes);
}
export const setSessionKeys = async (key: string) => {
const cryptoWorker = await new CryptoWorker();
const sessionKeyAttributes = await cryptoWorker.encryptToB64(key);
const sessionKey = sessionKeyAttributes.key;
const sessionNonce = sessionKeyAttributes.nonce;
const encryptionKey = sessionKeyAttributes.encryptedData;
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
};
export const getRecoveryKey = async () => {
let recoveryKey = null;
try {
const cryptoWorker = await new CryptoWorker();
const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const {
recoveryKeyEncryptedWithMasterKey,
recoveryKeyDecryptionNonce,
} = keyAttributes;
const masterKey = await getActualKey();
if (recoveryKeyEncryptedWithMasterKey) {
recoveryKey = await cryptoWorker.decryptB64(
recoveryKeyEncryptedWithMasterKey,
recoveryKeyDecryptionNonce,
masterKey
);
} else {
recoveryKey = await createNewRecoveryKey();
}
recoveryKey = await cryptoWorker.toHex(recoveryKey);
} catch (e) {
console.error('getRecoveryKey failed', e);
} finally {
return recoveryKey;
}
};
async function createNewRecoveryKey() {
const masterKey = await getActualKey();
const existingAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const cryptoWorker = await new CryptoWorker();
const recoveryKey = await cryptoWorker.generateEncryptionKey();
const encryptedMasterKey: B64EncryptionResult = await cryptoWorker.encryptToB64(
masterKey,
recoveryKey
);
const encryptedRecoveryKey: B64EncryptionResult = await cryptoWorker.encryptToB64(
recoveryKey,
masterKey
);
const recoveryKeyAttributes = {
masterKeyEncryptedWithRecoveryKey: encryptedMasterKey.encryptedData,
masterKeyDecryptionNonce: encryptedMasterKey.nonce,
recoveryKeyEncryptedWithMasterKey: encryptedRecoveryKey.encryptedData,
recoveryKeyDecryptionNonce: encryptedRecoveryKey.nonce,
};
await SetRecoveryKey(getToken(), recoveryKeyAttributes);
const updatedKeyAttributes = Object.assign(
existingAttributes,
recoveryKeyAttributes
);
setData(LS_KEYS.KEY_ATTRIBUTES, updatedKeyAttributes);
return recoveryKey;
}
export default CryptoWorker;

View file

@ -313,7 +313,7 @@ export async function deriveIntermediateKey(passphrase: string, salt: string) {
};
}
export async function generateMasterKey() {
export async function generateEncryptionKey() {
await sodium.ready;
return await toB64(sodium.crypto_kdf_keygen());
}
@ -361,3 +361,12 @@ export async function fromString(input: string) {
await sodium.ready;
return sodium.from_string(input);
}
export async function toHex(input: string) {
await sodium.ready;
return sodium.to_hex(await fromB64(input));
}
export async function fromHex(input: string) {
await sodium.ready;
return await toB64(sodium.from_hex(input));
}

View file

@ -6,3 +6,10 @@ export const isFirstLogin = () =>
export function setIsFirstLogin(status) {
setData(LS_KEYS.IS_FIRST_LOGIN, { status });
}
export const justSignedUp = () =>
getData(LS_KEYS.JUST_SIGNED_UP)?.status ?? false;
export function setJustSignedUp(status) {
setData(LS_KEYS.JUST_SIGNED_UP, { status });
}

View file

@ -4,6 +4,7 @@ export enum LS_KEYS {
KEY_ATTRIBUTES = 'keyAttributes',
SUBSCRIPTION = 'subscription',
IS_FIRST_LOGIN = 'isFirstLogin',
JUST_SIGNED_UP = 'justSignedUp',
}
export const setData = (key: LS_KEYS, value: object) => {

View file

@ -112,7 +112,7 @@ const englishConstants = {
'sorry, this operation is currently not supported on the web, please install the desktop app',
DOWNLOAD_APP: 'download',
APP_DOWNLOAD_URL: 'https://github.com/ente-io/bhari-frame/releases/',
EXPORT: 'export ',
EXPORT: 'export data',
SUBSCRIPTION_PLAN: 'subscription plan',
USAGE_DETAILS: 'usage',
FREE_SUBSCRIPTION_INFO: (expiryTime) => (
@ -160,6 +160,24 @@ const englishConstants = {
SYNC_FAILED:
'failed to sync with remote server, please refresh page to try again',
PASSWORD_GENERATION_FAILED: `your browser was unable to generate a strong enough password that meets ente's encryption standards, please try using the mobile app or another browser`,
CHANGE_PASSWORD: 'change password',
GO_BACK: 'go back',
DOWNLOAD_RECOVERY_KEY: 'recovery key',
SAVE_LATER: 'save later',
SAVE: 'save',
RECOVERY_KEY_DESCRIPTION: 'if you forget your password, the only way you can recover your data is with this key',
KEY_NOT_STORED_DISCLAIMER: 'we don\'t store this key, so please save this in a safe place',
RECOVERY_KEY_FILENAME: 'ente-recovery-key.txt',
FORGOT_PASSWORD: 'forgot password?',
RECOVER_ACCOUNT: 'recover account',
RETURN_RECOVERY_KEY_HINT: 'recovery key',
RECOVER: 'recover',
NO_RECOVERY_KEY: 'no recovery key?',
INCORRECT_RECOVERY_KEY: 'incorrect recovery key',
SORRY: 'sorry',
NO_RECOVERY_KEY_MESSAGE:
'due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key',
OK: 'ok',
};
export default englishConstants;

View file

@ -104,8 +104,8 @@ export class Crypto {
return libsodium.encryptUTF8(data, key);
}
async generateMasterKey() {
return libsodium.generateMasterKey();
async generateEncryptionKey() {
return libsodium.generateEncryptionKey();
}
async generateSaltToDeriveKey() {
@ -131,6 +131,12 @@ export class Crypto {
async fromB64(string) {
return libsodium.fromB64(string);
}
async toHex(string) {
return libsodium.toHex(string);
}
async fromHex(string) {
return libsodium.fromHex(string);
}
}
Comlink.expose(Crypto);