diff --git a/packages/chainsafex/package.json b/packages/chainsafex/package.json index d2661125cd..7f5009585d 100644 --- a/packages/chainsafex/package.json +++ b/packages/chainsafex/package.json @@ -9,7 +9,7 @@ "react": "^16.14.0", "react-dom": "^16.14.0", "react-scripts": "3.4.4", - "yup": "^0.31.1" + "yup": "^0.32.8" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", diff --git a/packages/common-components/src/CheckboxInput/CheckboxInput.tsx b/packages/common-components/src/CheckboxInput/CheckboxInput.tsx index 05c3eb3dc2..02d1b56ac4 100644 --- a/packages/common-components/src/CheckboxInput/CheckboxInput.tsx +++ b/packages/common-components/src/CheckboxInput/CheckboxInput.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent } from "react" +import React, { FormEvent, ReactNode } from "react" import { ITheme, makeStyles, createStyles } from "@chainsafe/common-theme" import clsx from "clsx" import { Typography } from "../Typography" @@ -100,9 +100,9 @@ const useStyles = makeStyles( ) interface ICheckboxProps - extends Omit, "value"> { + extends Omit, "value" | "label"> { className?: string - label?: string + label?: string | ReactNode error?: string value: boolean indeterminate?: boolean diff --git a/packages/common-components/src/CheckboxInput/FormikCheckboxInput.tsx b/packages/common-components/src/CheckboxInput/FormikCheckboxInput.tsx index a096fbd639..bcd38c5a04 100644 --- a/packages/common-components/src/CheckboxInput/FormikCheckboxInput.tsx +++ b/packages/common-components/src/CheckboxInput/FormikCheckboxInput.tsx @@ -1,11 +1,12 @@ -import React from "react" +import React, { ReactNode } from "react" import { useField } from "formik" import CheckboxInput from "./CheckboxInput" -interface IFormikCheckboxProps extends React.HTMLProps { +interface IFormikCheckboxProps + extends Omit, "label"> { className?: string name: string - label?: string + label?: string | ReactNode } const FormikCheckboxInput: React.FC = ({ @@ -22,7 +23,7 @@ const FormikCheckboxInput: React.FC = ({ return ( diff --git a/packages/common-components/src/FileInput/FileInput.tsx b/packages/common-components/src/FileInput/FileInput.tsx index 712919564b..782e351f92 100644 --- a/packages/common-components/src/FileInput/FileInput.tsx +++ b/packages/common-components/src/FileInput/FileInput.tsx @@ -70,6 +70,7 @@ interface IFileInputProps extends DropzoneOptions { label?: string showPreviews?: boolean pending?: ReactNode | ReactNode[] + maxFileSize?: number classNames?: { pending?: string filelist?: string @@ -84,6 +85,7 @@ const FileInput: React.FC = ({ name, label, pending, + maxFileSize, classNames, ...props }: IFileInputProps) => { @@ -94,10 +96,13 @@ const FileInput: React.FC = ({ const onDrop = useCallback( async (acceptedFiles: File[], fileRejections: FileRejection[]) => { + const filtered = acceptedFiles.filter((file) => + maxFileSize ? file.size <= maxFileSize : true, + ) setErrors([]) if (showPreviews) { setPreviews( - acceptedFiles.map((file: any) => + filtered.map((file: any) => Object.assign(file, { preview: URL.createObjectURL(file), }), @@ -105,7 +110,7 @@ const FileInput: React.FC = ({ ) } - helpers.setValue(acceptedFiles) + helpers.setValue(filtered) if (fileRejections.length > 0) { const fileDropRejectionErrors = fileRejections.map((fr) => diff --git a/packages/common-components/src/TextInput/FormikTextInput.tsx b/packages/common-components/src/TextInput/FormikTextInput.tsx index e8ce8f8cf0..870f30be75 100644 --- a/packages/common-components/src/TextInput/FormikTextInput.tsx +++ b/packages/common-components/src/TextInput/FormikTextInput.tsx @@ -43,7 +43,9 @@ const FormikTextInput: React.FC = ({ value={field.value} placeholder={placeholder} captionMessage={ - meta.error ? `${meta.error}` : captionMessage && captionMessage + meta.touched && meta.error + ? `${meta.error}` + : captionMessage && captionMessage } state={meta.error ? "error" : undefined} onChange={helpers.setValue} diff --git a/packages/common-components/src/Toaster/ToastContainer.tsx b/packages/common-components/src/Toaster/ToastContainer.tsx new file mode 100644 index 0000000000..2721ef3a49 --- /dev/null +++ b/packages/common-components/src/Toaster/ToastContainer.tsx @@ -0,0 +1,53 @@ +import React, { ReactNode } from "react" + +import { Placement } from "react-toast-notifications" +import { ITheme } from "@chainsafe/common-theme" +import { makeStyles, createStyles } from "@material-ui/styles" + +interface IStyleProps { + placement: Placement + hasToasts: boolean +} + +const useStyles = makeStyles(({ zIndex, constants }: ITheme) => + createStyles({ + container: (props: IStyleProps) => ({ + boxSizing: "border-box", + maxHeight: "100%", + overflow: "hidden", + padding: constants.generalUnit, + position: "fixed", + zIndex: zIndex?.blocker, + ...placements[props.placement], + }), + }), +) + +const placements = { + "top-left": { top: 0, left: 0 }, + "top-center": { top: 0, left: "50%", transform: "translateX(-50%)" }, + "top-right": { top: 0, right: 0 }, + "bottom-left": { bottom: 0, left: 0 }, + "bottom-center": { bottom: 0, left: "50%", transform: "translateX(-50%)" }, + "bottom-right": { bottom: 0, right: 0 }, +} + +export type ToastContainerProps = { + children?: ReactNode + hasToasts: boolean + placement: Placement +} + +const ToastContainer: React.FC = ({ + hasToasts, + placement, + ...props +}) => { + const classes = useStyles({ + hasToasts, + placement, + }) + return
+} + +export default ToastContainer diff --git a/packages/common-components/src/Toaster/ToasterProvider.tsx b/packages/common-components/src/Toaster/ToasterProvider.tsx index 603694d078..33e5795de4 100644 --- a/packages/common-components/src/Toaster/ToasterProvider.tsx +++ b/packages/common-components/src/Toaster/ToasterProvider.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from "react" import Toaster from "./Toaster" +import ToastContainer from "./ToastContainer" import { Placement, ToastProvider } from "react-toast-notifications" export interface IToasterProviderProps { @@ -19,7 +20,7 @@ export const ToasterProvider: React.FC = ({ {children} diff --git a/packages/common-contexts/package.json b/packages/common-contexts/package.json index a49a30899e..0d5b8c480d 100644 --- a/packages/common-contexts/package.json +++ b/packages/common-contexts/package.json @@ -14,8 +14,10 @@ "start": "rollup -c -w" }, "dependencies": { - "@imploy/api-client": "1.2.9", + "@imploy/api-client": "1.3.2", "axios": "^0.21.0", + "pbkdf2": "^3.1.1", + "tweetnacl": "^1.0.3", "uuid": "^8.3.1" }, "peerDependencies": { @@ -48,8 +50,8 @@ "rollup": "2.34.2", "rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-peer-deps-external": "^2.2.4", - "styled-components": "^5.2.1", "rollup-plugin-typescript2": "^0.29.0", + "styled-components": "^5.2.1", "typescript": "^4.0.5" }, "files": [ diff --git a/packages/common-contexts/src/ImployApiContext/ImployApiContext.tsx b/packages/common-contexts/src/ImployApiContext/ImployApiContext.tsx index f3c382adec..4ecfbd40f8 100644 --- a/packages/common-contexts/src/ImployApiContext/ImployApiContext.tsx +++ b/packages/common-contexts/src/ImployApiContext/ImployApiContext.tsx @@ -10,6 +10,7 @@ import { import jwtDecode from "jwt-decode" import { signMessage } from "./utils" import axios from "axios" +import { decryptFile, encryptFile } from "../helpers" export { Provider as OAuthProvider } @@ -34,9 +35,11 @@ type ImployApiContextProps = { type ImployApiContext = { imployApiClient: IImployApiClient isLoggedIn: boolean | undefined + secured: boolean | undefined isReturningUser: boolean selectWallet(): Promise resetAndSelectWallet(): Promise + secureAccount(masterPassword: string): Promise web3Login(): Promise getProviderUrl(provider: Provider): Promise loginWithGithub(code: string, state: string): Promise @@ -50,6 +53,7 @@ type ImployApiContext = { ): Promise loginWithFacebook(code: string, state: string): Promise logout(): void + validateMasterPassword(candidatePassword: string): Promise } const ImployApiContext = React.createContext( @@ -61,7 +65,7 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { const canUseLocalStorage = testLocalStorage() // initializing api const initialAxiosInstance = axios.create({ - // Disable the internal Axios JSON deserialization as this is handled by the client + // Disable the internal Axios JSON de serialization as this is handled by the client transformResponse: [], }) const initialApiClient = new ImployApiClient({}, apiUrl, initialAxiosInstance) @@ -73,10 +77,11 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { // access tokens const [accessToken, setAccessToken] = useState(undefined) + const [secured, setSecured] = useState(undefined) + const [refreshToken, setRefreshToken] = useState(undefined) const [decodedRefreshToken, setDecodedRefreshToken] = useState< - { exp: number } | undefined + { exp: number; mps?: string; uuid: string } | undefined >(undefined) - const [refreshToken, setRefreshToken] = useState(undefined) // returning user const isReturningUserLocal = @@ -105,7 +110,7 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { useEffect(() => { const initializeApiClient = async () => { const axiosInstance = axios.create({ - // Disable the internal Axios JSON deserialization as this is handled by the client + // Disable the internal Axios JSON de serialization as this is handled by the client transformResponse: [], }) @@ -128,9 +133,9 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { const { access_token, refresh_token, - } = await refreshTokenApiClient.getRefreshToken( - refreshTokenLocal, - ) + } = await refreshTokenApiClient.getRefreshToken({ + refresh: refreshTokenLocal, + }) setTokensAndSave(access_token, refresh_token) error.response.config.headers.Authorization = `Bearer ${access_token.token}` @@ -158,7 +163,7 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { const { access_token, refresh_token, - } = await apiClient.getRefreshToken(savedRefreshToken) + } = await apiClient.getRefreshToken({ refresh: savedRefreshToken }) setTokensAndSave(access_token, refresh_token) } catch (error) {} @@ -220,7 +225,9 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { useEffect(() => { if (refreshToken && refreshToken.token) { try { - const decoded = jwtDecode(refreshToken.token) + const decoded = jwtDecode<{ mps?: string; exp: number; uuid: string }>( + refreshToken.token, + ) setDecodedRefreshToken(decoded) } catch (error) { console.log("Error decoding access token") @@ -231,6 +238,14 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { useEffect(() => { if (accessToken && accessToken.token && imployApiClient) { imployApiClient?.setToken(accessToken.token) + const decodedAccessToken = jwtDecode<{ perm: { secured?: string } }>( + accessToken.token, + ) + if (decodedAccessToken.perm.secured === "true") { + setSecured(true) + } else { + setSecured(false) + } } }, [accessToken]) @@ -324,12 +339,61 @@ const ImployApiProvider = ({ apiUrl, children }: ImployApiContextProps) => { canUseLocalStorage && localStorage.removeItem(tokenStorageKey) } + const secureAccount = async (masterPassword: string) => { + try { + if (decodedRefreshToken && refreshToken) { + const uuidArray = new TextEncoder().encode(decodedRefreshToken.uuid) + const encryptedUuid = await encryptFile(uuidArray, masterPassword) + const encryptedUuidString = Buffer.from(encryptedUuid).toString( + "base64", + ) + await imployApiClient.secure({ + mps: encryptedUuidString, + }) + + const { + access_token, + refresh_token, + } = await imployApiClient.getRefreshToken({ + refresh: refreshToken.token, + }) + + setTokensAndSave(access_token, refresh_token) + return true + } else { + return false + } + } catch (error) { + return false + } + } + + const validateMasterPassword = async ( + candidatePassword: string, + ): Promise => { + if (!decodedRefreshToken || !decodedRefreshToken.mps) return false + try { + const toDecryptArray = Buffer.from(decodedRefreshToken.mps, "base64") + const decrypted = await decryptFile(toDecryptArray, candidatePassword) + if (decrypted) { + const decryptedUuid = new TextDecoder().decode(decrypted) + return decodedRefreshToken.uuid === decryptedUuid + } else { + return false + } + } catch (error) { + return false + } + } + return ( { resetAndSelectWallet, getProviderUrl, logout, + validateMasterPassword, }} > {children} diff --git a/packages/common-contexts/src/helpers/encryption.ts b/packages/common-contexts/src/helpers/encryption.ts new file mode 100644 index 0000000000..98322f0d9a --- /dev/null +++ b/packages/common-contexts/src/helpers/encryption.ts @@ -0,0 +1,87 @@ +const getPasswordKey = async (passwordBytes: Uint8Array) => + window.crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, [ + "deriveKey", + ]) + +const deriveKey = async ( + passwordKey: CryptoKey, + salt: Uint8Array, + keyUsage: KeyUsage[], +) => + window.crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: salt, + iterations: 250000, + hash: "SHA-256", + }, + passwordKey, + { name: "AES-GCM", length: 256 }, + false, + keyUsage, + ) + +export const encryptFile = async ( + fileArrayBuffer: ArrayBuffer, + password: string, +) => { + try { + const plainTextBytes = new Uint8Array(fileArrayBuffer) + const passwordBytes = new TextEncoder().encode(password) + + const salt = window.crypto.getRandomValues(new Uint8Array(16)) + const iv = window.crypto.getRandomValues(new Uint8Array(12)) + + const passwordKey = await getPasswordKey(passwordBytes) + + const aesKey = await deriveKey(passwordKey, salt, ["encrypt"]) + const cipherBytes = await window.crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv }, + aesKey, + plainTextBytes, + ) + + const cipherBytesArray = new Uint8Array(cipherBytes) + const resultBytes = new Uint8Array( + cipherBytesArray.byteLength + salt.byteLength + iv.byteLength, + ) + resultBytes.set(salt, 0) + resultBytes.set(iv, salt.byteLength) + resultBytes.set(cipherBytesArray, salt.byteLength + iv.byteLength) + + return resultBytes + } catch (error) { + console.error("Error encrypting file") + console.error(error) + throw error + } +} + +export const decryptFile = async ( + cipher: ArrayBuffer | Uint8Array, + password: string, +) => { + try { + const cipherBytes = new Uint8Array(cipher) + const passwordBytes = new TextEncoder().encode(password) + + const salt = cipherBytes.slice(0, 16) + const iv = cipherBytes.slice(16, 16 + 12) + const data = cipherBytes.slice(16 + 12) + const passwordKey = await getPasswordKey(passwordBytes) + const aesKey = await deriveKey(passwordKey, salt, ["decrypt"]) + + const decryptedContent = await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + }, + aesKey, + data, + ) + + return decryptedContent + } catch (error) { + return + } +} diff --git a/packages/common-contexts/src/helpers/index.ts b/packages/common-contexts/src/helpers/index.ts new file mode 100644 index 0000000000..7bcbcdabcc --- /dev/null +++ b/packages/common-contexts/src/helpers/index.ts @@ -0,0 +1 @@ +export { encryptFile, decryptFile } from "./encryption" diff --git a/packages/common-contexts/src/index.ts b/packages/common-contexts/src/index.ts index 97c616a571..e79060c992 100644 --- a/packages/common-contexts/src/index.ts +++ b/packages/common-contexts/src/index.ts @@ -1,3 +1,4 @@ export * from "./ImployApiContext" export * from "./UserContext" export * from "./BillingContext" +export * from "./helpers" diff --git a/packages/common-themes/src/Defaults/GlobalStyling.ts b/packages/common-themes/src/Defaults/GlobalStyling.ts index bc115cbc5c..d7e6d37028 100644 --- a/packages/common-themes/src/Defaults/GlobalStyling.ts +++ b/packages/common-themes/src/Defaults/GlobalStyling.ts @@ -1,4 +1,5 @@ -import { DefaultThemeConfig } from "./ThemeConfig" +import { DefaultThemeConfig, defaultFontFamilyStack } from "./ThemeConfig" + export const DefaultGlobalStyling = { html: { ...DefaultThemeConfig.typography.global, @@ -7,17 +8,17 @@ export const DefaultGlobalStyling = { // Change from `box-sizing: content-box` so that `width` // is not affected by `padding` or `border`. boxSizing: "border-box", - }, - '*, *:before, *:after': { - boxSizing: 'inherit', + "*, *:before, *:after": { + boxSizing: "inherit", }, - 'strong, b': { + "strong, b": { fontWeight: DefaultThemeConfig.typography.fontWeight.bold, }, body: { color: DefaultThemeConfig.palette.text.primary, ...DefaultThemeConfig.typography.body2, + ...defaultFontFamilyStack, backgroundColor: DefaultThemeConfig.palette.background.default, "& @media print": { // Save printer ink. diff --git a/packages/common-themes/src/Defaults/ThemeConfig.ts b/packages/common-themes/src/Defaults/ThemeConfig.ts index cf6721f5eb..37a33239ac 100644 --- a/packages/common-themes/src/Defaults/ThemeConfig.ts +++ b/packages/common-themes/src/Defaults/ThemeConfig.ts @@ -3,10 +3,12 @@ import { DefaultPalette } from "./ColorPalette" import { fade } from "../utils/colorManipulator" import { createBreakpoints } from "../Create/CreateBreakpoints" -const defaultFontFamilyStack = { - fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen','Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',sans-serif`, +export const defaultFontFamilyStack = { + fontFamily: `'Archivo', sans-serif`, } +const defaultFontStyles = {} + const defaultFontWeights: IFontWeights = { light: 300, regular: 400, @@ -94,59 +96,59 @@ const DefaultThemeConfig: IThemeConfig = { }, typography: { global: { - ...defaultFontFamilyStack, + ...defaultFontStyles, }, fontWeight: defaultFontWeights, h1: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.semibold, fontSize: 38, lineHeight: `46px`, }, h2: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.semibold, fontSize: 30, lineHeight: `38px`, }, h3: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 24, lineHeight: `32px`, }, h4: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 20, lineHeight: `28px`, }, h5: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 16, lineHeight: `24px`, }, h6: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 30, lineHeight: `46px`, }, subtitle1: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 24, lineHeight: `32px`, }, subtitle2: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 24, lineHeight: `32px`, }, body1: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 14, lineHeight: `22px`, @@ -156,7 +158,7 @@ const DefaultThemeConfig: IThemeConfig = { }, }, body2: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 12, lineHeight: `20px`, @@ -166,13 +168,13 @@ const DefaultThemeConfig: IThemeConfig = { }, }, button: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 14, lineHeight: `22px`, }, caption: { - ...defaultFontFamilyStack, + ...defaultFontStyles, fontWeight: defaultFontWeights.regular, fontSize: 12, lineHeight: `20px`, diff --git a/packages/files-landing-page/public/index.html b/packages/files-landing-page/public/index.html index 1d2c4023c8..fc66bc6a15 100644 --- a/packages/files-landing-page/public/index.html +++ b/packages/files-landing-page/public/index.html @@ -16,6 +16,13 @@ --> + + + = () => { diff --git a/packages/files-landing-page/src/Components/Modules/Footer.tsx b/packages/files-landing-page/src/Components/Modules/Footer.tsx index 3ed39237aa..bf9a212a1e 100644 --- a/packages/files-landing-page/src/Components/Modules/Footer.tsx +++ b/packages/files-landing-page/src/Components/Modules/Footer.tsx @@ -2,6 +2,7 @@ import React from "react" import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" import { Grid, Typography, Link } from "@chainsafe/common-components" import { Trans } from "@lingui/macro" +import { ROUTE_LINKS } from "../Routes" const useStyles = makeStyles(({ palette, constants, breakpoints }: ITheme) => { return createStyles({ @@ -130,12 +131,12 @@ const Footer: React.FC = () => { - + Terms of Service - + Privacy Policy diff --git a/packages/files-landing-page/src/Components/Pages/PrivacyPolicyPage.tsx b/packages/files-landing-page/src/Components/Pages/PrivacyPolicyPage.tsx index 8d72b8b583..04fd7e9050 100644 --- a/packages/files-landing-page/src/Components/Pages/PrivacyPolicyPage.tsx +++ b/packages/files-landing-page/src/Components/Pages/PrivacyPolicyPage.tsx @@ -96,7 +96,7 @@ const TermsOfServicePage: React.FC = () => { component="p" className={clsx(classes.caption, classes.padSmall)} > - Last Modified: November 13, 2020 + Last Modified: December 8, 2020 {/* welcome */} @@ -233,10 +233,10 @@ const TermsOfServicePage: React.FC = () => { > Our policy is to collect as little user information as possible. We - have no access to your uploaded content because it is all encrypted - end-to-end. We may collect user information that is necessary for us - to provide you the service. The type of information we collect about - you will depend. We may collect: + have limited access to your uploaded content because it is all + encrypted end-to-end. We may collect user information that is + necessary for us to provide you the service. The type of information + we collect about you will depend. We may collect: { of visits to certain pages, page interaction information (such as scrolling, clicks, and mouse-overs), methods used to browse away from the page, or any phone number used to call our customer service - number. + number. We collect cookies on this promotional website for + anonymized system monitoring and non-personal analytics. diff --git a/packages/files-ui/package.json b/packages/files-ui/package.json index 27115151ed..33251f6dba 100644 --- a/packages/files-ui/package.json +++ b/packages/files-ui/package.json @@ -13,6 +13,7 @@ "@lingui/react": "^3.2.3", "@sentry/react": "^5.28.0", "@types/yup": "^0.29.9", + "@types/zxcvbn": "^4.4.0", "babel-loader": "8.1.0", "babel-plugin-macros": "^2.8.0", "babel-preset-env": "^1.7.0", @@ -36,7 +37,8 @@ "react-use-hotjar": "1.0.8", "react-zoom-pan-pinch": "^1.6.1", "typescript": "~4.0.5", - "yup": "^0.31.1" + "yup": "^0.32.8", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.6", diff --git a/packages/files-ui/src/Components/FilesRoutes.tsx b/packages/files-ui/src/Components/FilesRoutes.tsx index bff20870e7..6646d8d407 100644 --- a/packages/files-ui/src/Components/FilesRoutes.tsx +++ b/packages/files-ui/src/Components/FilesRoutes.tsx @@ -6,6 +6,7 @@ import { useImployApi } from "@imploy/common-contexts" import HomePage from "./Pages/HomePage" import OAuthCallbackPage from "./Pages/OAuthCallback" import PurchasePlanPage from "./Pages/PurchasePlanPage" +import { useDrive } from "../Contexts/DriveContext" export const ROUTE_LINKS = { Landing: "/", @@ -18,27 +19,28 @@ export const ROUTE_LINKS = { } const FilesRoutes = () => { - const { isLoggedIn } = useImployApi() + const { isLoggedIn, secured } = useImployApi() + const { isMasterPasswordSet } = useDrive() return ( @@ -52,7 +54,7 @@ const FilesRoutes = () => { diff --git a/packages/files-ui/src/Components/Layouts/AppHeader.tsx b/packages/files-ui/src/Components/Layouts/AppHeader.tsx index ed3b08cb5c..61316b504b 100644 --- a/packages/files-ui/src/Components/Layouts/AppHeader.tsx +++ b/packages/files-ui/src/Components/Layouts/AppHeader.tsx @@ -19,6 +19,7 @@ import { import { ROUTE_LINKS } from "../FilesRoutes" // import SearchModule from "../Modules/SearchModule" import { Trans } from "@lingui/macro" +import { useDrive } from "../../Contexts/DriveContext" const useStyles = makeStyles( ({ palette, animation, breakpoints, constants, zIndex }: ITheme) => { @@ -142,7 +143,8 @@ const AppHeader: React.FC = ({ const { breakpoints }: ITheme = useTheme() const desktop = useMediaQuery(breakpoints.up("md")) - const { isLoggedIn, logout } = useImployApi() + const { isLoggedIn, logout, secured } = useImployApi() + const { isMasterPasswordSet } = useDrive() const { getProfileTitle, removeUser } = useUser() const signOut = useCallback(() => { @@ -153,10 +155,10 @@ const AppHeader: React.FC = ({ return (
- {isLoggedIn && ( + {isLoggedIn && secured && !!isMasterPasswordSet && ( {desktop ? ( @@ -188,6 +190,8 @@ const AppHeader: React.FC = ({ /> +   + beta {/* */} diff --git a/packages/files-ui/src/Components/Layouts/AppNav.tsx b/packages/files-ui/src/Components/Layouts/AppNav.tsx index 1ae99d28dc..9f41d4c529 100644 --- a/packages/files-ui/src/Components/Layouts/AppNav.tsx +++ b/packages/files-ui/src/Components/Layouts/AppNav.tsx @@ -184,6 +184,9 @@ const useStyles = makeStyles( spaceUsedMargin: { marginBottom: constants.generalUnit, }, + betaCaption: { + marginBottom: constants.generalUnit * 0.5, + }, }) }, ) @@ -199,7 +202,8 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { const desktop = useMediaQuery(breakpoints.up("md")) const { spaceUsed } = useDrive() - const { isLoggedIn, logout } = useImployApi() + const { isLoggedIn, logout, secured } = useImployApi() + const { isMasterPasswordSet } = useDrive() const { removeUser } = useUser() const signOut = useCallback(() => { @@ -220,10 +224,12 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { return (
- {isLoggedIn && ( + {isLoggedIn && secured && !!isMasterPasswordSet && ( {desktop && (
@@ -232,6 +238,10 @@ const AppNav: React.FC = ({ navOpen, setNavOpen }: IAppNav) => { Files +   + + beta +
)} diff --git a/packages/files-ui/src/Components/Layouts/AppWrapper.tsx b/packages/files-ui/src/Components/Layouts/AppWrapper.tsx index 8e2d7ce796..c75ecacca2 100644 --- a/packages/files-ui/src/Components/Layouts/AppWrapper.tsx +++ b/packages/files-ui/src/Components/Layouts/AppWrapper.tsx @@ -12,6 +12,7 @@ import clsx from "clsx" import { CssBaseline } from "@chainsafe/common-components" import AppHeader from "./AppHeader" import AppNav from "./AppNav" +import { useDrive } from "../../Contexts/DriveContext" interface IAppWrapper { children: ReactNode | ReactNode[] @@ -68,21 +69,21 @@ const AppWrapper: React.FC = ({ children }: IAppWrapper) => { const [navOpen, setNavOpen] = useState(desktop) - const { isLoggedIn } = useImployApi() - + const { isLoggedIn, secured } = useImployApi() + const { isMasterPasswordSet } = useDrive() return (
{children} diff --git a/packages/files-ui/src/Components/Modules/CreateFolderModule.tsx b/packages/files-ui/src/Components/Modules/CreateFolderModule.tsx index 5082fdf21c..9a90a257fa 100644 --- a/packages/files-ui/src/Components/Modules/CreateFolderModule.tsx +++ b/packages/files-ui/src/Components/Modules/CreateFolderModule.tsx @@ -113,7 +113,7 @@ const CreateFolderModule: React.FC = ({ }} >
- +
{!desktop && ( = ({ {desktop ? OK : Create} - +
diff --git a/packages/files-ui/src/Components/Modules/FileBrowserModule/FileBrowser.tsx b/packages/files-ui/src/Components/Modules/FileBrowserModule/FileBrowser.tsx index c57a296462..ee75199b4f 100644 --- a/packages/files-ui/src/Components/Modules/FileBrowserModule/FileBrowser.tsx +++ b/packages/files-ui/src/Components/Modules/FileBrowserModule/FileBrowser.tsx @@ -363,7 +363,8 @@ const FileBrowserModule: React.FC = ({ .test( "Invalid name", "File name cannot contain '/' character", - (val) => !invalidFilenameRegex.test(val || ""), + (val: string | null | undefined) => + !invalidFilenameRegex.test(val || ""), ) .required("File name is required"), }) diff --git a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx index 1d91fe8a17..973896a940 100644 --- a/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx +++ b/packages/files-ui/src/Components/Modules/FilePreviewModal.tsx @@ -186,7 +186,7 @@ const FilePreviewModal: React.FC<{ setIsLoading(true) setError(undefined) try { - const content = await getFileContent(file.name, token, (evt) => { + const content = await getFileContent(file.cid, token, (evt) => { setLoadingProgress((evt.loaded / file.size) * 100) }) setFileContent(content) diff --git a/packages/files-ui/src/Components/Modules/MasterKeySequence/MasterKeyModule.tsx b/packages/files-ui/src/Components/Modules/MasterKeySequence/MasterKeyModule.tsx new file mode 100644 index 0000000000..322afc1b79 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/MasterKeySequence/MasterKeyModule.tsx @@ -0,0 +1,38 @@ +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import { useState } from "react" +import clsx from "clsx" +import ExplainSlide from "./SequenceSlides/Explain.slide" +import SetMasterKeySlide from "./SequenceSlides/SetMasterKey.slide" + +const useStyles = makeStyles(({ breakpoints }: ITheme) => + createStyles({ + root: { + [breakpoints.down("md")]: {}, + }, + slide: {}, + }), +) + +interface IMasterKeyModule { + className?: string +} + +const MasterKeyModule: React.FC = ({ + className, +}: IMasterKeyModule) => { + const classes = useStyles() + const [slide, setSlide] = useState<"explain" | "set">("explain") + // TODO: WIRE POST SUBMIT + return ( +
+ {slide === "explain" ? ( + setSlide("set")} /> + ) : ( + + )} +
+ ) +} + +export default MasterKeyModule diff --git a/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/EnterMasterKey.slide.tsx b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/EnterMasterKey.slide.tsx new file mode 100644 index 0000000000..7f2da1678d --- /dev/null +++ b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/EnterMasterKey.slide.tsx @@ -0,0 +1,139 @@ +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import { + Button, + FormikTextInput, + Typography, +} from "@chainsafe/common-components" +import clsx from "clsx" +import { Form, Formik } from "formik" +import * as yup from "yup" +import { useDrive } from "../../../../Contexts/DriveContext" +import { useImployApi, useUser } from "@imploy/common-contexts" + +const useStyles = makeStyles( + ({ constants, breakpoints, palette, typography }: ITheme) => + createStyles({ + root: { + maxWidth: 320, + "& h2": { + textAlign: "center", + marginBottom: constants.generalUnit * 4.125, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }, + input: { + width: "100%", + margin: 0, + marginBottom: constants.generalUnit * 1.5, + "& span": { + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }, + inputLabel: { + fontSize: "16px", + lineHeight: "24px", + color: palette.additional["gray"][8], + marginBottom: constants.generalUnit, + }, + button: { + marginTop: constants.generalUnit * 3, + }, + userContainer: { + marginTop: constants.generalUnit * 4, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + logoutButton: { + padding: 0, + textDecoration: "underline", + border: "none", + cursor: "pointer", + backgroundColor: "transparent", + ...typography.body1, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }), +) + +interface IEnterMasterKeySlide { + className?: string +} + +const EnterMasterKeySlide: React.FC = ({ + className, +}: IEnterMasterKeySlide) => { + const classes = useStyles() + const { validateMasterPassword, logout } = useImployApi() + const { getProfileTitle } = useUser() + const masterKeyValidation = yup.object().shape({ + masterKey: yup + .string() + .test( + "Key valid", + "Encryption password is invalid", + async (value: string | null | undefined | object) => { + try { + return await validateMasterPassword(`${value}`) + } catch (error) { + return false + } + }, + ) + .required("Please provide an encryption password"), + }) + const { setMasterPassword } = useDrive() + + return ( +
+ { + helpers.setSubmitting(true) + setMasterPassword(values.masterKey) + helpers.setSubmitting(false) + }} + > +
+ + Encryption Password + + + + +
+
+ Signed in as: +
+ + {getProfileTitle()} + +
+ +
+
+ ) +} + +export default EnterMasterKeySlide diff --git a/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/Explain.slide.tsx b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/Explain.slide.tsx new file mode 100644 index 0000000000..22165a84bc --- /dev/null +++ b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/Explain.slide.tsx @@ -0,0 +1,72 @@ +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import { Button, Typography } from "@chainsafe/common-components" +import clsx from "clsx" + +const useStyles = makeStyles(({ breakpoints, constants, palette }: ITheme) => + createStyles({ + root: { + maxWidth: 320, + [breakpoints.down("md")]: {}, + "& p": { + fontWeight: 400, + marginBottom: constants.generalUnit * 2, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + "& h2": { + marginBottom: constants.generalUnit * 4.125, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }, + cta: { + marginTop: constants.generalUnit * 4.125, + }, + }), +) + +interface IExplainSlide { + className?: string + cta: () => void +} + +const ExplainSlide: React.FC = ({ + className, + cta, +}: IExplainSlide) => { + const classes = useStyles() + + return ( +
+ + A few things you should know.... + + + Using ChainSafe Files requires that you set an encryption password. This + is what disables your content from being read by us or any other + third-party. + + + Here’s the thing about your encryption password.{" "} + + Forgetting this password means that you will be permanently locked out + of your account. + {" "} + We aren’t storing any keys, and as a result, we will not be able to + recover your account. + + + Please do not share your encryption password with anyone. Record it + somewhere safe. + + +
+ ) +} + +export default ExplainSlide diff --git a/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/SetMasterKey.slide.tsx b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/SetMasterKey.slide.tsx new file mode 100644 index 0000000000..130445c534 --- /dev/null +++ b/packages/files-ui/src/Components/Modules/MasterKeySequence/SequenceSlides/SetMasterKey.slide.tsx @@ -0,0 +1,198 @@ +import { createStyles, ITheme, makeStyles } from "@chainsafe/common-theme" +import React from "react" +import { + Button, + FormikCheckboxInput, + FormikTextInput, + Typography, +} from "@chainsafe/common-components" +import clsx from "clsx" +import { Form, Formik } from "formik" +import * as yup from "yup" +import { ROUTE_LINKS } from "../../../FilesRoutes" +import { useDrive } from "../../../../Contexts/DriveContext" +import zxcvbn from "zxcvbn" + +const useStyles = makeStyles(({ breakpoints, constants, palette }: ITheme) => + createStyles({ + root: { + maxWidth: 320, + [breakpoints.down("md")]: {}, + "& p": { + fontWeight: 400, + marginBottom: constants.generalUnit * 2, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + "& h2": { + textAlign: "center", + marginBottom: constants.generalUnit * 4.125, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }, + input: { + margin: 0, + width: "100%", + marginBottom: constants.generalUnit * 1.5, + "& span": { + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, + }, + highlight: { + fontWeight: 700, + textDecoration: "underline", + }, + checkbox: { + marginBottom: constants.generalUnit, + [breakpoints.up("md")]: { + color: palette.additional["gray"][8], + }, + [breakpoints.down("md")]: { + color: palette.common.white.main, + "& a": { + color: `${palette.common.white.main} !important`, + }, + }, + }, + button: { + marginTop: constants.generalUnit * 3, + }, + inputLabel: { + fontSize: "16px", + lineHeight: "24px", + color: palette.additional["gray"][8], + marginBottom: constants.generalUnit, + }, + }), +) + +interface ISetMasterKeySlide { + className?: string +} + +const SetMasterKeySlide: React.FC = ({ + className, +}: ISetMasterKeySlide) => { + const classes = useStyles() + const { secureDrive } = useDrive() + + const masterKeyValidation = yup.object().shape({ + masterKey: yup + .string() + .test( + "Complexity", + "Encryption password needs to be more complex", + async (val: string | null | undefined | object) => { + if (val === undefined) { + return false + } + + const complexity = zxcvbn(`${val}`) + if (complexity.score >= 3) { + return true + } + return false + }, + ) + .required("Please provide an encryption password"), + confirmMasterKey: yup + .string() + .oneOf( + [yup.ref("masterKey"), undefined], + "Encryption password must match", + ) + .required("Encryption password confirmation is required'"), + privacyPolicy: yup + .boolean() + .oneOf([true], "Please accept the privacy policy"), + terms: yup.boolean().oneOf([true], "Please accept the terms & conditions."), + }) + + return ( +
+ + Set an Encryption Password + + { + helpers.setSubmitting(true) + secureDrive(values.masterKey) + helpers.setSubmitting(false) + }} + > +
+ + + + Please record your encryption password somewhere safe.
+ Forgetting this password means{" "} + + you are permanently locked out of your account. + +
+ + I have read the{" "} + + Privacy Policy + + + } + /> + + I have read the{" "} + + Terms of Service + + + } + /> + + +
+
+ ) +} + +export default SetMasterKeySlide diff --git a/packages/files-ui/src/Components/Modules/Settings/PurchasePlan/index.tsx b/packages/files-ui/src/Components/Modules/Settings/PurchasePlan/index.tsx index be300ad338..627c69cfad 100644 --- a/packages/files-ui/src/Components/Modules/Settings/PurchasePlan/index.tsx +++ b/packages/files-ui/src/Components/Modules/Settings/PurchasePlan/index.tsx @@ -170,7 +170,7 @@ const PurchasePlan: React.FC = () => { name: yup.string().required("Name is required"), email: yup.string().email("Email is invalid").required("Email is required"), country: yup.string().when(["zipCode"], { - is: (zipCode) => !zipCode, + is: (zipCode: string | undefined | null) => !zipCode, then: yup.string().required("Country or zip code is required"), }), zipCode: yup.string(), diff --git a/packages/files-ui/src/Components/Modules/UploadFileModule.tsx b/packages/files-ui/src/Components/Modules/UploadFileModule.tsx index 320a5907dc..f86d6cbaab 100644 --- a/packages/files-ui/src/Components/Modules/UploadFileModule.tsx +++ b/packages/files-ui/src/Components/Modules/UploadFileModule.tsx @@ -93,6 +93,7 @@ const UploadFileModule: React.FC = ({ multiple={true} className={classes.input} label="Upload Files and Folders" + maxSize={2 * 1024 ** 3} name="files" />
@@ -101,7 +102,7 @@ const UploadFileModule: React.FC = ({ size="medium" className={classes.cancelButton} variant="outline" - type="button" + type="reset" > Cancel diff --git a/packages/files-ui/src/Components/Pages/LoginPage.tsx b/packages/files-ui/src/Components/Pages/LoginPage.tsx index 52807a96d2..52bb0abe48 100644 --- a/packages/files-ui/src/Components/Pages/LoginPage.tsx +++ b/packages/files-ui/src/Components/Pages/LoginPage.tsx @@ -23,6 +23,9 @@ import LargeLightBulbSvg from "../../Media/LargeLightBulb.svg" import SmallBranchSvg from "../../Media/SmallBranch.svg" import { Trans } from "@lingui/macro" import { ROUTE_LINKS } from "../FilesRoutes" +import LandingImage from "../../Media/auth.jpg" +import MasterKeyModule from "../Modules/MasterKeySequence/MasterKeyModule" +import EnterMasterKeySlide from "../Modules/MasterKeySequence/SequenceSlides/EnterMasterKey.slide" const useStyles = makeStyles( ({ palette, constants, typography, breakpoints }: ITheme) => @@ -44,10 +47,10 @@ const useStyles = makeStyles( display: "flex", flexFlow: "column", "& > img": { - height: `calc(100% - 180px)`, - maxHeight: "1000px", - marginBottom: 50, - marginTop: 50, + width: `calc(100% - 100px)`, + maxWidth: "1200px", + maxHeight: `calc(100% - 100px)`, + margin: 50, }, }, logoContainer: { @@ -151,6 +154,12 @@ const useStyles = makeStyles( height: "auto", zIndex: 0, }, + betaCaption: { + marginBottom: constants.generalUnit * 0.5, + [breakpoints.down("md")]: { + color: palette.common.white.main, + }, + }, }), ) @@ -163,6 +172,8 @@ const LoginPage = () => { selectWallet, resetAndSelectWallet, getProviderUrl, + secured, + isLoggedIn, } = useImployApi() const { provider, wallet } = useWeb3() const [error, setError] = useState("") @@ -220,14 +231,7 @@ const LoginPage = () => { {desktop ? ( - - - Making secure cloud storage easier than ever. - + ) : ( <> @@ -249,132 +253,146 @@ const LoginPage = () => { ChainSafe Files +   + + beta +
- - {activeMode === "newUser" ? "Create an account" : "Welcome back!"} - - {error && ( - {error} - )} - {maintenanceMode && ( - - We're undergoing maintenace, thank you for being patient - - )} - - {!provider ? ( - - ) : ( + {!isLoggedIn ? ( <> + + {activeMode === "newUser" + ? "Create an account" + : "Welcome back!"} + + {error && ( + {error} + )} + {maintenanceMode && ( + + We're undergoing maintenance, thank you for being patient + + )} + + {!provider ? ( + + ) : ( + <> + + + + )} + {desktop && ( + + + or + + + )} + - - )} - {desktop && ( - - - or + {activeMode === "newUser" && ( + + By signing up you agree to the
+ + Terms of Service + {" "} + and{" "} + + Privacy Policy + +
+ )} + + {activeMode === "newUser" + ? "Already have an account?" + : "Not registered yet?"} -
- )} - - - - {activeMode === "newUser" && ( - - By signing up you agree to the
- - Terms of Service - {" "} - and{" "} - - Privacy Policy - -
+ {activeMode === "newUser" ? ( + Sign in + ) : ( + Create an account + )} + + + ) : !secured ? ( + + ) : ( + )} - - {activeMode === "newUser" - ? "Already have an account?" - : "Not registered yet?"} - - - {activeMode === "newUser" ? ( - Sign in - ) : ( - Create an account - )} -
diff --git a/packages/files-ui/src/Contexts/DriveContext.tsx b/packages/files-ui/src/Contexts/DriveContext.tsx index 4bbd60c66b..28ae01b4ef 100644 --- a/packages/files-ui/src/Contexts/DriveContext.tsx +++ b/packages/files-ui/src/Contexts/DriveContext.tsx @@ -5,7 +5,7 @@ import { } from "@imploy/api-client" import React, { useCallback, useEffect, useReducer } from "react" import { useState } from "react" -import { useImployApi } from "@imploy/common-contexts" +import { decryptFile, encryptFile, useImployApi } from "@imploy/common-contexts" import dayjs from "dayjs" import { v4 as uuidv4 } from "uuid" import { useToaster } from "@chainsafe/common-components" @@ -16,6 +16,7 @@ import { import { guessContentType } from "../Utils/contentTypeGuesser" import { CancelToken } from "axios" import { t } from "@lingui/macro" +import { readFileAsync } from "../Utils/Helpers" type DriveContextProps = { children: React.ReactNode | React.ReactNode[] @@ -49,10 +50,10 @@ type DriveContext = { deleteFile(cid: string): Promise downloadFile(cid: string): Promise getFileContent( - fileName: string, + cid: string, cancelToken?: CancelToken, onDownloadProgress?: (progressEvent: ProgressEvent) => void, - ): Promise + ): Promise list(body: FilesPathRequest): Promise currentPath: string updateCurrentPath(newPath: string): void @@ -60,6 +61,9 @@ type DriveContext = { uploadsInProgress: UploadProgress[] downloadsInProgress: DownloadProgress[] spaceUsed: number + isMasterPasswordSet: boolean + setMasterPassword(password: string): void + secureDrive(password: string): void } interface IItem extends FileContentResponse { @@ -68,11 +72,18 @@ interface IItem extends FileContentResponse { } const REMOVE_UPLOAD_PROGRESS_DELAY = 5000 +const MAX_FILE_SIZE = 2 * 1024 ** 3 const DriveContext = React.createContext(undefined) const DriveProvider = ({ children }: DriveContextProps) => { - const { imployApiClient, isLoggedIn } = useImployApi() + const { + imployApiClient, + isLoggedIn, + secured, + secureAccount, + validateMasterPassword, + } = useImployApi() const { addToastMessage } = useToaster() const refreshContents = useCallback( @@ -128,6 +139,9 @@ const DriveProvider = ({ children }: DriveContextProps) => { const [pathContents, setPathContents] = useState([]) const [spaceUsed, setSpaceUsed] = useState(0) + const [masterPassword, setMasterPassword] = useState( + undefined, + ) const setCurrentPath = (newPath: string) => dispatchCurrentPath({ type: "add", payload: newPath }) @@ -150,6 +164,12 @@ const DriveProvider = ({ children }: DriveContextProps) => { } }, [imployApiClient, pathContents, isLoggedIn]) + useEffect(() => { + if (!isLoggedIn) { + setMasterPassword(undefined) + } + }, [isLoggedIn]) + const [uploadsInProgress, dispatchUploadsInProgress] = useReducer( uploadsInProgressReducer, [], @@ -162,6 +182,8 @@ const DriveProvider = ({ children }: DriveContextProps) => { const uploadFiles = async (files: File[], path: string) => { const startUploadFile = async () => { + if (!masterPassword) return // TODO: Add better error handling here. + const id = uuidv4() const uploadProgress: UploadProgress = { id, @@ -174,15 +196,30 @@ const DriveProvider = ({ children }: DriveContextProps) => { } dispatchUploadsInProgress({ type: "add", payload: uploadProgress }) try { - const filesParam = files.map((f) => ({ - data: f, - fileName: f.name, - })) + const filesParam = await Promise.all( + files + .filter((f) => f.size <= MAX_FILE_SIZE) + .map(async (f) => { + const fileData = await readFileAsync(f) + const encryptedData = await encryptFile(fileData, masterPassword) + return { + data: new Blob([encryptedData], { type: f.type }), + fileName: f.name, + } + }), + ) + if (filesParam.length !== files.length) { + addToastMessage({ + message: + "We can't encrypt files larger than 2GB. Some items will not be uploaded", + appearance: "error", + }) + } // API call - const result = await imployApiClient.addCSFFiles( filesParam, path, + "", undefined, undefined, (progressEvent: { loaded: number; total: number }) => { @@ -211,6 +248,7 @@ const DriveProvider = ({ children }: DriveContextProps) => { return result } catch (error) { + debugger // setting error let errorMessage = t`Something went wrong. We couldn't upload your file` @@ -313,20 +351,37 @@ const DriveProvider = ({ children }: DriveContextProps) => { } const getFileContent = async ( - fileName: string, + cid: string, cancelToken?: CancelToken, onDownloadProgress?: (progressEvent: ProgressEvent) => void, ) => { + if (!masterPassword) return // TODO: Add better error handling here. + const file = pathContents.find((i) => i.cid === cid) + if (!file) return try { const result = await imployApiClient.getFileContent( { - path: currentPath + fileName, + path: currentPath + file.name, }, cancelToken, onDownloadProgress, ) - return result.data + + if (file.version === 0) { + return result.data + } else { + const decrypted = await decryptFile( + await result.data.arrayBuffer(), + masterPassword, + ) + if (decrypted) { + return new Blob([decrypted], { + type: file.content_type, + }) + } + } } catch (error) { + console.log(error) return Promise.reject() } } @@ -345,7 +400,7 @@ const DriveProvider = ({ children }: DriveContextProps) => { } dispatchDownloadsInProgress({ type: "add", payload: downloadProgress }) const result = await getFileContent( - itemToDownload?.name || "", + itemToDownload.cid, undefined, (progressEvent) => { dispatchDownloadsInProgress({ @@ -390,6 +445,26 @@ const DriveProvider = ({ children }: DriveContextProps) => { } } + const secureDrive = async (password: string) => { + if (secured) return + + const result = await secureAccount(password) + if (result) { + setMasterPassword(password) + } + } + + const setPassword = async (password: string) => { + if (!masterPassword && (await validateMasterPassword(password))) { + setMasterPassword(password) + } else { + console.log( + "The password is already set, or an incorrect password was entered.", + ) + return false + } + } + return ( { uploadsInProgress, spaceUsed, downloadsInProgress, + isMasterPasswordSet: !!masterPassword, + setMasterPassword: setPassword, + secureDrive, }} > {children} diff --git a/packages/files-ui/src/Media/auth.jpg b/packages/files-ui/src/Media/auth.jpg new file mode 100644 index 0000000000..20859ad220 Binary files /dev/null and b/packages/files-ui/src/Media/auth.jpg differ diff --git a/packages/files-ui/src/Utils/Helpers.tsx b/packages/files-ui/src/Utils/Helpers.tsx index 9c93681f48..a7f0f995bb 100644 --- a/packages/files-ui/src/Utils/Helpers.tsx +++ b/packages/files-ui/src/Utils/Helpers.tsx @@ -17,3 +17,17 @@ export const testLocalStorage = () => { return false } } + +export const readFileAsync = (file: Blob): Promise => { + return new Promise((resolve, reject) => { + let reader = new FileReader() + + reader.onload = () => { + reader.result && resolve(reader.result as ArrayBuffer) + } + + reader.onerror = reject + + reader.readAsArrayBuffer(file) + }) +} diff --git a/yarn.lock b/yarn.lock index 590e339cc2..be1dca529f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2218,10 +2218,10 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== -"@imploy/api-client@1.2.9": - version "1.2.9" - resolved "https://npm.pkg.github.com/download/@imploy/api-client/1.2.9/71e0ef959c03618eaf46c183254eaa89110864f621ed61ddf35c6a7811540bd9#0ade09d62e5d869b7e13a1fcbf20bce40ff15feb" - integrity sha512-+D9q0LCZnVI9Rlr+SyPNDHAQgp2jaTKad+d3yIzwTjRzXf5ucXd9khKBfSvzLwPNbWYPAMe0dBwF+/MMmQ2Nzw== +"@imploy/api-client@1.3.2": + version "1.3.2" + resolved "https://npm.pkg.github.com/download/@imploy/api-client/1.3.2/68ecff5b3d63f4610830610ae32b95576df720e08eacca13a8c1a10a47d285b8#5fec1e5785725e5edb073ceb24d4e3ab7f1ed535" + integrity sha512-xRLigU/Q1Q3+GW6QO9Tp85Oe2oPQlZevhdS+uf+aKEaP94c1+6vufD2EoHYa2u5T2tqmrdAh6il+wJJgA5BIUw== "@jest/console@^24.7.1", "@jest/console@^24.9.0": version "24.9.0" @@ -3844,6 +3844,11 @@ resolved "https://registry.yarnpkg.com/@types/jwt-decode/-/jwt-decode-2.2.1.tgz#afdf5c527fcfccbd4009b5fd02d1e18241f2d2f2" integrity sha512-aWw2YTtAdT7CskFyxEX2K21/zSDStuf/ikI3yBqmwpwJF0pS+/IX5DWv+1UFffZIbruP6cnT9/LAJV1gFwAT1A== +"@types/lodash@^4.14.165": + version "4.14.165" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" + integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== + "@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -4141,6 +4146,11 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.9.tgz#e2015187ae5739fd3b791b3b7ab9094f2aa5a474" integrity sha512-ZtjjlrHuHTYctHDz3c8XgInjj0v+Hahe32N/4cDa2banibf9w6aAgxwx0jZtBjKKzmGIU4NXhofEsBW1BbqrNg== +"@types/zxcvbn@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609" + integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg== + "@typescript-eslint/eslint-plugin@^2.10.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" @@ -11309,9 +11319,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@6.5.0: version "6.5.0" @@ -13661,6 +13671,11 @@ nan@^2.12.1, nan@^2.14.0, nan@^2.14.1, nan@^2.2.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -14522,7 +14537,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.17, pbkdf2@^3.0.3: +pbkdf2@^3.0.17, pbkdf2@^3.0.3, pbkdf2@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== @@ -18774,7 +18789,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -tweetnacl@^1.0.0: +tweetnacl@^1.0.0, tweetnacl@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== @@ -20114,13 +20129,20 @@ yargs@^13.0.0, yargs@^13.2.4, yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yup@^0.31.1: - version "0.31.1" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.31.1.tgz#0954cb181161f397b804346037a04f8a4b31599e" - integrity sha512-Lf6648jDYOWR75IlWkVfwesPyW6oj+50NpxlKvsQlpPsB8eI+ndI7b4S1VrwbmeV9hIZDu1MzrlIL4W+gK1jPw== +yup@^0.32.8: + version "0.32.8" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.8.tgz#16e4a949a86a69505abf99fd0941305ac9adfc39" + integrity sha512-SZulv5FIZ9d5H99EN5tRCRPXL0eyoYxWIP1AacCrjC9d4DfP13J1dROdKGfpfRHT3eQB6/ikBl5jG21smAfCkA== dependencies: "@babel/runtime" "^7.10.5" + "@types/lodash" "^4.14.165" lodash "^4.17.20" lodash-es "^4.17.11" + nanoclone "^0.2.1" property-expr "^2.0.4" toposort "^2.0.2" + +zxcvbn@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" + integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=