Module: Lich::Common::CLI::PasswordManager

Defined in:
documented/common/cli/cli_password_manager.rb

Class Method Summary collapse

Class Method Details

.add_account(account, password, frontend = nil) ⇒ Integer

Adds a new account with the specified password.

Examples:

Adding a new account

result = Lich::Common::CLI::PasswordManager.("my_account", "my_password", "stormfront")

Parameters:

  • account (String)

    The name of the account to be added.

  • password (String)

    The password for the new account.

  • frontend (String, nil) (defaults to: nil)

    Optional frontend to associate with the account.

Returns:

  • (Integer)

    Returns 0 on success, 1 if master password is unavailable, or 2 if authentication fails.

Raises:

  • (StandardError)

    Raises an error if there is an issue during the process.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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
# File 'documented/common/cli/cli_password_manager.rb', line 105

def self.(, password, frontend = nil)
  # Validate master password availability before attempting add
  unless validate_master_password_available
    return 1
  end

  data_dir = DATA_DIR
  yaml_file = Lich::Common::GUI::YamlState.yaml_file_path(data_dir)

  begin
    # Check if account already exists
    if File.exist?(yaml_file)
      yaml_data = YAML.load_file(yaml_file)
      if yaml_data['accounts'] && yaml_data['accounts'][]
        puts "error: Account '#{}' already exists"
        puts "Use --change-account-password to update the password."
        Lich.log "error: CLI add account failed - account '#{}' already exists"
        return 1
      end
    end

    Lich.log "info: Adding account '#{}' via CLI"

    # Authenticate with game servers to fetch characters (like GUI does)
    puts "Authenticating with game servers..."
    Lich.log "info: Authenticating account '#{}' with game servers"
    auth_data = Lich::Common::GUI::Authentication.authenticate(
      account: ,
      password: password,
      legacy: true
    )

    unless auth_data && auth_data.is_a?(Array) && !auth_data.empty?
      puts "error: Authentication failed or no characters found"
      Lich.log "error: CLI add account failed - game server authentication failed for '#{}'"
      return 2
    end

    Lich.log "info: Authentication successful - found #{auth_data.length} character(s)"

    # Determine frontend
    selected_frontend = if frontend
                          # Frontend provided via --frontend flag
                          Lich.log "info: Using provided frontend: #{frontend}"
                          frontend
                        else
                          # Check predominant frontend in YAML, or prompt
                          predominant = determine_predominant_frontend(yaml_file)
                          if predominant
                            puts "Using predominant frontend: #{predominant}"
                            Lich.log "info: Using predominant frontend: #{predominant}"
                            predominant
                          else
                            # Prompt user
                            prompt_for_frontend
                          end
                        end

    # Convert authentication data to character list
    character_list = Lich::Common::GUI::AccountManager.convert_auth_data_to_characters(
      auth_data,
      selected_frontend || 'stormfront'
    )

    # Save account + characters using AccountManager
    if Lich::Common::GUI::AccountManager.(data_dir, , password, character_list)
      puts "success: Account '#{}' added with #{character_list.length} character(s)"
      Lich.log "info: Account '#{}' added successfully with #{character_list.length} character(s)"
      if selected_frontend.nil? || selected_frontend.empty?
        puts "note: Frontend not set - use GUI to configure or rerun with --frontend"
        Lich.log "warning: No frontend set for account '#{}'"
      end
      0
    else
      puts "error: Failed to save account"
      Lich.log "error: CLI add account failed - could not save account '#{}'"
      1
    end
  rescue StandardError => e
    # CRITICAL: Only log e.message, NEVER log password values
    puts "error: #{e.message}"
    Lich.log "error: CLI add account failed for '#{}': #{e.message}"
    1
  end
end

.change_account_password(account, new_password) ⇒ Integer

Changes the password for a specified account.

Examples:

Changing an account password

result = Lich::Common::CLI::PasswordManager.("my_account", "new_password")

Parameters:

  • account (String)

    The name of the account whose password is to be changed.

  • new_password (String)

    The new password for the account.

Returns:

  • (Integer)

    Returns 0 on success, 1 if master password is unavailable, 2 if account not found, or 1 on error.

Raises:

  • (StandardError)

    Raises an error if there is an issue during the process.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
