Using local and session storage to maintain session.

+ Fixed SSR of styled components.
+ Added warninig message in console.
This commit is contained in:
Pushkar Anand 2020-09-13 12:00:07 +05:30
parent 2e10ea441e
commit 5478a2e8a1
12 changed files with 129 additions and 60 deletions

15
.babelrc Normal file
View file

@ -0,0 +1,15 @@
{
"presets": [
"next/babel"
],
"plugins": [
[
"styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}

View file

@ -1,8 +1,12 @@
import React, { useState, createContext } from 'react';
import React, { useEffect, useState } from 'react';
import styled, {createGlobalStyle } from 'styled-components';
import Navbar from 'components/Navbar';
import constants from 'utils/strings/constants';
import 'bootstrap/dist/css/bootstrap.min.css';
import Button from 'react-bootstrap/Button';
import { clearKeys } from 'utils/storage/sessionStorage';
import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
const GlobalStyles = createGlobalStyle`
html, body {
@ -37,23 +41,45 @@ const Image = styled.img`
margin-right: 5px;
`;
export interface IAppContext {
key: string;
setKey: (key: string) => void
}
export const AppContext = createContext<IAppContext>(null);
const FlexContainer = styled.div`
flex: 1;
`;
export default function App({ Component, pageProps }) {
const [key, setKey] = useState<string>();
const router = useRouter();
const [user, setUser] = useState();
useEffect(() => {
const user = getData(LS_KEYS.USER);
setUser(user);
console.log(`%c${constants.CONSOLE_WARNING_STOP}`, 'color: red; font-size: 52px;');
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
router.events.on('routeChangeComplete', () => {
const user = getData(LS_KEYS.USER);
setUser(user);
});
}, []);
const logout = () => {
clearKeys();
clearData();
router.push("/");
}
return (
<AppContext.Provider value={{ key, setKey }}>
<>
<GlobalStyles />
<Navbar>
<Image src="/icon.png" />
{constants.COMPANY_NAME}
<FlexContainer>
<Image src="/icon.png" />
{constants.COMPANY_NAME}
</FlexContainer>
{user && <Button variant='link' onClick={logout}>
<span className="material-icons">power_settings_new</span>
</Button>}
</Navbar>
<Component />
</AppContext.Provider>
</>
);
}

View file

@ -29,6 +29,7 @@ export default class MyDocument extends Document {
sheet.seal()
}
}
render() {
return (
<Html>

View file

@ -6,14 +6,14 @@ 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, SESSION_KEYS } from 'utils/sessionStorage';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import * as Yup from 'yup';
import { hash } from 'utils/crypto/scrypt';
import { strToUint8, base64ToUint8 } from 'utils/crypto/common';
import { AppContext } from 'pages/_app';
import { decrypt } from 'utils/crypto/aes';
import { strToUint8, base64ToUint8, secureRandomString } from 'utils/crypto/common';
import { decrypt, encrypt } from 'utils/crypto/aes';
import { keyAttributes } from 'types';
import { setKey, SESSION_KEYS, getKey } from 'utils/storage/sessionStorage';
const Image = styled.img`
width: 200px;
@ -29,16 +29,16 @@ export default function Credentials() {
const router = useRouter();
const [keyAttributes, setKeyAttributes] = useState<keyAttributes>();
const [loading, setLoading] = useState(false);
const context = useContext(AppContext);
useEffect(() => {
const user = getData(SESSION_KEYS.USER);
const keyAttributes = getData(SESSION_KEYS.KEY_ATTRIBUTES);
const user = getData(LS_KEYS.USER);
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!user?.token) {
router.push('/');
} else if (!keyAttributes) {
router.push('/generate');
} else if (context.key) {
} else if (key) {
router.push('/gallery')
} else {
setKeyAttributes(keyAttributes);
@ -54,7 +54,11 @@ export default function Credentials() {
if (kekHash === keyAttributes.kekHash) {
const key = await decrypt(keyAttributes.encryptedKey, kek, keyAttributes.encryptedKeyIV);
context.setKey(key);
const sessionKey = secureRandomString(32);
const sessionIV = secureRandomString(16);
const encryptionKey = await encrypt(key, sessionKey, sessionIV);
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionIV });
router.push('/gallery');
} else {
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
@ -93,7 +97,7 @@ export default function Credentials() {
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Button block type='submit' disabled={loading}>{constants.SET_PASSPHRASE}</Button>
<Button block type='submit' disabled={loading}>{constants.VERIFY_PASSPHRASE}</Button>
</Form>
)}
</Formik>

View file

@ -1,24 +1,23 @@
import React, { useContext, useEffect } from 'react';
import Link from 'next/link';
import { AppContext } from 'pages/_app';
import { useRouter } from 'next/router';
import Container from 'components/Container';
import Card from 'react-bootstrap/Card';
import Button from 'react-bootstrap/Button';
import { clearData } from 'utils/sessionStorage';
import { clearData } from 'utils/storage/localStorage';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
export default function Gallery() {
const context = useContext(AppContext);
const router = useRouter();
useEffect(() => {
if (!context.key) {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) {
router.push("/");
}
}, []);
const logout = () => {
context.setKey(null);
clearKeys();
clearData();
router.push('/');
}

View file

@ -11,9 +11,9 @@ import { secureRandomString, strToUint8, base64ToUint8, binToBase64 } from 'util
import { hash } from 'utils/crypto/scrypt';
import { encrypt } from 'utils/crypto/aes';
import { putKeyAttributes } from 'services/userService';
import { getData, SESSION_KEYS } from 'utils/sessionStorage';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { AppContext } from 'pages/_app';
import { getKey, SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
const Image = styled.img`
width: 200px;
@ -30,13 +30,13 @@ export default function Generate() {
const [loading, setLoading] = useState(false);
const [token, setToken] = useState<string>();
const router = useRouter();
const context = useContext(AppContext);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
useEffect(() => {
const user = getData(SESSION_KEYS.USER);
const user = getData(LS_KEYS.USER);
if (!user?.token) {
router.push("/");
} else if (context.key) {
} else if (key) {
router.push('/gallery');
} else {
setToken(user.token);
@ -55,11 +55,17 @@ export default function Generate() {
const kekHash = await hash(base64ToUint8(kek), base64ToUint8(kekHashSalt));
const encryptedKeyIV = secureRandomString(16);
const encryptedKey = await encrypt(key, kek, encryptedKeyIV);
await putKeyAttributes(token, {
const keyAttributes = {
kekSalt, kekHashSalt, kekHash,
encryptedKeyIV, encryptedKey,
});
context.setKey(key);
};
await putKeyAttributes(token, keyAttributes);
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
const sessionKey = secureRandomString(32);
const sessionIV = secureRandomString(16);
const encryptionKey = await encrypt(key, sessionKey, sessionIV);
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
setData(LS_KEYS.SESSION, { sessionKey, sessionIV });
router.push('/gallery');
} else {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);

View file

@ -9,7 +9,7 @@ import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import { getOtt } from 'services/userService';
import Container from 'components/Container';
import { setData, SESSION_KEYS, getData } from 'utils/sessionStorage';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
interface formValues {
email: string;
@ -20,7 +20,7 @@ export default function Home() {
const router = useRouter();
useEffect(() => {
const user = getData(SESSION_KEYS.USER);
const user = getData(LS_KEYS.USER);
if (user?.email) {
router.push('/verify');
}
@ -30,7 +30,7 @@ export default function Home() {
try {
setLoading(true);
await getOtt(email);
setData(SESSION_KEYS.USER, { email });
setData(LS_KEYS.USER, { email });
router.push('/verify');
} catch (e) {
setFieldError('email', `${constants.UNKNOWN_ERROR} ${e.message}`);

View file

@ -5,7 +5,7 @@ import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import constants from 'utils/strings/constants';
import styled from 'styled-components';
import { SESSION_KEYS, getData, setData } from 'utils/sessionStorage';
import { LS_KEYS, getData, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
@ -28,7 +28,7 @@ export default function Verify() {
const router = useRouter();
useEffect(() => {
const user = getData(SESSION_KEYS.USER);
const user = getData(LS_KEYS.USER);
if (!user?.email) {
router.push("/");
} else if (user.token) {
@ -42,11 +42,11 @@ export default function Verify() {
try {
setLoading(true);
const resp = await verifyOtt(email, ott);
setData(SESSION_KEYS.USER, {
setData(LS_KEYS.USER, {
email,
token: resp.data.token,
});
setData(SESSION_KEYS.KEY_ATTRIBUTES, resp.data.keyAttributes);
setData(LS_KEYS.KEY_ATTRIBUTES, resp.data.keyAttributes);
if (resp.data.keyAttributes?.encryptedKey) {
router.push("/credentials");
} else {

View file

@ -1,17 +0,0 @@
export enum SESSION_KEYS {
USER='user',
SESSION='session',
KEY_ATTRIBUTES='keyAttributes',
}
export const setData = (key: SESSION_KEYS, value: object) => {
sessionStorage.setItem(key, JSON.stringify(value));
}
export const getData = (key: SESSION_KEYS) => {
return JSON.parse(sessionStorage.getItem(key));
}
export const clearData = () => {
sessionStorage.clear();
}

View file

@ -0,0 +1,17 @@
export enum LS_KEYS {
USER='user',
SESSION='session',
KEY_ATTRIBUTES='keyAttributes',
}
export const setData = (key: LS_KEYS, value: object) => {
localStorage.setItem(key, JSON.stringify(value));
}
export const getData = (key: LS_KEYS) => {
return JSON.parse(localStorage.getItem(key));
}
export const clearData = () => {
localStorage.clear();
}

View file

@ -0,0 +1,15 @@
export enum SESSION_KEYS {
ENCRYPTION_KEY='encryptionKey',
}
export const setKey = (key: SESSION_KEYS, value: object) => {
sessionStorage.setItem(key, JSON.stringify(value));
}
export const getKey = (key: SESSION_KEYS) => {
return JSON.parse(sessionStorage.getItem(key));
}
export const clearKeys = () => {
sessionStorage.clear();
}

View file

@ -25,6 +25,7 @@ const englishConstants = {
ENTER_PASSPHRASE: 'Please enter your passphrase.',
RETURN_PASSPHRASE_HINT: 'That thing you promised to never forget.',
SET_PASSPHRASE: 'Set Passphrase',
VERIFY_PASSPHRASE: 'Verify Passphrase',
INCORRECT_PASSPHRASE: 'Incorrect Passphrase',
ENTER_ENC_PASSPHRASE: 'Please enter a passphrase that we can use to encrypt your data.',
PASSPHRASE_DISCLAIMER: () => (
@ -36,6 +37,8 @@ const englishConstants = {
PASSPHRASE_HINT: 'Something you will never forget',
PASSPHRASE_CONFIRM: 'Please repeat it once more',
PASSPHRASE_MATCH_ERROR: `Passphrase didn't match`,
CONSOLE_WARNING_STOP: 'STOP!',
CONSOLE_WARNING_DESC: `This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone's account, it is a scam and will give them access to your account.`
};
export default englishConstants;