1- import { app , session , BrowserWindow } from 'electron' ;
1+ /* eslint-disable import/order */
2+ import path from 'path' ;
3+ import crypto from 'crypto' ;
4+
25import { rimraf } from 'rimraf' ;
6+ import { app , session , BrowserWindow , ipcMain , BrowserView } from 'electron' ;
37
48// eslint-disable-next-line @typescript-eslint/ban-ts-comment
59// @ts -ignore:next-line
@@ -12,22 +16,26 @@ import packageJson from '../../../package.json';
1216import { JITSI_SERVER_CAPTURE_SCREEN_PERMISSIONS_CLEARED } from '../../jitsi/actions' ;
1317import { dispatch , listen } from '../../store' ;
1418import { readSetting } from '../../store/readSetting' ;
19+ import {
20+ APP_ALLOWED_NTLM_CREDENTIALS_DOMAINS_SET ,
21+ APP_MAIN_WINDOW_TITLE_SET ,
22+ APP_PATH_SET ,
23+ APP_VERSION_SET ,
24+ APP_SCREEN_CAPTURE_FALLBACK_FORCED_SET ,
25+ } from '../actions' ;
1526import {
1627 SETTINGS_CLEAR_PERMITTED_SCREEN_CAPTURE_PERMISSIONS ,
1728 SETTINGS_NTLM_CREDENTIALS_CHANGED ,
1829 SETTINGS_SET_HARDWARE_ACCELERATION_OPT_IN_CHANGED ,
1930 SETTINGS_SET_IS_VIDEO_CALL_SCREEN_CAPTURE_FALLBACK_ENABLED_CHANGED ,
31+ SETTINGS_SET_SCREEN_LOCK_PASSWORD_CHANGED ,
32+ SETTINGS_SET_SCREEN_LOCK_PASSWORD_HASHED ,
33+ MENU_BAR_LOCK_SCREEN_CLICKED ,
2034} from '../../ui/actions' ;
2135import { askForClearScreenCapturePermission } from '../../ui/main/dialogs' ;
2236import { getRootWindow } from '../../ui/main/rootWindow' ;
2337import { preloadBrowsersList } from '../../utils/browserLauncher' ;
24- import {
25- APP_ALLOWED_NTLM_CREDENTIALS_DOMAINS_SET ,
26- APP_MAIN_WINDOW_TITLE_SET ,
27- APP_PATH_SET ,
28- APP_VERSION_SET ,
29- APP_SCREEN_CAPTURE_FALLBACK_FORCED_SET ,
30- } from '../actions' ;
38+ import { getPersistedValues } from './persistence' ;
3139
3240export const packageJsonInformation = {
3341 productName : packageJson . productName ,
@@ -39,8 +47,6 @@ export const electronBuilderJsonInformation = {
3947 protocol : electronBuilderJson . protocols . schemes [ 0 ] ,
4048} ;
4149
42- let isScreenCaptureFallbackForced = false ;
43-
4450export const getPlatformName = ( ) : string => {
4551 switch ( process . platform ) {
4652 case 'win32' :
@@ -54,6 +60,215 @@ export const getPlatformName = (): string => {
5460 }
5561} ;
5662
63+ let isScreenCaptureFallbackForced = false ;
64+
65+ let lockWindow : BrowserView | null = null ;
66+
67+ // Track original rootWindow state while locked so we can restore it on unlock
68+ const lockState : {
69+ originalBounds ?: Electron . Rectangle ;
70+ prevResizable ?: boolean ;
71+ prevMinimizable ?: boolean ;
72+ prevMaximizable ?: boolean ;
73+ moveListener ?: ( ) => void ;
74+ resizeListener ?: ( ) => void ;
75+ } = { } ;
76+
77+ const showLockWindow = async ( ) : Promise < void > => {
78+ try {
79+ const persisted = getPersistedValues ( ) ;
80+ if ( ! persisted ?. screenLockPasswordHash ) {
81+ console . log ( 'No screen lock password configured; skipping lock overlay' ) ;
82+ return ;
83+ }
84+
85+ const rootWindow = await getRootWindow ( ) ;
86+
87+ if ( lockWindow && ! lockWindow . webContents . isDestroyed ( ) ) {
88+ return ;
89+ }
90+
91+ // Save current window flags and bounds so we can restore them
92+ try {
93+ lockState . originalBounds = rootWindow . getBounds ( ) ;
94+ lockState . prevResizable = rootWindow . isResizable ( ) ;
95+ lockState . prevMinimizable = rootWindow . isMinimizable ( ) ;
96+ lockState . prevMaximizable = rootWindow . isMaximizable ( ) ;
97+ // Capture movability if available on the platform
98+ try {
99+ if ( typeof ( rootWindow as any ) . isMovable === 'function' ) {
100+ ( lockState as any ) . prevMovable = ( rootWindow as any ) . isMovable ( ) ;
101+ }
102+ } catch ( e ) {
103+ // ignore
104+ }
105+ } catch ( e ) {
106+ // if anything fails while saving/enforcing, continue — locking view still provides protection over content
107+ }
108+
109+ // Create a BrowserView and attach it to the root window so the lock screen
110+ // appears inside the application window (not above other apps) and covers
111+ // the entire content area.
112+ lockWindow = new BrowserView ( {
113+ webPreferences : {
114+ nodeIntegration : true ,
115+ contextIsolation : false ,
116+ } ,
117+ } ) ;
118+
119+ rootWindow . setBrowserView ( lockWindow ) ;
120+
121+ const updateBounds = ( ) => {
122+ try {
123+ const { width, height } = rootWindow . getContentBounds ( ) ;
124+ lockWindow ?. setBounds ( { x : 0 , y : 0 , width, height } ) ;
125+ } catch ( e ) {
126+ /* ignore errors while updating bounds */
127+ }
128+ } ;
129+
130+ // ensure the view resizes with the window
131+ lockWindow . setAutoResize ( { width : true , height : true } ) ;
132+ updateBounds ( ) ;
133+
134+ // keep bounds updated on move/resize
135+ rootWindow . addListener ( 'resize' , updateBounds ) ;
136+ rootWindow . addListener ( 'move' , updateBounds ) ;
137+
138+ lockWindow . webContents . loadFile (
139+ path . join ( app . getAppPath ( ) , 'app/lockScreen.html' )
140+ ) ;
141+
142+ lockWindow . webContents . once ( 'did-finish-load' , async ( ) => {
143+ try {
144+ await lockWindow ?. webContents . executeJavaScript ( `
145+ window.electronAPI = {
146+ verifyPassword: (password) => require('electron').ipcRenderer.invoke('lock:verify', password),
147+ unlockApp: () => require('electron').ipcRenderer.invoke('lock:unlock')
148+ };
149+ null;
150+ ` ) ;
151+ } catch ( e ) {
152+ console . warn ( 'Failed to inject electronAPI into lock view' , e ) ;
153+ }
154+
155+ lockWindow ?. webContents . focus ( ) ;
156+ } ) ;
157+
158+ // cleanup when view is destroyed
159+ lockWindow . webContents . once ( 'destroyed' , ( ) => {
160+ try {
161+ rootWindow . removeBrowserView ( lockWindow as BrowserView ) ;
162+ } catch ( e ) {
163+ // ignore
164+ }
165+ lockWindow = null ;
166+
167+ // restore window flags and remove listeners
168+ try {
169+ if ( lockState . moveListener ) {
170+ rootWindow . removeListener ( 'move' , lockState . moveListener ) ;
171+ }
172+ if ( lockState . resizeListener ) {
173+ rootWindow . removeListener ( 'resize' , lockState . resizeListener ) ;
174+ }
175+
176+ // remove the updateBounds listeners we added earlier
177+ rootWindow . removeListener ( 'resize' , updateBounds ) ;
178+ rootWindow . removeListener ( 'move' , updateBounds ) ;
179+
180+ if ( typeof lockState . prevResizable === 'boolean' ) {
181+ rootWindow . setResizable ( ! ! lockState . prevResizable ) ;
182+ }
183+ if ( typeof lockState . prevMinimizable === 'boolean' ) {
184+ rootWindow . setMinimizable ( ! ! lockState . prevMinimizable ) ;
185+ }
186+ if ( typeof lockState . prevMaximizable === 'boolean' ) {
187+ rootWindow . setMaximizable ( ! ! lockState . prevMaximizable ) ;
188+ }
189+ if (
190+ typeof rootWindow . setMovable === 'function' &&
191+ typeof ( lockState as any ) . prevMovable !== 'undefined'
192+ ) {
193+ rootWindow . setMovable ( ! ! ( lockState as any ) . prevMovable ) ;
194+ }
195+ } catch ( e ) {
196+ // ignore
197+ }
198+
199+ // clear stored state
200+ lockState . originalBounds = undefined ;
201+ lockState . prevResizable = undefined ;
202+ lockState . prevMinimizable = undefined ;
203+ lockState . prevMaximizable = undefined ;
204+ lockState . moveListener = undefined ;
205+ lockState . resizeListener = undefined ;
206+ } ) ;
207+ } catch ( error ) {
208+ console . error ( 'Error showing lock window:' , error ) ;
209+ }
210+ } ;
211+
212+ // Register lock-related IPC handlers. This is called from `setupApp` after Electron is ready
213+ export const registerLockIpcHandlers = ( ) : void => {
214+ if ( ! ipcMain || typeof ipcMain . handle !== 'function' ) {
215+ // eslint-disable-next-line no-console
216+ console . warn (
217+ 'ipcMain is not available; lock IPC handlers will not be registered.'
218+ ) ;
219+ return ;
220+ }
221+
222+ ipcMain . handle ( 'lock:verify' , async ( _event , password : string ) => {
223+ try {
224+ const persisted = getPersistedValues ( ) ;
225+ const storedHash = persisted ?. screenLockPasswordHash ?? null ;
226+ if ( ! storedHash ) {
227+ return false ;
228+ }
229+
230+ const hash = crypto
231+ . createHash ( 'sha256' )
232+ . update ( String ( password ) )
233+ . digest ( 'hex' ) ;
234+
235+ return hash === storedHash ;
236+ } catch ( error ) {
237+ console . error ( 'Error verifying lock password:' , error ) ;
238+ return false ;
239+ }
240+ } ) ;
241+
242+ ipcMain . handle ( 'lock:unlock' , async ( ) => {
243+ try {
244+ if ( lockWindow && ! lockWindow . webContents . isDestroyed ( ) ) {
245+ try {
246+ const rootWindow = await getRootWindow ( ) ;
247+ rootWindow . removeBrowserView ( lockWindow ) ;
248+ } catch ( e ) {
249+ // ignore
250+ }
251+ try {
252+ // WebContents.destroy may not be in the TypeScript definitions, cast to any
253+ ( lockWindow . webContents as any ) . destroy ?.( ) ;
254+ } catch ( e ) {
255+ // ignore
256+ }
257+ lockWindow = null ;
258+ }
259+ const rootWindow = await getRootWindow ( ) ;
260+ if ( rootWindow && ! rootWindow . isDestroyed ( ) ) {
261+ rootWindow . show ( ) ;
262+ rootWindow . focus ( ) ;
263+ }
264+ return true ;
265+ } catch ( error ) {
266+ console . error ( 'Error unlocking app:' , error ) ;
267+ return false ;
268+ }
269+ } ) ;
270+ } ;
271+
57272export const relaunchApp = ( ...args : string [ ] ) : void => {
58273 const command = process . argv . slice ( 1 , app . isPackaged ? 1 : 2 ) ;
59274 app . relaunch ( { args : [ ...command , ...args ] } ) ;
@@ -171,7 +386,11 @@ export const setupApp = (): void => {
171386 } , 100 ) ; // Brief delay to let window state stabilize
172387 } ) ;
173388
174- app . whenReady ( ) . then ( ( ) => preloadBrowsersList ( ) ) ;
389+ // Register IPC handlers and other readiness tasks once Electron is ready
390+ app . whenReady ( ) . then ( ( ) => {
391+ preloadBrowsersList ( ) ;
392+ registerLockIpcHandlers ( ) ;
393+ } ) ;
175394
176395 listen ( SETTINGS_SET_HARDWARE_ACCELERATION_OPT_IN_CHANGED , ( ) => {
177396 relaunchApp ( ) ;
@@ -199,6 +418,27 @@ export const setupApp = (): void => {
199418 }
200419 ) ;
201420
421+ // Hash and persist screen lock password when renderer sends plaintext
422+ listen ( SETTINGS_SET_SCREEN_LOCK_PASSWORD_CHANGED , ( action ) => {
423+ try {
424+ const plain = action . payload || '' ;
425+ const hash = plain
426+ ? crypto . createHash ( 'sha256' ) . update ( String ( plain ) ) . digest ( 'hex' )
427+ : null ;
428+ dispatch ( {
429+ type : SETTINGS_SET_SCREEN_LOCK_PASSWORD_HASHED ,
430+ payload : hash ,
431+ } ) ;
432+ } catch ( error ) {
433+ console . error ( 'Error hashing screen lock password:' , error ) ;
434+ }
435+ } ) ;
436+
437+ // Show lock overlay when menu item clicked
438+ listen ( MENU_BAR_LOCK_SCREEN_CLICKED , async ( ) => {
439+ await showLockWindow ( ) ;
440+ } ) ;
441+
202442 listen ( APP_ALLOWED_NTLM_CREDENTIALS_DOMAINS_SET , ( action ) => {
203443 if ( action . payload . length > 0 ) {
204444 session . defaultSession . allowNTLMCredentialsForDomains ( action . payload ) ;
0 commit comments