Module: Lich::Common::CredentialScrub

Defined in:
documented/common/credential_scrub.rb

Overview

Redacts startup credentials once the login sequence no longer needs them.

Lich keeps startup credentials where a running script can read them: @launch_data and @argv_options are instance variables on the top-level main object, and ARGV is a global constant. All three outlive login, so both the eaccess key and the account password stay readable for the rest of the session. Both are live credentials: the key is not single-use and stays valid for some period after issue, so a key read hours into a session may still be usable.

Values are overwritten in place rather than reassigned because other objects hold references to the same Array, Hash, and String instances (the login GUI keeps the launch data array; the argv option pipeline threads one options hash through three stages). Replacing a container would leave those references pointing at unscrubbed copies.

Scope: this closes script-readable and log-readable paths. It does not guarantee the bytes are gone from process memory. Earlier copies may survive in socket write buffers, in a spawned frontend's argv, or in unreclaimed heap.

Constant Summary collapse

REDACTED =

Replacement text for a redacted value.

'[scrubbed]'
LAUNCH_DATA_SECRET =

launch_data entry holding the eaccess key.

/\AKEY=/i
ARGV_SECRET =

argv entries of the form --flag=secret. Deliberately excludes --account=, which is an account identifier rather than a credential.

/\A(--(?:password|master-password)=)(.+)\z/i
OPTION_SECRET_KEYS =

argv_options keys holding reusable credentials.

[:password].freeze

Class Method Summary collapse

Class Method Details

.redact_argv(argv) ⇒ Array<String>

Returns a copy of argv with secret values masked, without touching the input. Use this where the real argv must still be used for its original purpose (e.g. exec) but a redacted form is needed for something incidental to that purpose, such as a log line.

Unlike scrub_argv!, this never mutates: reconnect logs its exec argv while also passing the same, unredacted array to exec, so an in-place scrub here would strip the real password before the process replacement that needs it.

Parameters:

  • argv (Array<String>, nil)

    argument vector to redact

Returns:

  • (Array<String>)

    a new array; entries that don't match ARGV_SECRET are unchanged. Returns an empty array for nil or non-Array input.



102
103
104
105
106
107
108
109
110
111
# File 'documented/common/credential_scrub.rb', line 102

def redact_argv(argv)
  return [] unless argv.is_a?(Array)

  argv.map do |arg|
    next arg unless arg.is_a?(String)

    match = ARGV_SECRET.match(arg)
    match.nil? ? arg : "#{match[1]}#{REDACTED}"
  end
end

.scrub_argv!(argv) ⇒ Integer

Overwrites secret-bearing argv values in place, preserving the flag prefix so later ARGV.include? and ARGV.find checks behave unchanged.

Parameters:

  • argv (Array<String>, nil)

    argument vector, normally ARGV

Returns:

  • (Integer)

    number of entries rewritten



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'documented/common/credential_scrub.rb', line 73

def scrub_argv!(argv)
  return 0 unless argv.is_a?(Array)

  scrubbed = 0
  argv.each_with_index do |arg, index|
    next unless arg.is_a?(String)

    match = ARGV_SECRET.match(arg)
    next if match.nil?

    scrubbed += 1 if overwrite(argv, index, "#{match[1]}#{REDACTED}")
  end
  scrubbed
end

.scrub_launch_data!(launch_data) ⇒ Integer

Overwrites the KEY= entry of a launch data array in place.

Parameters:

  • launch_data (Array<String>, nil)

    launch lines; nil is tolerated because direct host/port and --pipe startups never build one

Returns:

  • (Integer)

    number of entries rewritten



56
57
58
59
60
61
62
63
64
65
66
# File 'documented/common/credential_scrub.rb', line 56

def scrub_launch_data!(launch_data)
  return 0 unless launch_data.is_a?(Array)

  scrubbed = 0
  launch_data.each_with_index do |line, index|
    next unless line.is_a?(String) && line.match?(LAUNCH_DATA_SECRET)

    scrubbed += 1 if overwrite(launch_data, index, "KEY=#{REDACTED}")
  end
  scrubbed
end

.scrub_options!(options) ⇒ Array<Symbol>

Overwrites secret values in an options hash in place.

Parameters:

  • options (Hash, nil)

    the argv options hash

Returns:

  • (Array<Symbol>)

    keys that were rewritten



117
118
119
120
121
122
123
124
125
126
# File 'documented/common/credential_scrub.rb', line 117

def scrub_options!(options)
  return [] unless options.is_a?(Hash)

  OPTION_SECRET_KEYS.select do |key|
    value = options[key]
    next false unless value.is_a?(String)

    overwrite(options, key, REDACTED)
  end
end

.shred_file(path, attempts: 3) ⇒ Boolean

Overwrites a file's contents and removes it. Never raises.

Overwrite-then-delete is best effort: copy-on-write, journaling, and flash translation layers may retain the original blocks. The retry loop exists because Windows refuses deletion while a spawned frontend still holds the file open.

Parameters:

  • path (String, nil)

    file to remove

  • attempts (Integer) (defaults to: 3)

    delete attempts before giving up

Returns:

  • (Boolean)

    true when the file is gone



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'documented/common/credential_scrub.rb', line 138

def shred_file(path, attempts: 3)
  path = path.to_s
  return false if path.empty?

  overwrite_contents(path)
  attempts.times do |attempt|
    begin
      File.delete(path) if File.exist?(path)
      return true unless File.exist?(path)
    rescue StandardError
      nil
    end
    sleep 0.05 unless attempt == attempts - 1
  end
  !File.exist?(path)
rescue StandardError
  false
end