86
87
88
89
90
91
92
93
94
95
# File 'documented/common/cli/cli_password_manager.rb', line 22

def self.(, new_password)
  # Validate master password availability before attempting change
  unless validate_master_password_available
    return 1
  end

  data_dir = DATA_DIR
  yaml_file = Lich::Common::GUI::YamlState.yaml_file_path(data_dir)

  unless File.exist?(yaml_file)
    puts "error: entry.yaml not found at #{yaml_file}"
    return 2
  end

  begin
    yaml_data = YAML.load_file(yaml_file)
    encryption_mode = (yaml_data['encryption_mode'] || 'plaintext').to_sym

    # Find account
    unless yaml_data['accounts'] && yaml_data['accounts'][]
      puts "error: Account '#{}' not found"
      Lich.log "error: CLI change password failed - account '#{}' not found"
      return 2
    end

    Lich.log "info: Changing password for account '#{}' (mode: #{encryption_mode})"

    # Encrypt password based on mode
    encrypted = case encryption_mode
                when :plaintext
                  new_password
                when :standard
                  Lich::Common::GUI::PasswordCipher.encrypt(
                    new_password,
                    mode: :standard,
                    account_name: 
                  )
                when :enhanced
                  master_password = Lich::Common::GUI::MasterPasswordManager.retrieve_master_password
                  if master_password.nil?
                    puts 'error: Enhanced mode requires master password in keychain'
                    Lich.log 'error: CLI change password failed - master password not in keychain'
                    return 1
                  end
                  Lich::Common::GUI::PasswordCipher.encrypt(
                    new_password,
                    mode: :enhanced,
                    account_name: ,
                    master_password: master_password
                  )
                else
                  puts "error: Unknown encryption mode: #{encryption_mode}"
                  Lich.log "error: CLI change password failed - unknown encryption mode: #{encryption_mode}"
                  return 1
                end

    # Update account password
    yaml_data['accounts'][]['password'] = encrypted

    # Save YAML
    File.open(yaml_file, 'w', 0o600) do |file|
      file.write(YAML.dump(yaml_data))
    end

    puts "success: Password changed for account '#{}'"
    Lich.log "info: Password changed successfully for account '#{}'"
    0
  rescue StandardError => e
    # CRITICAL: Only log e.message, NEVER log password values
    puts "error: #{e.message}"
    Lich.log "error: CLI change password failed for '#{}': #{e.message}"
    1
  end
end

.change_master_password(old_password, new_password = nil) ⇒ Integer

Changes the master password used for account encryption.

Examples:

Changing the master password

result = Lich::Common::CLI::PasswordManager.change_master_password("old_password", "new_password")

Parameters:

  • old_password (String)

    The current master password.

  • new_password (String, nil) (defaults to: nil)

    The new master password. If nil, prompts for input.

Returns:

  • (Integer)

    Returns 0 on success, 1 if validation fails, or 2 if the YAML file is not found.

Raises:

  • (StandardError)

    Raises an error if there is an issue during the process.



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
# File 'documented/common/cli/cli_password_manager.rb', line 198

