mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
adding encryption
This commit is contained in:
parent
5e4e491483
commit
ade221be7d
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
1
src/constants/values.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const MASTER_PASSWORD_LENGTH = 4096
|
@ -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"})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
6
src/utils/build-encryption-password.ts
Normal file
6
src/utils/build-encryption-password.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function buildEncryptionPassword(
|
||||||
|
password: string,
|
||||||
|
email: string,
|
||||||
|
): string {
|
||||||
|
return `${password}-blablabla-do-not-bruteforce-passwords-${email}`
|
||||||
|
}
|
5
src/utils/decrypt-string.ts
Normal file
5
src/utils/decrypt-string.ts
Normal 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)
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
42
src/utils/encrypt-user-note.ts
Normal file
42
src/utils/encrypt-user-note.ts
Normal 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)
|
||||||
|
}
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user