Class: Lich::Common::GUI::MasterPasswordPromptUI

Inherits:
Object
  • Object
show all
Defined in:
documented/common/gui/master_password_prompt_ui.rb

Overview

UI for prompting the user for the master password. This class provides various dialog methods for password input and validation.

Examples:

Showing the master password dialog

result = Lich::Common::GUI::MasterPasswordPromptUI.show_dialog

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.show_dialogString?

Displays a dialog for the user to enter their master password. This method blocks until the dialog is completed.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_dialog

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'documented/common/gui/master_password_prompt_ui.rb', line 15

def self.show_dialog
  # Block until dialog completes, using condition variable for sync
  result = nil
  mutex = Mutex.new
  condition = ConditionVariable.new

  Gtk.queue do
    result = new.create_dialog
    mutex.synchronize { condition.signal }
  end

  # Wait for dialog to complete on main thread
  mutex.synchronize { condition.wait(mutex) }
  result
end

.show_password_confirmation_for_mode_change(validation_test = nil, leaving_enhanced: false) ⇒ String?

Displays a dialog to confirm the master password when changing encryption modes. This method blocks until the dialog is completed.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_password_confirmation_for_mode_change(validation_test)

Parameters:

  • validation_test (Object, nil) (defaults to: nil)

    Optional validation test for the password.

  • leaving_enhanced (Boolean) (defaults to: false)

    Indicates if the user is leaving enhanced mode.

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'documented/common/gui/master_password_prompt_ui.rb', line 59

def self.show_password_confirmation_for_mode_change(validation_test = nil, leaving_enhanced: false)
  result = nil
  mutex = Mutex.new
  condition = ConditionVariable.new

  if leaving_enhanced
    message = "<b>Confirm Master Password</b>\n\n" +
              "Enter your master password to change encryption modes.\n\n" +
              "Your password will be removed from Keychain after the mode change."
  else
    message = "<b>Confirm Master Password</b>\n\n" +
              "Enter your master password to enable enhanced encryption.\n\n" +
              "Your password will be stored securely in your system Keychain."
  end

  Gtk.queue do
    result = new.create_password_validation_dialog(
      validation_test,
      title: "Confirm Master Password",
      instructions: message
    )
    mutex.synchronize { condition.signal }
  end

  mutex.synchronize { condition.wait(mutex) }
  result
end

.show_password_for_data_access(validation_test = nil) ⇒ String?

Displays a dialog for the user to enter their master password to access data. This method blocks until the dialog is completed.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_password_for_data_access(validation_test)

Parameters:

  • validation_test (Object, nil) (defaults to: nil)

    Optional validation test for the password.

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'documented/common/gui/master_password_prompt_ui.rb', line 93

def self.show_password_for_data_access(validation_test = nil)
  result = nil
  mutex = Mutex.new
  condition = ConditionVariable.new

  Gtk.queue do
    result = new.create_password_validation_dialog(
      validation_test,
      title: "Enter Master Password",
      instructions: "<b>Provide Master Password</b>\n\n" +
                   "Your data is encrypted with a master password.\n\n" +
                   "Enter your master password to access and convert your saved entries."
    )
    mutex.synchronize { condition.signal }
  end

  mutex.synchronize { condition.wait(mutex) }
  result
end

.show_password_recovery_dialog(validation_test = nil) ⇒ String?

Displays a dialog for the user to enter their master password for recovery. This method blocks until the dialog is completed.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_password_recovery_dialog(validation_test)

Parameters:

  • validation_test (Object, nil) (defaults to: nil)

    Optional validation test for the password.

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'documented/common/gui/master_password_prompt_ui.rb', line 119

def self.show_password_recovery_dialog(validation_test = nil)
  result = nil
  mutex = Mutex.new
  condition = ConditionVariable.new

  Gtk.queue do
    result = new.create_password_validation_dialog(
      validation_test,
      title: "Recover Master Password",
      instructions: "<b>Recover Master Password</b>\n\n" +
                   "Your master password was removed from your system Keychain.\n\n" +
                   "Enter your existing master password to restore access to your encrypted credentials.",
      show_success_dialog: true
    )
    mutex.synchronize { condition.signal }
  end

  mutex.synchronize { condition.wait(mutex) }
  result
