From 634f0a35d7ef310b3d970a5de00ae6263b8b6cd3 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 30 Sep 2023 11:54:09 +0700 Subject: [PATCH 1/6] feat: add master password interface --- src/main/ipc/ipc_cipher.ts | 64 +++++++++++++++ src/main/preload.ts | 22 +++-- src/renderer/contexts/AuthProvider.tsx | 10 +-- .../WelcomeScreen/SetupAccountCallout.tsx | 80 +++++++++++++++++++ src/renderer/screens/WelcomeScreen/index.tsx | 47 +++++------ .../screens/WelcomeScreen/styles.module.scss | 29 +++++++ 6 files changed, 216 insertions(+), 36 deletions(-) create mode 100644 src/main/ipc/ipc_cipher.ts create mode 100644 src/renderer/screens/WelcomeScreen/SetupAccountCallout.tsx diff --git a/src/main/ipc/ipc_cipher.ts b/src/main/ipc/ipc_cipher.ts new file mode 100644 index 00000000..8955c331 --- /dev/null +++ b/src/main/ipc/ipc_cipher.ts @@ -0,0 +1,64 @@ +import crypto from 'crypto'; +import CommunicateHandler from './../CommunicateHandler'; + +export class Encryption { + protected key: Buffer; + + constructor(masterkey: string, salt: string) { + this.key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512'); + } + + decrypt(encdata: string) { + const buffer = Buffer.from(encdata, 'base64'); + const iv = buffer.subarray(0, 16); + const data = buffer.subarray(16); + const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv); + const text = + decipher.update(data).toString('utf8') + decipher.final('utf8'); + return text; + } + + encrypt(plain: string) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv); + return Buffer.concat([ + iv, + cipher.update(plain, 'utf8'), + cipher.final(), + ]).toString('base64'); + } +} + +const EncryptionDict: Record = {}; + +CommunicateHandler.handle( + 'encrypt', + ([text, masterkey, salt]: [string, string, string]) => { + try { + const key = masterkey + '_' + salt; + if (!EncryptionDict[key]) { + EncryptionDict[key] = new Encryption(masterkey, salt); + } + + return EncryptionDict[key].encrypt(text); + } catch { + return null; + } + }, +); + +CommunicateHandler.handle( + 'decrypt', + ([encrypted, masterkey, salt]: [string, string, string]) => { + try { + const key = masterkey + '_' + salt; + if (!EncryptionDict[key]) { + EncryptionDict[key] = new Encryption(masterkey, salt); + } + + return EncryptionDict[key].decrypt(encrypted); + } catch { + return null; + } + }, +); diff --git a/src/main/preload.ts b/src/main/preload.ts index c82b6423..f5a8c46b 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -57,7 +57,7 @@ const electronHandler = { // Related to File O/I // ---------------------------------- showSaveDialog: ( - options: SaveDialogSyncOptions + options: SaveDialogSyncOptions, ): Promise => ipcRenderer.invoke('show-save-dialog', [options]), @@ -78,7 +78,7 @@ const electronHandler = { checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), handleMenuClick: ( - callback: (event: IpcRendererEvent, id: string) => void + callback: (event: IpcRendererEvent, id: string) => void, ) => { if (cacheHandleMenuClickCb) { ipcRenderer.off('native-menu-click', cacheHandleMenuClickCb); @@ -88,7 +88,7 @@ const electronHandler = { }, listenDeeplink: ( - callback: (event: IpcRendererEvent, url: string) => void + callback: (event: IpcRendererEvent, url: string) => void, ) => { ipcRenderer.removeAllListeners('deeplink'); return ipcRenderer.on('deeplink', callback); @@ -100,28 +100,28 @@ const electronHandler = { }, listenUpdateAvailable: ( - callback: (event: IpcRendererEvent, e: UpdateInfo) => void + callback: (event: IpcRendererEvent, e: UpdateInfo) => void, ) => { ipcRenderer.removeAllListeners('update-available'); return ipcRenderer.on('update-available', callback); }, listenUpdateNotAvailable: ( - callback: (event: IpcRendererEvent, e: UpdateInfo) => void + callback: (event: IpcRendererEvent, e: UpdateInfo) => void, ) => { ipcRenderer.removeAllListeners('update-not-available'); return ipcRenderer.on('update-not-available', callback); }, listenUpdateDownloadProgress: ( - callback: (event: IpcRendererEvent, e: ProgressInfo) => void + callback: (event: IpcRendererEvent, e: ProgressInfo) => void, ) => { ipcRenderer.removeAllListeners('update-download-progress'); return ipcRenderer.on('update-download-progress', callback); }, listenUpdateDownloaded: ( - callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void + callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void, ) => { ipcRenderer.removeAllListeners('update-downloaded'); return ipcRenderer.on('update-downloaded', callback); @@ -129,12 +129,18 @@ const electronHandler = { listen: function listen( name: string, - callback: (event: IpcRendererEvent, ...args: T[]) => void + callback: (event: IpcRendererEvent, ...args: T[]) => void, ) { return ipcRenderer.on(name, callback); }, openExternal: (url: string) => ipcRenderer.invoke('open-external', [url]), + + // Encryption + encrypt: (text: string, masterKey: string, salt: string) => + ipcRenderer.invoke('encrypt', [text, masterKey, salt]), + decrypt: (encrypted: string, masterKey: string, salt: string) => + ipcRenderer.invoke('decrypt', [encrypted, masterKey, salt]), }; contextBridge.exposeInMainWorld('electron', electronHandler); diff --git a/src/renderer/contexts/AuthProvider.tsx b/src/renderer/contexts/AuthProvider.tsx index db5f555f..0862b1d3 100644 --- a/src/renderer/contexts/AuthProvider.tsx +++ b/src/renderer/contexts/AuthProvider.tsx @@ -12,14 +12,14 @@ import { useDevice } from './DeviceProvider'; import NotImplementCallback from 'libs/NotImplementCallback'; import { parseDeeplinkForToken } from 'libs/ParseDeeplink'; -interface User { +export interface LoginUser { id: number; name: string; picture: string; } const UserContext = createContext<{ - user?: User; + user?: LoginUser; loading: boolean; }>({ loading: true }); @@ -39,9 +39,9 @@ function AuthProviderBody({ children, token, }: PropsWithChildren<{ token?: string | null }>) { - const { data, isLoading } = useSWR( + const { data, isLoading } = useSWR( token ? 'https://api.querymaster.io/v1/user' : null, - { shouldRetryOnError: false, revalidateOnFocus: false } + { shouldRetryOnError: false, revalidateOnFocus: false }, ); return ( @@ -77,7 +77,7 @@ export default function AuthProvider({ children }: PropsWithChildren) { .then((res) => res.data); return r; }, - [deviceId, token] + [deviceId, token], ); const onLogout = useCallback(() => { diff --git a/src/renderer/screens/WelcomeScreen/SetupAccountCallout.tsx b/src/renderer/screens/WelcomeScreen/SetupAccountCallout.tsx new file mode 100644 index 00000000..5ff11ed8 --- /dev/null +++ b/src/renderer/screens/WelcomeScreen/SetupAccountCallout.tsx @@ -0,0 +1,80 @@ +import Button from 'renderer/components/Button'; +import styles from './styles.module.scss'; +import Stack from 'renderer/components/Stack'; +import { LoginUser, useCurrentUser } from 'renderer/contexts/AuthProvider'; +import PasswordField from 'renderer/components/PasswordField'; + +function SetupAccountNotLogin() { + return ( +
+

Setup Account

+ +

Link with your Github account to share your work across devices.

+ +

+ It is safe! All your database credientials are + encrypted using your own master password. We don't store your master + password. +

+ + + + +
+ ); +} + +function SetupAccountDetail({ user }: { user: LoginUser }) { + return ( +
+

Welcome, {user.name}

+

+ Provide us with your master password. It will be used for encrypt and + decrypt your credentials before save on our server. +

+
    +
  • We do not store your master password on our server.
  • +
  • + Forget your master password means that you will also lose your saved + data +
  • +
+ +

+ +
+ ); +} + +export default function SetupAccountCallout() { + const { user } = useCurrentUser(); + return ( +
+ {user ? : } +
+ ); +} diff --git a/src/renderer/screens/WelcomeScreen/index.tsx b/src/renderer/screens/WelcomeScreen/index.tsx index 7f5a9cbc..8a99a990 100644 --- a/src/renderer/screens/WelcomeScreen/index.tsx +++ b/src/renderer/screens/WelcomeScreen/index.tsx @@ -1,11 +1,9 @@ import { useCallback } from 'react'; import Stack from 'renderer/components/Stack'; -import Heading from 'renderer/components/Typo/Heading'; -import imageLogo from './../../../../assets/icon.svg'; -import pkg from '../../../../package.json'; import Contributors from './Contributors'; import Button from 'renderer/components/Button'; import ButtonGroup from 'renderer/components/ButtonGroup'; +import SetupAccountCallout from './SetupAccountCallout'; export default function WelcomeScreen() { const onGithubClicked = useCallback(() => { @@ -17,28 +15,31 @@ export default function WelcomeScreen() { }, []); return ( - - +
+ +
+ +
+ Querym is a free, open-source, and cross-platform + GUI tool for databases. Although this project is relatively young, + we are ambitious in our goal to create one of the best tools + available. +
- QueryM v{pkg.version} +
+ + + + +
-
- QueryM is a complete free open-source cross platform - database graphical client. Please support us on: + {window.env.env !== 'development' && } +
- -
- - - - -
- - {window.env.env !== 'development' && } -
+
); } diff --git a/src/renderer/screens/WelcomeScreen/styles.module.scss b/src/renderer/screens/WelcomeScreen/styles.module.scss index 52bb3064..2d5a5aa1 100644 --- a/src/renderer/screens/WelcomeScreen/styles.module.scss +++ b/src/renderer/screens/WelcomeScreen/styles.module.scss @@ -1,3 +1,32 @@ +.calloutBackground { + background-color: #4158D0; + background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%); + padding: 40px; +} + +.calloutContainer { + background: var(--color-surface); + max-width: 700px; + border-radius: 4px; + padding: 1rem; + + font-size: 1rem; + + h2 { + margin-bottom: 2rem; + } + + p { + margin-bottom: 1rem; + } + + ul { + margin-bottom: 1rem; + margin-top: 1rem; + padding-left: 2rem; + } +} + .contributorList { margin-top: 1rem; list-style: none; From 9a90743297121881fd9013760217ef3ea4b0b9e6 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 30 Sep 2023 22:19:11 +0700 Subject: [PATCH 2/6] add more mock interface for login --- src/main/ipc/index.ts | 1 + .../components/PasswordField/index.tsx | 67 +++++++-------- .../components/TextField/TextAreaField.tsx | 34 ++++++++ src/renderer/components/TextField/index.tsx | 75 ++++++++--------- src/renderer/contexts/AuthProvider.tsx | 32 ++++++- .../ExportConnectionStringItem.tsx | 5 +- .../WelcomeScreen/SetupAccountCallout.tsx | 83 +++++++++++++++++-- 7 files changed, 211 insertions(+), 86 deletions(-) create mode 100644 src/renderer/components/TextField/TextAreaField.tsx diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 1550f073..6e44a010 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -3,3 +3,4 @@ import './ipc_native_menu'; import './ipc_other'; import './ipc_rdms'; import './ipc_auto_update'; +import './ipc_cipher'; diff --git a/src/renderer/components/PasswordField/index.tsx b/src/renderer/components/PasswordField/index.tsx index 8b8255cf..2c24cb2f 100644 --- a/src/renderer/components/PasswordField/index.tsx +++ b/src/renderer/components/PasswordField/index.tsx @@ -1,38 +1,39 @@ import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useState } from 'react'; +import { useState, forwardRef } from 'react'; import TextField, { TextFieldCommonProps } from 'renderer/components/TextField'; -export default function PasswordField({ - label, - value, - autoFocus, - onChange, - placeholder, - readOnly, -}: TextFieldCommonProps) { - const props = { - label, - value, - autoFocus, - onChange, - placeholder, - readOnly, - }; - const [showPassword, setShowPassword] = useState(false); +const PasswordField = forwardRef( + function PasswordField( + { label, value, autoFocus, onChange, placeholder, readOnly }, + ref, + ) { + const props = { + label, + value, + autoFocus, + onChange, + placeholder, + readOnly, + }; + const [showPassword, setShowPassword] = useState(false); - return ( - setShowPassword(!showPassword)} - actionIcon={ - showPassword ? ( - - ) : ( - - ) - } - {...props} - /> - ); -} + return ( + setShowPassword(!showPassword)} + actionIcon={ + showPassword ? ( + + ) : ( + + ) + } + {...props} + /> + ); + }, +); + +export default PasswordField; diff --git a/src/renderer/components/TextField/TextAreaField.tsx b/src/renderer/components/TextField/TextAreaField.tsx new file mode 100644 index 00000000..e6baabef --- /dev/null +++ b/src/renderer/components/TextField/TextAreaField.tsx @@ -0,0 +1,34 @@ +import { forwardRef } from 'react'; +import styles from './styles.module.scss'; +import { TextFieldCommonProps } from '.'; + +const TextAreaField = forwardRef( + function TextField( + { label, value, autoFocus, onChange, placeholder, readOnly, onKeyDown }, + ref, + ) { + return ( +
+ {label && } + +
+