adding encryption

This commit is contained in:
Myzel394 2022-10-16 18:37:09 +02:00
parent 5e4e491483
commit ade221be7d
11 changed files with 138 additions and 11 deletions

View File

@ -28,10 +28,12 @@
"react-icons": "^4.4.0", "react-icons": "^4.4.0",
"react-router-dom": "^6.4.2", "react-router-dom": "^6.4.2",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"secure-random-password": "^0.2.3",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/date-fns": "^2.6.0", "@types/date-fns": "^2.6.0",
"@types/openpgp": "^4.4.18", "@types/openpgp": "^4.4.18",
"@types/react": "^18.0.17", "@types/react": "^18.0.17",
@ -39,6 +41,7 @@
"@types/react-icons": "^3.0.0", "@types/react-icons": "^3.0.0",
"@types/react-router": "^5.1.19", "@types/react-router": "^5.1.19",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/secure-random-password": "^0.2.1",
"@types/ua-parser-js": "^0.7.36", "@types/ua-parser-js": "^0.7.36",
"@types/yup": "^0.32.0", "@types/yup": "^0.32.0",
"@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/eslint-plugin": "^5.40.0",

View File

@ -7,6 +7,9 @@ interface AuthContextTypeBase {
isAuthenticated: boolean isAuthenticated: boolean
login: (user: User, callback: () => void) => Promise<void> login: (user: User, callback: () => void) => Promise<void>
logout: () => void logout: () => void
_decryptContent: (content: string) => string
_encryptContent: (content: string) => string
_setMasterPassword: (masterPassword: string) => void
} }
interface AuthContextTypeAuthenticated { interface AuthContextTypeAuthenticated {
@ -32,6 +35,15 @@ const AuthContext = createContext<AuthContextType>({
logout: () => { logout: () => {
throw new Error("logout() not implemented") 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 export default AuthContext

View File

@ -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 {useLocalStorage} from "react-use"
import {AxiosError} from "axios" import {AxiosError} from "axios"
@ -12,6 +19,7 @@ import {
refreshToken, refreshToken,
} from "~/apis" } from "~/apis"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {decryptString, encryptString} from "~/utils"
import AuthContext, {AuthContextType} from "./AuthContext" import AuthContext, {AuthContextType} from "./AuthContext"
@ -22,6 +30,7 @@ export interface AuthContextProviderProps {
export default function AuthContextProvider({ export default function AuthContextProvider({
children, children,
}: AuthContextProviderProps): ReactElement { }: AuthContextProviderProps): ReactElement {
const [masterPassword, setMasterPassword] = useState<string | null>(null)
const [user, setUser] = useLocalStorage<User | null>( const [user, setUser] = useLocalStorage<User | null>(
"_global-context-auth-user", "_global-context-auth-user",
null, null,
@ -41,6 +50,28 @@ export default function AuthContextProvider({
callback?.() 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< const {mutateAsync: refresh} = useMutation<
RefreshTokenResult, RefreshTokenResult,
AxiosError, AxiosError,
@ -55,6 +86,9 @@ export default function AuthContextProvider({
login, login,
logout, logout,
isAuthenticated: user !== null, isAuthenticated: user !== null,
_setMasterPassword: setMasterPassword,
_encryptContent: encryptContent,
_decryptContent: decryptContent,
}), }),
[refresh, login, logout], [refresh, login, logout],
) )

1
src/constants/values.ts Normal file
View File

@ -0,0 +1 @@
export const MASTER_PASSWORD_LENGTH = 4096

View File

@ -3,15 +3,17 @@ import {useFormik} from "formik"
import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md" import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md"
import {generateKey} from "openpgp" import {generateKey} from "openpgp"
import React, {ReactElement, useMemo} from "react" import React, {ReactElement, useMemo} from "react"
import passwordGenerator from "secure-random-password"
import {LoadingButton} from "@mui/lab" import {LoadingButton} from "@mui/lab"
import {Box, Grid, InputAdornment, Typography} from "@mui/material" import {Box, Grid, InputAdornment, Typography} from "@mui/material"
import {useMutation} from "@tanstack/react-query"
import {PasswordField} from "~/components" import {PasswordField} from "~/components"
import {encryptString} from "~/utils" import {buildEncryptionPassword, encryptString} from "~/utils"
import {isDev} from "~/constants/development" import {isDev} from "~/constants/development"
import {useUser} from "~/hooks" import {useUser} from "~/hooks"
import {useMutation} from "@tanstack/react-query" import {MASTER_PASSWORD_LENGTH} from "~/constants/values"
interface Form { interface Form {
password: string password: string
@ -29,7 +31,7 @@ const schema = yup.object().shape({
export default function PasswordForm(): ReactElement { export default function PasswordForm(): ReactElement {
const user = useUser() const user = useUser()
const {} = useMutation() const {} = useMutation<>()
const awaitGenerateKey = useMemo( const awaitGenerateKey = useMemo(
() => () =>
generateKey({ generateKey({
@ -52,12 +54,22 @@ export default function PasswordForm(): ReactElement {
try { try {
const keyPair = await awaitGenerateKey const keyPair = await awaitGenerateKey
const encryptedPrivateKey = encryptString( const masterPassword = passwordGenerator.randomPassword({
keyPair.privateKey, length: MASTER_PASSWORD_LENGTH,
})
const encryptionPassword = buildEncryptionPassword(
values.password,
user.email.address,
)
const encryptedMasterPassword = encryptString(
masterPassword,
`${values.password}-${user.email.address}`, `${values.password}-${user.email.address}`,
) )
const encryptedPrivateKey = encryptString(
console.log(encryptedPrivateKey) keyPair.privateKey,
masterPassword,
)
} catch (error) { } catch (error) {
setErrors({detail: "An error occurred"}) setErrors({detail: "An error occurred"})
} }

View File

@ -22,6 +22,11 @@ export enum Language {
EN_US = "en_US", EN_US = "en_US",
} }
export enum Theme {
LIGHT = "light",
DARK = "dark",
}
export interface User { export interface User {
id: string id: string
createdAt: Date createdAt: Date
@ -72,3 +77,8 @@ export interface Alias {
imageProxyFormat: ImageProxyFormatType imageProxyFormat: ImageProxyFormatType
imageProxyUserAgent: ProxyUserAgentType imageProxyUserAgent: ProxyUserAgentType
} }
export interface UserNote {
theme: Theme
privateKey: string
}

View File

@ -0,0 +1,6 @@
export default function buildEncryptionPassword(
password: string,
email: string,
): string {
return `${password}-blablabla-do-not-bruteforce-passwords-${email}`
}

View File

@ -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)
}

View File

@ -1,7 +1,5 @@
import Crypto from "crypto-js" import Crypto from "crypto-js"
export default function encryptString(value: string, password: string): string { 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, password).toString()
return Crypto.AES.encrypt(value, key).toString()
} }

View File

@ -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)
}

View File

@ -6,3 +6,7 @@ export * from "./parse-fastapi-error"
export {default as parseFastapiError} from "./parse-fastapi-error" export {default as parseFastapiError} from "./parse-fastapi-error"
export * from "./when-element-has-bounds" export * from "./when-element-has-bounds"
export {default as whenElementHasBounds} 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"