end

.show_recovery_dialog(validation_test = nil) ⇒ String?

Displays a dialog for the user to recover their master password. This method is an alias for show_password_recovery_dialog.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_recovery_dialog(validation_test)

Parameters:

  • validation_test (Object, nil) (defaults to: nil)

    Optional validation test for the password.

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



146
147
148
# File 'documented/common/gui/master_password_prompt_ui.rb', line 146

def self.show_recovery_dialog(validation_test = nil)
  show_password_recovery_dialog(validation_test)
end

.show_recovery_success_dialogString?

Displays a dialog indicating that the master password recovery was successful. This method blocks until the dialog is completed.

Examples:

password = Lich::Common::GUI::MasterPasswordPromptUI.show_recovery_success_dialog

Returns:

  • (String, nil)

    The entered master password or nil if canceled.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'documented/common/gui/master_password_prompt_ui.rb', line 36

def self.show_recovery_success_dialog
  # Block until dialog completes, using condition variable for sync
  result = nil
  mutex = Mutex.new
  condition = ConditionVariable.new

  Gtk.queue do
    result = new.create_recovery_success_dialog
    mutex.synchronize { condition.signal }
  end

  # Wait for dialog to complete on main thread
  mutex.synchronize { condition.wait(mutex) }
  result
end

Instance Method Details

#create_and_wire_show_password_checkbox(content_box, entries_to_toggle) ⇒ Gtk::CheckButton

Creates a checkbox to toggle the visibility of password entries.

Examples:

checkbox = create_and_wire_show_password_checkbox(content_box, [password_entry])

Parameters:

  • content_box (Gtk::Box)

    The box to which the checkbox will be added.

  • entries_to_toggle (Array<Gtk::Entry>)

    The entries whose visibility will be toggled.

Returns:

  • (Gtk::CheckButton)

    The created checkbox.



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
# File 'documented/common/gui/master_password_prompt_ui.rb', line 539

def create_and_wire_show_password_checkbox(content_box, entries_to_toggle)
  show_password_check = Gtk::CheckButton.new("Show password")
  show_password_check.active = false

  Accessibility.make_accessible(
    show_password_check,
    "Show Password Checkbox",
    "Toggle to display password characters",
    :check_button
  )

  content_box.pack_start(show_password_check, expand: false)

  show_password_check.signal_connect('toggled') do |_widget|
    entries_to_toggle.each { |entry| entry.visibility = show_password_check.active? }
  end

  show_password_check
end

#create_dialogObject



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'documented/common/gui/master_password_prompt_ui.rb', line 150