def self.change_master_password(old_password, new_password = nil)
  data_dir = DATA_DIR
  yaml_file = Lich::Common::GUI::YamlState.yaml_file_path(data_dir)

  unless File.exist?(yaml_file)
    puts "error: entry.yaml not found at #{yaml_file}"
    return 2
  end

  begin
    yaml_data = YAML.load_file(yaml_file)
    encryption_mode = (yaml_data['encryption_mode'] || 'plaintext').to_sym

    unless encryption_mode == :enhanced
      puts "error: Master password only used in Enhanced encryption mode"
      puts "Current mode: #{encryption_mode}"
      Lich.log "error: CLI change master password failed - wrong encryption mode: #{encryption_mode}"
      return 3
    end

    Lich.log "info: Starting CLI master password change"

    # Validate old password
    validation_test = yaml_data['master_password_validation_test']
    unless Lich::Common::GUI::MasterPasswordManager.validate_master_password(old_password, validation_test)
      puts 'error: Current master password incorrect'
      Lich.log 'error: CLI change master password failed - incorrect current password'
      return 1
    end

    Lich.log "info: Current master password validated successfully"

    # Use provided password or prompt for new password
    if new_password.nil?
      print "Enter new master password: "
      input = $stdin.gets
      if input.nil?
        puts 'error: Unable to read password from STDIN / terminal'
        puts 'Please run this command interactively (not in a pipe or automated script without input)'
        Lich.log 'error: CLI change master password failed - stdin unavailable'
        return 1
      end
      new_password = input.strip

      print "Confirm new master password: "
      input = $stdin.gets
      if input.nil?
        puts 'error: Unable to read password from STDIN / terminal'
        puts 'Please run this command interactively (not in a pipe or automated script without input)'
        Lich.log 'error: CLI change master password failed - stdin unavailable'
        return 1
      end
      confirm_password = input.strip

      unless new_password == confirm_password
        puts "error: Passwords do not match"
        Lich.log "error: CLI change master password failed - password confirmation mismatch"
        return 1
      end
    end

    if new_password.length < 8
      puts "error: Password must be at least 8 characters"
      Lich.log "error: CLI change master password failed - password too short"
      return 1
    end

     = yaml_data['accounts'].length
    Lich.log "info: Re-encrypting #{} account(s) with new master password"

    # Re-encrypt all accounts
    yaml_data['accounts'].each do |_username, |
      # Decrypt with old password
      plaintext = Lich::Common::GUI::PasswordCipher.decrypt(
        ['password'],
        mode: :enhanced,
        master_password: old_password
      )

      # Encrypt with new password
      new_encrypted = Lich::Common::GUI::PasswordCipher.encrypt(
        plaintext,
        mode: :enhanced,
        master_password: new_password
      )

      ['password'] = new_encrypted
    end

    # Update validation test
    new_validation = Lich::Common::GUI::MasterPasswordManager.create_validation_test(new_password)
    yaml_data['master_password_validation_test'] = new_validation

    # Update keychain
    unless Lich::Common::GUI::MasterPasswordManager.store_master_password(new_password)
      puts 'error: Failed to update keychain'
      Lich.log 'error: CLI change master password failed - keychain update failed'
      return 1
    end

    # Save YAML
    File.open(yaml_file, 'w', 0o600) do |file|
      file.write(YAML.dump(yaml_data))
    end

    puts 'success: Master password changed'
    Lich.log 'info: Master password changed successfully via CLI'
    0
  rescue StandardError => e
    # CRITICAL: Only log e.message, NEVER log password values
    puts "error: #{e.message}"
    Lich.log "error: CLI change master password failed: #{e.message}"
    1
  end
end

.determine_predominant_frontend(yaml_file) ⇒ String?

Determines the most commonly used frontend from the accounts in the YAML file.

Examples:

Determining predominant frontend

frontend = Lich::Common::CLI::PasswordManager.determine_predominant_frontend("path/to/yaml_file")

Parameters:

  • yaml_file (String)

    The path to the YAML file containing account data.

Returns:

  • (String, nil)

    Returns the predominant frontend or nil if none found.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'documented/common/cli/cli_password_manager.rb', line 319

def self.determine_predominant_frontend(yaml_file)
  return nil unless File.exist?(yaml_file)

  yaml_data = YAML.load_file(yaml_file)
  return nil unless yaml_data['accounts']

  frontend_counts = Hash.new(0)
  yaml_data['accounts'].each do |_username, |
    next unless ['characters']

    ['characters'].each do |char|
      fe = char['frontend']
      frontend_counts[fe] += 1 if fe && !fe.empty?
    end
  end

  return nil if frontend_counts.empty?

  frontend_counts.max_by { |_fe, count| count }&.first
end

.get_master_password_from_keychain_or_promptString?

Retrieves the master password from the keychain or prompts the user to create one.

Examples:

Getting master password from keychain or prompting

master_password = Lich::Common::CLI::PasswordManager.get_master_password_from_keychain_or_prompt

Returns:

  • (String, nil)

    Returns the master password or nil if not available.



549
550
551
552
553
554
555
556
557
# File 'documented/common/cli/cli_password_manager.rb', line 549

