Skip to content

Commit 25c1dfc

Browse files
authored
feat: add master password sync (#152)
* feat: add master password interface * add more mock interface for login * add remote connection * Change the name in to lower case * remove contributor rendering * remove unused
1 parent fae6600 commit 25c1dfc

File tree

26 files changed

+821
-151
lines changed

26 files changed

+821
-151
lines changed

LICENSE

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
631631
state the exclusion of warranty; and each file should have at least
632632
the "copyright" line and a pointer to where the full notice is found.
633633

634-
QueryM, Database Client
634+
Querym, Database Client
635635
Copyright (C) 2023 Visal .In
636636

637637
This program is free software: you can redistribute it and/or modify
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
652652
If the program does terminal interaction, make it output a short
653653
notice like this when it starts in an interactive mode:
654654

655-
QueryM Copyright (C) 2023 Visal .In
655+
m Copyright (C) 2023 Visal .In
656656
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657657
This is free software, and you are welcome to redistribute it
658658
under certain conditions; type `show c' for details.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@
195195
},
196196
"build": {
197197
"protocols": {
198-
"name": "QueryM",
198+
"name": "Querym",
199199
"schemes": [
200200
"querymaster"
201201
]
202202
},
203-
"productName": "QueryM",
203+
"productName": "Querym",
204204
"appId": "com.invisal.querymaster",
205205
"asar": true,
206206
"asarUnpack": "**\\*.{node,dll}",

src/libs/ConnectionListStorage.ts renamed to src/libs/ConnectionListStorage/ConnectionListLocalStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ConnectionStoreItem } from 'drivers/base/SQLLikeConnection';
22
import { v1 as uuidv1 } from 'uuid';
33
import { db } from 'renderer/db';
44

5-
export default class ConnectionListStorage {
5+
export default class ConnectionListLocalStorage {
66
protected connections: ConnectionStoreItem[] = [];
77
protected dict: Record<string, ConnectionStoreItem> = {};
88

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
ConnectionStoreConfig,
3+
ConnectionStoreItem,
4+
} from 'drivers/base/SQLLikeConnection';
5+
import { QueryDialetType } from 'libs/QueryBuilder';
6+
import RemoteAPI from 'renderer/utils/RemoteAPI';
7+
8+
export default class ConnectionListRemoteStorage {
9+
protected connections: ConnectionStoreItem[] = [];
10+
protected dict: Record<string, ConnectionStoreItem> = {};
11+
protected masterPassword: string;
12+
protected salt: string;
13+
protected api: RemoteAPI;
14+
15+
constructor(api: RemoteAPI, masterPassword: string, salt: string) {
16+
this.api = api;
17+
this.masterPassword = masterPassword;
18+
this.salt = salt;
19+
}
20+
21+
async loadAll() {
22+
const conns = await this.api.getAll();
23+
this.connections = [];
24+
25+
for (const conn of conns.nodes) {
26+
try {
27+
const config: ConnectionStoreConfig = JSON.parse(
28+
await window.electron.decrypt(
29+
conn.content,
30+
this.masterPassword,
31+
this.salt,
32+
),
33+
);
34+
35+
if (config) {
36+
this.connections.push({
37+
config,
38+
id: conn.id,
39+
createdAt: conn.created_at,
40+
lastUsedAt: conn.last_used_at,
41+
name: conn.name,
42+
type: conn.connection_type as QueryDialetType,
43+
});
44+
}
45+
} catch (e) {
46+
console.error(e);
47+
}
48+
}
49+
50+
this.dict = this.connections.reduce(
51+
(acc, cur) => {
52+
acc[cur.id] = cur;
53+
return acc;
54+
},
55+
{} as Record<string, ConnectionStoreItem>,
56+
);
57+
}
58+
59+
get(id: string): ConnectionStoreItem | undefined {
60+
return this.dict[id];
61+
}
62+
63+
getAll(): ConnectionStoreItem[] {
64+
return this.connections;
65+
}
66+
67+
async save(
68+
data: Omit<ConnectionStoreItem, 'id'> & { id?: string },
69+
): Promise<ConnectionStoreItem> {
70+
const r = await this.api.saveConnection(data.id, {
71+
connection_type: data.type,
72+
content: await window.electron.encrypt(
73+
JSON.stringify(data.config),
74+
this.masterPassword,
75+
this.salt,
76+
),
77+
name: data.name,
78+
});
79+
80+
const newData = { ...data, id: r.id };
81+
this.dict[newData.id] = newData;
82+
83+
return newData;
84+
}
85+
86+
async remove(id: string) {
87+
delete this.dict[id];
88+
this.connections = this.connections.filter((conn) => conn.id !== id);
89+
this.api.removeConnection(id);
90+
}
91+
92+
async updateLastUsed(id: string) {
93+
if (this.dict[id]) {
94+
this.dict[id].lastUsedAt = Math.ceil(Date.now() / 1000);
95+
this.api.updateConnectionLastUsed(id);
96+
}
97+
}
98+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ConnectionStoreItem } from 'drivers/base/SQLLikeConnection';
2+
3+
export default abstract class IConnectionListStorage {
4+
abstract loadAll(): Promise<void>;
5+
abstract get(id: string): ConnectionStoreItem | undefined;
6+
abstract getAll(): ConnectionStoreItem[];
7+
abstract save(
8+
data: Omit<ConnectionStoreItem, 'id'> & { id?: string },
9+
): Promise<ConnectionStoreItem>;
10+
abstract remove(id: string): Promise<void>;
11+
abstract updateLastUsed(id: string): Promise<void>;
12+
}

src/libs/SqlRunnerManager.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ export interface SqlStatementWithAnalyze extends SqlStatement {
99

1010
export type BeforeAllEventCallback = (
1111
statements: SqlStatementWithAnalyze[],
12-
skipProtection?: boolean
12+
skipProtection?: boolean,
1313
) => Promise<boolean>;
1414

1515
export type BeforeEachEventCallback = (
1616
statements: SqlStatementWithAnalyze,
17-
skipProtection?: boolean
17+
skipProtection?: boolean,
1818
) => Promise<boolean>;
1919

2020
export interface SqlStatementResult {
@@ -41,13 +41,11 @@ export class SqlRunnerManager {
4141

4242
async execute(
4343
statements: SqlStatement[],
44-
options?: SqlExecuteOption
44+
options?: SqlExecuteOption,
4545
): Promise<SqlStatementResult[]> {
4646
const result: SqlStatementResult[] = [];
4747
const parser = new Parser();
4848

49-
console.log(statements);
50-
5149
// We only wrap transaction if it is multiple statement and
5250
// insideTransactin is specified. Single statement, by itself, is
5351
// transactional already.
@@ -89,7 +87,7 @@ export class SqlRunnerManager {
8987
const startTime = Date.now();
9088
const returnedResult = await this.executor(
9189
statement.sql,
92-
statement.params
90+
statement.params,
9391
);
9492

9593
if (!returnedResult?.error) {
@@ -117,7 +115,7 @@ export class SqlRunnerManager {
117115

118116
unregisterBeforeAll(cb: BeforeAllEventCallback) {
119117
this.beforeAllCallbacks = this.beforeAllCallbacks.filter(
120-
(prevCb) => prevCb !== cb
118+
(prevCb) => prevCb !== cb,
121119
);
122120
}
123121

@@ -127,7 +125,7 @@ export class SqlRunnerManager {
127125

128126
unregisterBeforeEach(cb: BeforeEachEventCallback) {
129127
this.beforeEachCallbacks = this.beforeEachCallbacks.filter(
130-
(prevCb) => prevCb !== cb
128+
(prevCb) => prevCb !== cb,
131129
);
132130
}
133131
}

src/main/ipc/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import './ipc_native_menu';
33
import './ipc_other';
44
import './ipc_rdms';
55
import './ipc_auto_update';
6+
import './ipc_cipher';

src/main/ipc/ipc_cipher.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import crypto from 'crypto';
2+
import CommunicateHandler from './../CommunicateHandler';
3+
4+
export class Encryption {
5+
protected key: Buffer;
6+
7+
constructor(masterkey: string, salt: string) {
8+
this.key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512');
9+
}
10+
11+
decrypt(encdata: string) {
12+
const buffer = Buffer.from(encdata, 'base64');
13+
const iv = buffer.subarray(0, 16);
14+
const data = buffer.subarray(16);
15+
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv);
16+
const text =
17+
decipher.update(data).toString('utf8') + decipher.final('utf8');
18+
return text;
19+
}
20+
21+
encrypt(plain: string) {
22+
const iv = crypto.randomBytes(16);
23+
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv);
24+
return Buffer.concat([
25+
iv,
26+
cipher.update(plain, 'utf8'),
27+
cipher.final(),
28+
]).toString('base64');
29+
}
30+
}
31+
32+
const EncryptionDict: Record<string, Encryption> = {};
33+
34+
CommunicateHandler.handle(
35+
'encrypt',
36+
([text, masterkey, salt]: [string, string, string]) => {
37+
try {
38+
const key = masterkey + '_' + salt;
39+
if (!EncryptionDict[key]) {
40+
EncryptionDict[key] = new Encryption(masterkey, salt);
41+
}
42+
43+
return EncryptionDict[key].encrypt(text);
44+
} catch {
45+
return null;
46+
}
47+
},
48+
);
49+
50+
CommunicateHandler.handle(
51+
'decrypt',
52+
([encrypted, masterkey, salt]: [string, string, string]) => {
53+
try {
54+
const key = masterkey + '_' + salt;
55+
if (!EncryptionDict[key]) {
56+
EncryptionDict[key] = new Encryption(masterkey, salt);
57+
}
58+
59+
return EncryptionDict[key].decrypt(encrypted);
60+
} catch {
61+
return null;
62+
}
63+
},
64+
);

src/main/preload.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const electronHandler = {
5757
// Related to File O/I
5858
// ----------------------------------
5959
showSaveDialog: (
60-
options: SaveDialogSyncOptions
60+
options: SaveDialogSyncOptions,
6161
): Promise<string | undefined> =>
6262
ipcRenderer.invoke('show-save-dialog', [options]),
6363

@@ -78,7 +78,7 @@ const electronHandler = {
7878
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
7979

8080
handleMenuClick: (
81-
callback: (event: IpcRendererEvent, id: string) => void
81+
callback: (event: IpcRendererEvent, id: string) => void,
8282
) => {
8383
if (cacheHandleMenuClickCb) {
8484
ipcRenderer.off('native-menu-click', cacheHandleMenuClickCb);
@@ -88,7 +88,7 @@ const electronHandler = {
8888
},
8989

9090
listenDeeplink: (
91-
callback: (event: IpcRendererEvent, url: string) => void
91+
callback: (event: IpcRendererEvent, url: string) => void,
9292
) => {
9393
ipcRenderer.removeAllListeners('deeplink');
9494
return ipcRenderer.on('deeplink', callback);
@@ -100,41 +100,47 @@ const electronHandler = {
100100
},
101101

102102
listenUpdateAvailable: (
103-
callback: (event: IpcRendererEvent, e: UpdateInfo) => void
103+
callback: (event: IpcRendererEvent, e: UpdateInfo) => void,
104104
) => {
105105
ipcRenderer.removeAllListeners('update-available');
106106
return ipcRenderer.on('update-available', callback);
107107
},
108108

109109
listenUpdateNotAvailable: (
110-
callback: (event: IpcRendererEvent, e: UpdateInfo) => void
110+
callback: (event: IpcRendererEvent, e: UpdateInfo) => void,
111111
) => {
112112
ipcRenderer.removeAllListeners('update-not-available');
113113
return ipcRenderer.on('update-not-available', callback);
114114
},
115115

116116
listenUpdateDownloadProgress: (
117-
callback: (event: IpcRendererEvent, e: ProgressInfo) => void
117+
callback: (event: IpcRendererEvent, e: ProgressInfo) => void,
118118
) => {
119119
ipcRenderer.removeAllListeners('update-download-progress');
120120
return ipcRenderer.on('update-download-progress', callback);
121121
},
122122

123123
listenUpdateDownloaded: (
124-
callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void
124+
callback: (event: IpcRendererEvent, e: UpdateDownloadedEvent) => void,
125125
) => {
126126
ipcRenderer.removeAllListeners('update-downloaded');
127127
return ipcRenderer.on('update-downloaded', callback);
128128
},
129129

130130
listen: function listen<T = unknown[]>(
131131
name: string,
132-
callback: (event: IpcRendererEvent, ...args: T[]) => void
132+
callback: (event: IpcRendererEvent, ...args: T[]) => void,
133133
) {
134134
return ipcRenderer.on(name, callback);
135135
},
136136

137137
openExternal: (url: string) => ipcRenderer.invoke('open-external', [url]),
138+
139+
// Encryption
140+
encrypt: (text: string, masterKey: string, salt: string) =>
141+
ipcRenderer.invoke('encrypt', [text, masterKey, salt]),
142+
decrypt: (encrypted: string, masterKey: string, salt: string) =>
143+
ipcRenderer.invoke('decrypt', [encrypted, masterKey, salt]),
138144
};
139145

140146
contextBridge.exposeInMainWorld('electron', electronHandler);

0 commit comments

Comments
 (0)