def create_dialog
  # Create modal dialog for master password creation
  dialog = Gtk::Dialog.new(
    title: "Create Master Password",
    parent: nil,
    flags: :modal,
    buttons: [
      [Gtk::Stock::OK, Gtk::ResponseType::OK],
      [Gtk::Stock::CANCEL, Gtk::ResponseType::CANCEL]
    ]
  )

  dialog.set_default_size(500, 450)

  content_box = Gtk::Box.new(:vertical, 12)
  content_box.border_width = 12

  # ====================================================================
  # SECTION 1: Instructions
  # ====================================================================
  instructions = Gtk::Label.new
  instructions.markup = "<b>Create Master Password</b>\n\n" +
                        "This password protects all your saved login credentials.\n" +
                        "Choose a strong password you can remember.\n\n" +
                        "Suggested length: <b>12 characters minimum</b>"
  instructions.wrap = true
  instructions.justify = :left
  content_box.pack_start(instructions, expand: false)

  # ====================================================================
  # SECTION 2: Password Input
  # ====================================================================
  password_label = Gtk::Label.new("Enter Master Password:")
  content_box.pack_start(password_label, expand: false)

  password_entry = Gtk::Entry.new
  password_entry.visibility = false
  password_entry.placeholder_text = "Enter password here"
  content_box.pack_start(password_entry, expand: false)

  # ====================================================================
  # SECTION 3: Real-time Strength Meter
  # ====================================================================
  strength_box = Gtk::Box.new(:horizontal, 10)

  strength_label = Gtk::Label.new("Strength:")
  strength_label.width_request = 70
  strength_box.pack_start(strength_label, expand: false)

  strength_bar = Gtk::ProgressBar.new
  strength_bar.fraction = 0.0
  strength_box.pack_start(strength_bar, expand: true)

  strength_text = Gtk::Label.new("Very Weak")
  strength_text.width_request = 80
  strength_box.pack_start(strength_text, expand: false)

  content_box.pack_start(strength_box, expand: false)

  # ====================================================================
  # SECTION 4: Category Checklist
  # ====================================================================
  category_frame = Gtk::Frame.new("Password Requirements")
  category_box = Gtk::Box.new(:vertical, 5)
  category_box.border_width = 10

  uppercase_icon = Gtk::Label.new
  uppercase_icon.markup = "<span foreground='gray'>✗</span>"
  uppercase_label = Gtk::Label.new("Uppercase letters (A-Z)")
  uppercase_item = Gtk::Box.new(:horizontal, 5)
  uppercase_item.pack_start(uppercase_icon, expand: false)
  uppercase_item.pack_start(uppercase_label, expand: true)
  category_box.pack_start(uppercase_item, expand: false)

  lowercase_icon = Gtk::Label.new
  lowercase_icon.markup = "<span foreground='gray'>✗</span>"
  lowercase_label = Gtk::Label.new("Lowercase letters (a-z)")
  lowercase_item = Gtk::Box.new(:horizontal, 5)
  lowercase_item.pack_start(lowercase_icon, expand: false)
  lowercase_item.pack_start(lowercase_label, expand: true)
  category_box.pack_start(lowercase_item, expand: false)

  numbers_icon = Gtk::Label.new
  numbers_icon.markup = "<span foreground='gray'>✗</span>"
  numbers_label = Gtk::Label.new("Numbers (0-9)")
  numbers_item = Gtk::Box.new(:horizontal, 5)
  numbers_item.pack_start(numbers_icon, expand: false)
  numbers_item.pack_start(numbers_label, expand: true)
  category_box.pack_start(numbers_item, expand: false)

  special_icon = Gtk::Label.new
  special_icon.markup = "<span foreground='gray'>✗</span>"
  special_label = Gtk::Label.new("Special characters (!@#$%^&*)")
  special_item = Gtk::Box.new(:horizontal, 5)
  special_item.pack_start(special_icon, expand: false)
  special_item.pack_start(special_label, expand: true)
  category_box.pack_start(special_item, expand: false)

  length_icon = Gtk::Label.new
  length_icon.markup = "<span foreground='gray'>✗</span>"
  length_label = Gtk::Label.new("Length: 0 / 12")
  length_item = Gtk::Box.new(:horizontal, 5)
  length_item.pack_start(length_icon, expand: false)
  length_item.pack_start(length_label, expand: true)
  category_box.pack_start(length_item, expand: false)

  category_frame.add(category_box)
  content_box.pack_start(category_frame, expand: false)

  # ====================================================================
  # SECTION 5: Confirmation Password
  # ====================================================================
  confirm_label = Gtk::Label.new("Confirm Master Password:")
  content_box.pack_start(confirm_label, expand: false)

  confirm_entry = Gtk::Entry.new
  confirm_entry.visibility = false
  confirm_entry.placeholder_text = "Re-enter password to confirm"
  content_box.pack_start(confirm_entry, expand: false)

  # ====================================================================
  # SECTION 6: Password Match Status
  # ====================================================================
  match_status = Gtk::Label.new("")
  match_status.justify = :left
  content_box.pack_start(match_status, expand: false)

  # ====================================================================
  # SECTION 7: Show Password Checkbox
  # ====================================================================
  create_and_wire_show_password_checkbox(content_box, [password_entry, confirm_entry])

  # ====================================================================
  # Real-time strength updates and password matching
  # ====================================================================
  # Helper to update password match status
  update_match_status = lambda do
    if password_entry.text.empty? && confirm_entry.text.empty?
      match_status.markup = ""
    elsif password_entry.text == confirm_entry.text && !password_entry.text.empty?
      match_status.markup = "<span foreground='#44ff44'>✓ Passwords match</span>"
    else
      match_status.markup = "<span foreground='#ff4444'>✗ Passwords do not match</span>"
    end
  end

  password_entry.signal_connect('changed') do
    password = password_entry.text
    strength = calculate_password_strength(password)
    strength_bar.fraction = strength / 100.0
    strength_text.text = get_strength_label(strength)

    # Update category icons
    update_category_icon(uppercase_icon, password.match?(/[A-Z]/), '#44ff44')
    update_category_icon(lowercase_icon, password.match?(/[a-z]/), '#44ff44')
    update_category_icon(numbers_icon, password.match?(/[0-9]/), '#44ff44')
    update_category_icon(special_icon, password.match?(/[!@#$%^&*\-_=+\[\]{};:'\",.<>?\/\\|`~]/), '#44ff44')
    update_category_icon(length_icon, password.length >= 12, '#44ff44')
    length_label.text = "Length: #{password.length} / 12"

    # Update password match status
    update_match_status.call
  end

  confirm_entry.signal_connect('changed') do
    # Update password match status
    update_match_status.call
  end

  # Set content area
  dialog.child.add(content_box)
  dialog.show_all

  # ====================================================================
  # Dialog Response Handling
  # ====================================================================
  password = nil
  response = dialog.run

  if response == Gtk::ResponseType::OK
    entered_password = password_entry.text
    confirm_password = confirm_entry.text

    if entered_password.empty?
      show_error_dialog("Password cannot be empty")
      password = nil
    elsif entered_password != confirm_password
      show_error_dialog("Passwords do not match")
      password = nil
    else
      password = entered_password
    end
  end

  dialog.destroy
  password
end

#create_password_validation_dialog(validation_test = nil, title: "Validate Master Password", instructions: "Enter your master password:", show_success_dialog: false) ⇒ Hash

Creates a modal dialog for validating the master password. This method blocks until the dialog is completed.

Examples:

result = Lich::Common::GUI::MasterPasswordPromptUI.create_password_validation_dialog(validation_test, title: "Validate Password", instructions: "Enter your password:")

Parameters:

  • validation_test (Object, nil) (defaults to: nil)

    Optional validation test for the password.

  • title (String) (defaults to: "Validate Master Password")

    The title of the dialog.

  • instructions (String) (defaults to: "Enter your master password:")

    Instructions to display in the dialog.

  • show_success_dialog (Boolean) (defaults to: false)

    Indicates if a success dialog should be shown after validation.

Returns:

  • (Hash)

    A hash containing the entered password and a flag indicating if the session should continue.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'documented/common/gui/master_password_prompt_ui.rb', line 357

def create_password_validation_dialog(validation_test = nil, title: "Validate Master Password", instructions: "Enter your master password:", show_success_dialog: false)
  # Create modal dialog for password validation
  # Single password entry - validates against PBKDF2 test
  dialog = Gtk::Dialog.new(
    title: title,
    parent: nil,
    flags: :modal,
    buttons: [
      [Gtk::Stock::OK, Gtk::ResponseType::OK],
      [Gtk::Stock::CANCEL, Gtk::ResponseType::CANCEL]
    ]
  )

  dialog.set_default_size(500, 250)

  content_box = Gtk::Box.new(:vertical, 12)
  content_box.border_width = 12

  # ====================================================================
  # SECTION 1: Instructions
  # ====================================================================
  instructions_label = Gtk::Label.new
  instructions_label.markup = instructions
  instructions_label.wrap = true
  instructions_label.justify = :left
  content_box.pack_start(instructions_label, expand: false)

  # ====================================================================
  # SECTION 2: Password Input
  # ====================================================================
  password_label = Gtk::Label.new("Enter Master Password:")
  content_box.pack_start(password_label, expand: false)

  password_entry = Gtk::Entry.new
  password_entry.visibility = false
  password_entry.placeholder_text = "Enter your master password"
  content_box.pack_start(password_entry, expand: false)

  # ====================================================================
  # SECTION 3: Show Password Checkbox
  # ====================================================================
  create_and_wire_show_password_checkbox(content_box, [password_entry])

  # ====================================================================
  # Error Message Label
  # ====================================================================
  error_label = Gtk::Label.new("")
  error_label.justify = :left
  content_box.pack_start(error_label, expand: false)

  # Set content area
  dialog.child.add(content_box)
  dialog.show_all

  # ====================================================================
  # Dialog Response Handling with Validation
  # ====================================================================
  password = nil
  continue_session = false

  loop do
    response = dialog.run

    if response == Gtk::ResponseType::OK
      entered_password = password_entry.text

      if entered_password.empty?
        error_label.markup = "<span foreground='#ff4444'>Password cannot be empty</span>"
        next
      elsif validation_test && !validation_test.empty?
        # Validate password correctness against PBKDF2 test
        unless MasterPasswordManager.validate_master_password(entered_password, validation_test)
          error_label.markup = "<span foreground='#ff4444'>Incorrect password. Please try again.</span>"
          password_entry.text = ""
          password_entry.grab_focus
          next
        end
      end

      # Validation passed - password is correct
      password = entered_password

      # Show success confirmation if appropriate for this context
      if show_success_dialog
        success_result = create_recovery_success_dialog
        continue_session = success_result[:continue_session]
      end
      break
    elsif response == Gtk::ResponseType::CANCEL
      password = nil
      break
    end
  end

  dialog.destroy
  { password: password, continue_session: continue_session }
end

#create_recovery_dialog(validation_test = nil) ⇒ Object



455
456
457
458
459
460
461
462
463
# File 'documented/common/gui/master_password_prompt_ui.rb', line 455

def create_recovery_dialog(validation_test = nil)
  create_password_validation_dialog(
    validation_test,
    title: "Recover Master Password",
    instructions: "<b>Recover Master Password</b>\n\n" +
                 "Your master password was removed from your system Keychain.\n\n" +
                 "Enter your existing master password to restore access to your encrypted credentials."
  )
end

#create_recovery_success_dialogHash

Creates a modal dialog for confirming the success of the master password recovery. This method blocks until the dialog is completed.

Examples:

result = Lich::Common::GUI::MasterPasswordPromptUI.create_recovery_success_dialog

Returns:

  • (Hash)

    A hash containing a flag indicating if the session should continue.



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'documented/common/gui/master_password_prompt_ui.rb', line 470

def create_recovery_success_dialog
  # Create modal dialog for master password recovery success confirmation
  dialog = Gtk::Dialog.new(
    title: "Password Recovered",
    parent: nil,
    flags: :modal,
    buttons: []
  )

  dialog.set_default_size(400, 250)

  content_box = Gtk::Box.new(:vertical, 12)
  content_box.border_width = 12

  # ====================================================================
  # Success Message
  # ====================================================================
  success_message = Gtk::Label.new
  success_message.markup = "<b>✓ Password Successfully Saved</b>\n\n" +
                           "Your master password has been restored to your system Keychain.\n" +
                           "You can now access your encrypted credentials."
  success_message.wrap = true
  success_message.justify = :center
  content_box.pack_start(success_message, expand: false)

  # Set content area
  dialog.child.add(content_box)
  dialog.show_all

  # ====================================================================
  # Button Setup with 1-second delay to prevent accidental clicks
  # ====================================================================
  continue_session = nil

  GLib::Timeout.add(1000) do
    # Add Continue and Close buttons
    continue_button = Gtk::Button.new(label: "Continue")
    close_button = Gtk::Button.new(label: "Close")

    dialog.action_area.pack_start(close_button, expand: false, fill: false, padding: 5)
    dialog.action_area.pack_start(continue_button, expand: false, fill: false, padding: 5)
    dialog.action_area.show_all

    # Set up button handlers
    continue_button.signal_connect('clicked') do
      continue_session = true
      dialog.destroy
    end

    close_button.signal_connect('clicked') do
      continue_session = false
      dialog.destroy
    end

    false # Don't repeat the timeout
  end

  # Wait for dialog to be destroyed by button click
  dialog.run

  { continue_session: continue_session }
end