def self.get_master_password_from_keychain_or_prompt
  # Check if password already exists in keychain
  existing = Lich::Common::GUI::MasterPasswordManager.retrieve_master_password
  return existing if existing

  # Not in keychain, prompt user to create one
  puts "Creating new master password for Enhanced encryption mode..."
  prompt_and_confirm_password("Enter new master password")
end

.prompt_and_confirm_password(prompt = "Enter password") ⇒ String?

Prompts the user to enter and confirm a password.

Examples:

Prompting and confirming a password

password = Lich::Common::CLI::PasswordManager.prompt_and_confirm_password("Enter your password")

Parameters:

  • prompt (String) (defaults to: "Enter password")

    The prompt message to display to the user.

Returns:

  • (String, nil)

    Returns the confirmed password or nil if there was an error.



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
532
533
534
535
536
537
538
539
540
541
542
543
# File 'documented/common/cli/cli_password_manager.rb', line 506

def self.prompt_and_confirm_password(prompt = "Enter password")
  print "#{prompt}: "
  input = $stdin.gets
  if input.nil?
    puts 'error: Unable to read password from STDIN / terminal'
    puts 'Please run this command interactively (not in a pipe or automated script without input)'
    Lich.log 'error: Password prompt failed - stdin unavailable'
    return nil
  end
  password = input.strip

  print "Confirm #{prompt.downcase}: "
  input = $stdin.gets
  if input.nil?
    puts 'error: Unable to read password from STDIN / terminal'
    puts 'Please run this command interactively (not in a pipe or automated script without input)'
    Lich.log 'error: Password confirmation failed - stdin unavailable'
    return nil
  end
  confirm_password = input.strip

  unless password == confirm_password
    puts "error: Passwords do not match"
    Lich.log "error: Password confirmation mismatch"
    return nil
  end

  if password.length < 8
    puts "error: Password must be at least 8 characters"
    Lich.log "error: Password too short (minimum 8 characters)"
    return nil
  end

  password
rescue StandardError => e
  Lich.log "error: Password prompt failed: #{e.message}"
  nil
end

.prompt_for_frontendString?

Prompts the user to select a frontend from a list of options.

Examples:

Prompting for frontend

selected_frontend = Lich::Common::CLI::PasswordManager.prompt_for_frontend

Returns:

  • (String, nil)

    Returns the selected frontend or nil if skipped.



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'documented/common/cli/cli_password_manager.rb', line 344

def self.prompt_for_frontend
  puts "\nSelect frontend (or press Enter to skip):"
  puts "  1. wizard"
  puts "  2. stormfront"
  puts "  3. avalon"
  print "Choice (1-3 or Enter): "

  input = $stdin.gets
  if input.nil?
    puts 'error: Unable to read input from STDIN / terminal'
    puts 'Please run this command interactively (not in a pipe or automated script without input)'
    return nil
  end
  choice = input.strip
  return nil if choice.empty?

  case choice
  when '1' then 'wizard'
  when '2' then 'stormfront'
  when '3' then 'avalon'
  else
    puts "Invalid choice, skipping frontend selection"
    nil
  end
end

.prompt_for_master_passwordString?

Prompts the user to enter the master password.

Examples:

Prompting for master password

master_password = Lich::Common::CLI::PasswordManager.prompt_for_master_password

Returns:

  • (String, nil)

    Returns the entered master password or nil if there was an error.



563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'documented/common/cli/cli_password_manager.rb', line 563

def self.prompt_for_master_password
  print "Enter master password: "
  input = $stdin.gets
  if input.nil?
    puts 'error: Unable to read password from STDIN / terminal'
    Lich.log 'error: Master password prompt failed - stdin unavailable'
    return nil
  end
  input.strip
rescue StandardError => e
  Lich.log "error: Master password prompt failed: #{e.message}"
  nil
end

.recover_master_password(master_password = nil) ⇒ Integer

Recovers the master password from the keychain or prompts the user for it.

Examples:

Recovering the master password

result = Lich::Common::CLI::PasswordManager.recover_master_password

Parameters:

  • master_password (String, nil) (defaults to: nil)

    Optional master password to validate against.

Returns:

  • (Integer)

    Returns 0 on success, 1 if validation fails, or 2 if the YAML file is not found.

Raises:

  • (StandardError)

    Raises an error if there is an issue during the process.



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
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
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
# File 'documented/common/cli/cli_password_manager.rb', line 425

def self.recover_master_password(master_password = nil)
  data_dir = DATA_DIR
  yaml_file = Lich::Common::GUI::YamlState.yaml_file_path(data_dir)

  unless File.exist?(yaml_file)
    puts "error: entry.yaml not found at #{yaml_file}"
    return 2
  end

  begin
    yaml_data = YAML.load_file(yaml_file)
    encryption_mode = (yaml_data['encryption_mode'] || 'plaintext').to_sym

    unless encryption_mode == :enhanced
      puts "error: Master password recovery only works in Enhanced encryption mode"
      puts "Current mode: #{encryption_mode}"
      Lich.log "error: CLI recover master password failed - wrong encryption mode: #{encryption_mode}"
      return 3
    end

    # Must have validation test to validate password
    validation_test = yaml_data['master_password_validation_test']
    unless validation_test
      puts "error: No validation test found - cannot recover master password"
      Lich.log "error: CLI recover master password failed - no validation test"
      return 1
    end

    Lich.log "info: Starting master password recovery"

    # Get password to validate
    if master_password.nil?
      print "Enter master password: "
      input = $stdin.gets
      if input.nil?
        puts 'error: Unable to read password from STDIN / terminal'
        puts 'Please run this command interactively (not in a pipe or automated script without input)'
        Lich.log 'error: CLI recover master password failed - stdin unavailable'
        return 1
      end
      master_password = input.strip
    end

    if master_password.length < 8
      puts "error: Password must be at least 8 characters"
      Lich.log "error: CLI recover master password failed - password too short"
      return 1
    end

    # Validate password against validation test
    unless Lich::Common::GUI::MasterPasswordManager.validate_master_password(master_password, validation_test)
      puts "error: Password validation failed"
      Lich.log "error: CLI recover master password failed - password validation failed"
      return 1
    end

    Lich.log "info: Password validated successfully"

    # Store validated password in keychain
    unless Lich::Common::GUI::MasterPasswordManager.store_master_password(master_password)
      puts 'error: Failed to store master password in keychain'
      Lich.log 'error: CLI recover master password failed - keychain storage failed'
      return 1
    end

    puts 'success: Master password recovered and restored to keychain'
    Lich.log 'info: Master password recovered successfully via CLI'
    0
  rescue StandardError => e
    # CRITICAL: Only log e.message, NEVER log password values
    puts "error: #{e.message}"
    Lich.log "error: CLI recover master password failed: #{e.message}"
    1
  end
end

.validate_master_password_availableBoolean

Validates if the master password is available for use.

Examples:

Validating master password availability

is_available = Lich::Common::CLI::PasswordManager.validate_master_password_available

Returns:

  • (Boolean)

    Returns true if the master password is available, false otherwise.



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
# File 'documented/common/cli/cli_password_manager.rb', line 374

def self.validate_master_password_available
  data_dir = DATA_DIR
  yaml_file = Lich::Common::GUI::YamlState.yaml_file_path(data_dir)

  unless File.exist?(yaml_file)
    puts "error: entry.yaml not found"
    return false
  end

  begin
    yaml_data = YAML.load_file(yaml_file)
    encryption_mode = (yaml_data['encryption_mode'] || 'plaintext').to_sym

    # Non-enhanced modes don't need master password
    return true unless encryption_mode == :enhanced

    # Check if validation test exists (indicator of Enhanced mode setup)
    unless yaml_data['master_password_validation_test']
      puts "error: No validation test found in entry.yaml"
      puts "Master password recovery may be needed"
      return false
    end

    # Check if keychain is available and has the password
    unless Lich::Common::GUI::MasterPasswordManager.keychain_available?
      puts "error: Keychain not available on this system"
      return false
    end

    master_password = Lich::Common::GUI::MasterPasswordManager.retrieve_master_password
    if master_password.nil? || master_password.empty?
      puts "error: Master password not found in keychain"
      puts "Use: lich --recover-master-password"
      puts "     to restore the master password from your accounts"
      Lich.log "info: Master password validation failed - keychain missing, user can recover"
      return false
    end

    true
  rescue StandardError => e
    Lich.log "error: Master password validation failed: #{e.message}"
    false
  end
end