From ade221be7df362abf0cc00ee760a0841bf323f6e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 16 Oct 2022 18:37:09 +0200 Subject: [PATCH] adding encryption --- package.json | 3 ++ src/AuthContext/AuthContext.ts | 12 ++++++ src/AuthContext/AuthContextProvider.tsx | 36 +++++++++++++++- src/constants/values.ts | 1 + .../CompleteAccountRoute/PasswordForm.tsx | 26 ++++++++---- src/server-types.ts | 10 +++++ src/utils/build-encryption-password.ts | 6 +++ src/utils/decrypt-string.ts | 5 +++ src/utils/encrypt-string.ts | 4 +- src/utils/encrypt-user-note.ts | 42 +++++++++++++++++++ src/utils/index.ts | 4 ++ 11 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 src/constants/values.ts create mode 100644 src/utils/build-encryption-password.ts create mode 100644 src/utils/decrypt-string.ts create mode 100644 src/utils/encrypt-user-note.ts diff --git a/package.json b/package.json index 1e69cb5..e9ec628 100755 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "react-icons": "^4.4.0", "react-router-dom": "^6.4.2", "react-use": "^17.4.0", + "secure-random-password": "^0.2.3", "ua-parser-js": "^1.0.2", "yup": "^0.32.11" }, "devDependencies": { + "@types/crypto-js": "^4.1.1", "@types/date-fns": "^2.6.0", "@types/openpgp": "^4.4.18", "@types/react": "^18.0.17", @@ -39,6 +41,7 @@ "@types/react-icons": "^3.0.0", "@types/react-router": "^5.1.19", "@types/react-router-dom": "^5.3.3", + "@types/secure-random-password": "^0.2.1", "@types/ua-parser-js": "^0.7.36", "@types/yup": "^0.32.0", "@typescript-eslint/eslint-plugin": "^5.40.0", diff --git a/src/AuthContext/AuthContext.ts b/src/AuthContext/AuthContext.ts index 6a12cec..e27a8a5 100644 --- a/src/AuthContext/AuthContext.ts +++ b/src/AuthContext/AuthContext.ts @@ -7,6 +7,9 @@ interface AuthContextTypeBase { isAuthenticated: boolean login: (user: User, callback: () => void) => Promise logout: () => void + _decryptContent: (content: string) => string + _encryptContent: (content: string) => string + _setMasterPassword: (masterPassword: string) => void } interface AuthContextTypeAuthenticated { @@ -32,6 +35,15 @@ const AuthContext = createContext({ logout: () => { throw new Error("logout() not implemented") }, + _decryptContent: () => { + throw new Error("_decryptContent() not implemented") + }, + _encryptContent: () => { + throw new Error("_encryptContent() not implemented") + }, + _setMasterPassword: () => { + throw new Error("_setMasterPassword() not implemented") + }, }) export default AuthContext diff --git a/src/AuthContext/AuthContextProvider.tsx b/src/AuthContext/AuthContextProvider.tsx index 94f842e..d07bf24 100644 --- a/src/AuthContext/AuthContextProvider.tsx +++ b/src/AuthContext/AuthContextProvider.tsx @@ -1,4 +1,11 @@ -import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react" +import { + ReactElement, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from "react" import {useLocalStorage} from "react-use" import {AxiosError} from "axios" @@ -12,6 +19,7 @@ import { refreshToken, } from "~/apis" import {client} from "~/constants/axios-client" +import {decryptString, encryptString} from "~/utils" import AuthContext, {AuthContextType} from "./AuthContext" @@ -22,6 +30,7 @@ export interface AuthContextProviderProps { export default function AuthContextProvider({ children, }: AuthContextProviderProps): ReactElement { + const [masterPassword, setMasterPassword] = useState(null) const [user, setUser] = useLocalStorage( "_global-context-auth-user", null, @@ -41,6 +50,28 @@ export default function AuthContextProvider({ callback?.() }, []) + const encryptContent = useCallback( + (content: string) => { + if (!masterPassword) { + throw new Error("Master password not set.") + } + + return encryptString(content, masterPassword) + }, + [masterPassword], + ) + + const decryptContent = useCallback( + (content: string) => { + if (!masterPassword) { + throw new Error("Master password not set.") + } + + return decryptString(content, masterPassword) + }, + [masterPassword], + ) + const {mutateAsync: refresh} = useMutation< RefreshTokenResult, AxiosError, @@ -55,6 +86,9 @@ export default function AuthContextProvider({ login, logout, isAuthenticated: user !== null, + _setMasterPassword: setMasterPassword, + _encryptContent: encryptContent, + _decryptContent: decryptContent, }), [refresh, login, logout], ) diff --git a/src/constants/values.ts b/src/constants/values.ts new file mode 100644 index 0000000..83df337 --- /dev/null +++ b/src/constants/values.ts @@ -0,0 +1 @@ +export const MASTER_PASSWORD_LENGTH = 4096 diff --git a/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx b/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx index cc42d9f..a1dedc0 100644 --- a/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx +++ b/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx @@ -3,15 +3,17 @@ import {useFormik} from "formik" import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md" import {generateKey} from "openpgp" import React, {ReactElement, useMemo} from "react" +import passwordGenerator from "secure-random-password" import {LoadingButton} from "@mui/lab" import {Box, Grid, InputAdornment, Typography} from "@mui/material" +import {useMutation} from "@tanstack/react-query" import {PasswordField} from "~/components" -import {encryptString} from "~/utils" +import {buildEncryptionPassword, encryptString} from "~/utils" import {isDev} from "~/constants/development" import {useUser} from "~/hooks" -import {useMutation} from "@tanstack/react-query" +import {MASTER_PASSWORD_LENGTH} from "~/constants/values" interface Form { password: string @@ -29,7 +31,7 @@ const schema = yup.object().shape({ export default function PasswordForm(): ReactElement { const user = useUser() - const {} = useMutation() + const {} = useMutation<>() const awaitGenerateKey = useMemo( () => generateKey({ @@ -52,12 +54,22 @@ export default function PasswordForm(): ReactElement { try { const keyPair = await awaitGenerateKey - const encryptedPrivateKey = encryptString( - keyPair.privateKey, + const masterPassword = passwordGenerator.randomPassword({ + length: MASTER_PASSWORD_LENGTH, + }) + + const encryptionPassword = buildEncryptionPassword( + values.password, + user.email.address, + ) + const encryptedMasterPassword = encryptString( + masterPassword, `${values.password}-${user.email.address}`, ) - - console.log(encryptedPrivateKey) + const encryptedPrivateKey = encryptString( + keyPair.privateKey, + masterPassword, + ) } catch (error) { setErrors({detail: "An error occurred"}) } diff --git a/src/server-types.ts b/src/server-types.ts index 0714df6..f32d4df 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -22,6 +22,11 @@ export enum Language { EN_US = "en_US", } +export enum Theme { + LIGHT = "light", + DARK = "dark", +} + export interface User { id: string createdAt: Date @@ -72,3 +77,8 @@ export interface Alias { imageProxyFormat: ImageProxyFormatType imageProxyUserAgent: ProxyUserAgentType } + +export interface UserNote { + theme: Theme + privateKey: string +} diff --git a/src/utils/build-encryption-password.ts b/src/utils/build-encryption-password.ts new file mode 100644 index 0000000..e903996 --- /dev/null +++ b/src/utils/build-encryption-password.ts @@ -0,0 +1,6 @@ +export default function buildEncryptionPassword( + password: string, + email: string, +): string { + return `${password}-blablabla-do-not-bruteforce-passwords-${email}` +} diff --git a/src/utils/decrypt-string.ts b/src/utils/decrypt-string.ts new file mode 100644 index 0000000..946a58e --- /dev/null +++ b/src/utils/decrypt-string.ts @@ -0,0 +1,5 @@ +import Crypto from "crypto-js" + +export default function decryptString(value: string, password: string): string { + return Crypto.AES.decrypt(value, password).toString(Crypto.enc.Utf8) +} diff --git a/src/utils/encrypt-string.ts b/src/utils/encrypt-string.ts index 679e04a..05040fd 100644 --- a/src/utils/encrypt-string.ts +++ b/src/utils/encrypt-string.ts @@ -1,7 +1,5 @@ import Crypto from "crypto-js" export default function encryptString(value: string, password: string): string { - const key = `${password}-${process.env.NODE_PUBLIC_NEXT_PUBLIC_PUBLIC_ENCRYPTION_SAL}` - - return Crypto.AES.encrypt(value, key).toString() + return Crypto.AES.encrypt(value, password).toString() } diff --git a/src/utils/encrypt-user-note.ts b/src/utils/encrypt-user-note.ts new file mode 100644 index 0000000..769558b --- /dev/null +++ b/src/utils/encrypt-user-note.ts @@ -0,0 +1,42 @@ +import * as yup from "yup" + +import {Theme, UserNote} from "~/server-types" + +import decryptString from "./decrypt-string" +import encryptString from "./encrypt-string" + +export const USER_NOTE_SCHEMA = yup.object().shape({ + privateKey: yup.string().required(), + theme: yup.string().oneOf(Object.values(Theme)).required(), +}) + +export function createUserNote( + privateKey: string, + theme: Theme = Theme.LIGHT, +): UserNote { + return { + theme, + privateKey, + } +} + +export function decryptUserNote( + encryptedUserNote: string, + password: string, +): UserNote { + const data = decryptString(encryptedUserNote, password) + const userNote = JSON.parse(data) + + USER_NOTE_SCHEMA.validateSync(userNote) + + return userNote +} + +export function encryptUserNote(userNote: UserNote, password: string): string { + const data = JSON.stringify({ + ...userNote, + encryptionDate: new Date(), + }) + + return encryptString(data, password) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 9bbea87..1c835c7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,3 +6,7 @@ export * from "./parse-fastapi-error" export {default as parseFastapiError} from "./parse-fastapi-error" export * from "./when-element-has-bounds" export {default as whenElementHasBounds} from "./when-element-has-bounds" +export * from "./build-encryption-password" +export {default as buildEncryptionPassword} from "./build-encryption-password" +export * from "./decrypt-string" +export {default as decryptString} from "./decrypt-string"