commit
4f5f0fe378
BIN
public/vault.png
BIN
public/vault.png
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
|
@ -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;
|
||||
|
@ -44,12 +44,12 @@ function ConfirmDialog({ callback, action, ...props }: Props) {
|
|||
</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]}
|
||||
|
|
61
src/components/MessageDailog.tsx
Normal file
61
src/components/MessageDailog.tsx
Normal 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>
|
||||
);
|
||||
}
|
117
src/components/PassphraseForm.tsx
Normal file
117
src/components/PassphraseForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
135
src/components/PasswordForm.tsx
Normal file
135
src/components/PasswordForm.tsx
Normal 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;
|
86
src/components/RecoveryKeyModal.tsx
Normal file
86
src/components/RecoveryKeyModal.tsx
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
79
src/pages/changePassword/index.tsx
Normal file
79
src/pages/changePassword/index.tsx
Normal 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')}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
try {
|
||||
const { passphrase, confirm } = values;
|
||||
if (passphrase === confirm) {
|
||||
const onSubmit = async (passphrase, setFieldError) => {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const key: string = await cryptoWorker.generateMasterKey();
|
||||
const masterKey: string = await cryptoWorker.generateEncryptionKey();
|
||||
const recoveryKey: string = await cryptoWorker.generateEncryptionKey();
|
||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
||||
let kek: KEK;
|
||||
try {
|
||||
kek = await cryptoWorker.deriveSensitiveKey(
|
||||
passphrase,
|
||||
kekSalt
|
||||
);
|
||||
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||
} catch (e) {
|
||||
setFieldError(
|
||||
'confirm',
|
||||
constants.PASSWORD_GENERATION_FAILED
|
||||
);
|
||||
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||
return;
|
||||
}
|
||||
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(
|
||||
key,
|
||||
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,
|
||||
key
|
||||
masterKey
|
||||
);
|
||||
|
||||
const keyAttributes = {
|
||||
const keyAttributes: KeyAttributes = {
|
||||
kekSalt,
|
||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||
keyDecryptionNonce: encryptedKeyAttributes.nonce,
|
||||
encryptedKey: masterKeyEncryptedWithKek.encryptedData,
|
||||
keyDecryptionNonce: masterKeyEncryptedWithKek.nonce,
|
||||
publicKey: keyPair.publicKey,
|
||||
encryptedSecretKey:
|
||||
encryptedKeyPairAttributes.encryptedData,
|
||||
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
|
||||
);
|
||||
|
||||
setData(
|
||||
LS_KEYS.KEY_ATTRIBUTES,
|
||||
await generateIntermediateKeyAttributes(
|
||||
await putAttributes(token, getData(LS_KEYS.USER).name, keyAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
key
|
||||
)
|
||||
masterKey
|
||||
);
|
||||
|
||||
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(masterKey);
|
||||
setJustSignedUp(true);
|
||||
router.push('/gallery');
|
||||
} else {
|
||||
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
|
||||
}
|
||||
} catch (e) {
|
||||
setFieldError(
|
||||
'passphrase',
|
||||
`${constants.UNKNOWN_ERROR} ${e.message}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
<>
|
||||
<PasswordForm
|
||||
callback={onSubmit}
|
||||
buttonText={constants.SET_PASSPHRASE}
|
||||
back={logoutUser}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
70
src/pages/recover/index.tsx
Normal file
70
src/pages/recover/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue