Module: Pwss::CommandSemantics

Defined in:
lib/pwss/cli/command_semantics.rb

Overview

what we are supposed to do with each command

Constant Summary collapse

VERSION =
Pwss::VERSION
MAN =

TODO: make the list of entries read from code

<<EOS
NAME
  pwss -- A command-line password manager

SYNOPSYS
  pwss command [options] [args]

DESCRIPTION
  PWSS is a password manager, in the spirit of pass and pws.

  Features:

  * PWSS manages password *files*:
    - A password file can store different entries (password and other
      sensitive information)
    - The user can manage different password files (e.g., work, personal)

  * Entries in a password file can be of different types.  Each type stores
    different information.  Use the 'describe' command for more info about 
    the available types and their fields.

  * Password files can be encrypted

  * Encrypted password files can be decrypted, for instance, to batch process
    entries, to migrate to another tool, or to manually edit entries

  * Entries are human-readable (and editable), when the password file is not
    encrypted

  * A console allows to decrypt a file once and perform multiple queries

EXAMPLES
  pwss help                             # get syntax of each command

  # scenario
  pwss init -f a.enc                    # generate an encrypted safe a.enc
  pwss add -f a.enc                     # add an entry (pwss will generate a random 16-char password)
  pwss get -f a.enc my secret account   # find an entry
  pwss console -f a.enc                 # decrypt a.enc and enter the pwss console to operate on a.enc

VERSION
  This is version #{VERSION}

LICENSE
  MIT

SEE ALSO
  pwss man
  pwss help
  https://github.com/avillafiorita/pwss
EOS
DEFAULT_BASENAME =

the default filename YOU SHOULDN’T BE USING THESE CONTANSTS. USE ‘default_filename` INSTEAD

File.join(Dir.home, ".pwss.yaml")
DEFAULT_FILENAME =
DEFAULT_BASENAME + ".gpg"
@@cache =

cache is the content of the file last operated on is it used

nil

Class Method Summary collapse

Class Method Details

.add_entry(opts, argv) ⇒ Object



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
# File 'lib/pwss/cli/command_semantics.rb', line 196

def self.add_entry opts, argv
  waiting  = opts[:wait]
  type     = opts[:type] || "entry"
  strategy = opts[:ask] ? "ask" : (opts[:method] || "random")
  length   = opts[:length]

  safe = use_safe opts[:filename]

  # the title can be specified in the argument
  arguments = Hash.new
  arguments["title"] = argv.join(" ") if argv != []
  arguments[:strategy] = strategy
  arguments[:length] = length

  new_entry = eval("Pwss::" + type.capitalize).new
  new_entry.ask arguments

  puts "Adding entry '#{new_entry.entry["title"]}' of type '#{type}' to #{safe.filename}"
  safe.add new_entry.entry
  safe.save
  puts "Entry added"
  
  # make password available in the clipboard, if there is a password to make available
  if new_entry.entry["password"] 
    Pwss::Password.to_clipboard "password", new_entry.entry["password"], waiting
  end
end

.all_safesObject

return all the default safes we look for



100
101
102
# File 'lib/pwss/cli/command_semantics.rb', line 100

def self.all_safes
  [".enc", ".gpg", ""].map { |ext| DEFAULT_BASENAME + ext }
end

.ambiguous_defaultObject

return true if the default basename appears with different extensions.

for instance: if the DEFAULT_BASENAME appears both with .gpg and .enc (or plain and encrypted).

This is potentially a problem, since all operations are performed on a different file from the one the user believes it is operating on.



90
91
92
# File 'lib/pwss/cli/command_semantics.rb', line 90

def self.ambiguous_default
  [".enc", ".gpg", ""].map { |ext| File.exist?(DEFAULT_BASENAME + ext) }.count(true) > 1
end

.console(opts, argv = []) ⇒ Object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/pwss/cli/command_semantics.rb', line 317

def self.console opts, argv = []
  all_commands = Pwss::CommandSyntax.commands
  all_commands.delete(:console)
  open opts, argv # open and cache the file
  
  i = 0
  while true
    string = Readline.readline('pwss:%03d> ' % i, true)
    string.gsub!(/^pwss /, "") # as a courtesy, remove any leading pwss string
    if string == "exit" or string == "quit" or string == "." then
      exit 0
    end
    reps all_commands, string.split(' ')
    i = i + 1
  end
end

.decrypt(opts, argv = []) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/pwss/cli/command_semantics.rb', line 297

def self.decrypt opts, argv = []
  # filename: passed from options, cached one or, in order, .gpg, .enc, plain (but plain will fail)
  filename = opts[:filename] || (@@cache ? @@cache.filename : default_filename)

  if not File.exist?(filename)
    raise "Error: file #{filename} does not exist."
  end

  if not Pwss::FileOps.encrypted? filename
    raise "Error: #{filename} does not end with '.gpg' or '.enc' (and I assume it to be in plain text)"
  end

  safe = use_safe filename
  safe.toggle_encryption
  safe.save
  puts "A plain text copy now lives in #{safe.filename}"
  puts "You might want to check everything is ok and delete the plain file: #{filename}"
  puts "If you do nothing, the next pwss command will run on #{default_filename}"
end

.default(opts, argv = []) ⇒ Object



340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/pwss/cli/command_semantics.rb', line 340

def self.default opts, argv = []
  if @@cache
    @@cache.filename
  elsif self.no_default
    puts "No default password file found."
    puts "Use -f if you have a password file stored somewhere else."
    puts "pwss init will create #{default_filename}."
  elsif self.ambiguous_default
    puts "Operating on #{default_filename}."
    puts "Warning: #{existing_safes.join(", ")} exist."
  else
    puts "Operating on #{default_filename}"
  end
end

.default_filenameObject

return the default filename

this is obtained by looking for plain text or encryped versions of the DEFAULT_BASENAME, with the following priority: .enc, .gpg, plain text.

If no file is found (like it might be the case when running the init command), use GPG



73
74
75
76
77
78
79
# File 'lib/pwss/cli/command_semantics.rb', line 73

def self.default_filename
  [".enc", ".gpg", ""].each do |ext|
    filename = DEFAULT_BASENAME + ext
    return filename if File.exist?(filename)
  end
  return DEFAULT_FILENAME
end

.describe(opts = nil, argv = []) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/pwss/cli/command_semantics.rb', line 137

def self.describe  opts = nil, argv = []
  if opts[:type]
    types = [("Pwss::" + opts[:type].capitalize).to_sym]
  else
    types = [Pwss::Entry] + ObjectSpace.each_object(Class).select { |klass| klass < Pwss::Entry }
  end
  types.each do |type|
    t = eval("#{type}.new")
    puts "#{type.to_s.gsub("Pwss::", "").downcase}:\n  #{t.fields.join(", ")}\n\n"
  end
end

.destroy(opts, argv) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
# File 'lib/pwss/cli/command_semantics.rb', line 255

def self.destroy opts, argv
  safe = use_safe opts[:filename]

  string = argv.join(" ")
  entries_with_idx = safe.match string
  id = Pwss::Safe.choose_entry entries_with_idx, true    
  if id != -1 then
    safe.destroy id
    safe.save
  end
end

.encrypt(opts, argv = []) ⇒ Object



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
# File 'lib/pwss/cli/command_semantics.rb', line 267

def self.encrypt opts, argv = []
  # filename: use the one passed from the cli or the cached one or .pwss.yaml DEFAULT_*BASE*NAME
  filename = opts[:filename] || (@@cache ? @@cache.filename : DEFAULT_BASENAME)
  encryption = opts[:symmetric] ? :enc : :gpg

  if not File.exist?(filename)
    raise "Error: file #{filename} does not exist."
  end

  if Pwss::FileOps.encrypted? filename
    raise "Error: #{filename} ends with '.gpg' or '.enc' (and I assume these files to be encrypted)"
  end

  if encryption == :enc then
    password = Pwss::Password.ask_password_twice
    if password == "" then
      raise "Error: Please specify a non-empty password."
    end  
  else
    password = nil # it will be asked by GPG
  end
  
  safe = use_safe filename
  safe.toggle_encryption :password => password, :schema => encryption
  safe.save
  puts "An encrypted copy now lives in #{safe.filename}"
  puts "You might want to check everything is ok and delete the plain file: #{filename}"
  puts "If you do nothing, the next pwss command will run on #{default_filename}"
end

.existing_safesObject

return the existing safes



105
106
107
# File 'lib/pwss/cli/command_semantics.rb', line 105

def self.existing_safes
  [".enc", ".gpg", ""].map { |ext| DEFAULT_BASENAME + ext }.select { |x| File.exist?(x) }
end

.get(opts, argv) ⇒ Object



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
# File 'lib/pwss/cli/command_semantics.rb', line 170

def self.get opts, argv
  waiting = opts[:wait]
  stdout_opt = opts[:stdout]
  field_name = opts[:field] || "password"
  id = opts[:id]

  safe = use_safe opts[:filename]
  show = opts[:show]

  if not id 
    string = argv.join(" ")
    entries_with_idx = safe.match string
    id = Pwss::Safe.choose_entry entries_with_idx
  end

  if id != -1 and safe.get(id) then
    field_value = safe.get_field id, field_name
    if stdout_opt then
      printf("%s", field_value)
    else
      puts (show ? safe.get(id).to_yaml : safe.get_pruned(id).to_yaml )
      Pwss::Password.to_clipboard(field_name, field_value, waiting)
    end
  end
end

.help(opts = nil, argv = []) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/pwss/cli/command_semantics.rb', line 121

def self.help opts = nil, argv = []
  all_commands = Pwss::CommandSyntax.commands
  
  if argv != []
    argv.map { |x| puts all_commands[x.to_sym][0] }
  else
    puts "pwss command [options] [args]"
    puts ""
    puts "Available commands:"
    puts ""
    all_commands.keys.each do |key|
      puts "  " + all_commands[key][0].banner
    end
  end
end

.init(opts, argv = []) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/pwss/cli/command_semantics.rb', line 149

def self.init opts, argv = []
  filename = opts[:filename] || @@cache.filename || DEFAULT_FILENAME

  if File.exist?(filename)
    raise "Error: file #{filename} already exists."
  end

  safe = Pwss::Safe.new filename
  safe.save
  
  puts "New safe created in #{filename}"
end

.list(opts, argv = []) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/pwss/cli/command_semantics.rb', line 162

def self.list opts, argv = []
  safe = use_safe opts[:filename]
  clean = opts[:clean]

  cleaned_entries = safe.prune(["created_at", "updated_at"]).map { |x| Pwss::Fields.to_clean_hash x }
  puts cleaned_entries.to_yaml
end

.man(opts = nil, argv = []) ⇒ Object



117
118
119
# File 'lib/pwss/cli/command_semantics.rb', line 117

def self.man opts = nil, argv = []
  puts MAN
end

.no_defaultObject

return true if none of the default files exist



95
96
97
# File 'lib/pwss/cli/command_semantics.rb', line 95

def self.no_default
  [".enc", ".gpg", ""].map { |ext| File.exist?(DEFAULT_BASENAME + ext) }.count(true) == 0
end

.open(opts, argv = []) ⇒ Object



334
335
336
337
338
# File 'lib/pwss/cli/command_semantics.rb', line 334

def self.open opts, argv = []
  filename = opts[:filename] || default_filename
  @@cache = load_safe filename
  puts "Loaded #{filename}"
end

.reps(all_commands, argv) ⇒ Object

read-eval-print step



356
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
# File 'lib/pwss/cli/command_semantics.rb', line 356

def self.reps all_commands, argv
  if argv == []
    Pwss::CommandSemantics.help
    exit 0
  else
    command = argv[0]
    syntax_and_semantics = all_commands[command.to_sym]

    if syntax_and_semantics
      opts = syntax_and_semantics[0]
      function = syntax_and_semantics[1]
      
      begin
        parser = Slop::Parser.new(opts)
        result = parser.parse(argv[1..-1])
        options = result.to_hash
        arguments = result.arguments

        eval "Pwss::CommandSemantics::#{function}(options, arguments)"
      rescue Slop::Error => e
        puts "pwss: #{e}"
      rescue Exception => e
        puts e
      end
    else
      puts "pwss: '#{command}' is not a pwss command. See 'pwss help'"
    end
  end
end

.update(opts, argv) ⇒ Object



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
# File 'lib/pwss/cli/command_semantics.rb', line 224

def self.update opts, argv
  field    = (opts.to_hash[:password] or opts.to_hash[:method] or opts.to_hash[:ask]) ? "password" : opts.to_hash[:field]
  strategy = opts.to_hash[:ask] ? "ask" : (opts.to_hash[:method] || "random")
  length   = opts.to_hash[:length]
  waiting  = opts.to_hash[:wait]
  string   = argv.join(" ") # the entry we are looking for

  if not field then
    raise "Error: please specify a field to update (use --field, -p, or --ask)"
  end

  safe = use_safe opts[:filename]

  entries_with_idx = safe.match string
  id = Pwss::Safe.choose_entry entries_with_idx, true
  if id != -1 then
    old_value = safe.get_field id, field
    new_value = Pwss::Fields.ask field, { strategy: strategy, length: length }
    printf "Updating #{field} field of '#{safe.entries[id]["title"]}' in #{safe.filename} ..."
    safe.update id, field, new_value
    safe.save
    puts "... done"
    puts "The old value of #{field} is: #{old_value}"
    
    # make the field available in the clipboard, just in case it is needed
    if field == "password"
      Pwss::Password.to_clipboard "password", new_value, waiting
    end
  end
end

.version(opts = nil, argv = []) ⇒ Object



113
114
115
# File 'lib/pwss/cli/command_semantics.rb', line 113

def self.version opts = nil, argv = []
  puts "pwss version #{VERSION}"
end