pwss 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +9 -18
- data/.travis.yml +4 -0
- data/LICENSE.txt +17 -18
- data/README.md +454 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/pwss +6 -0
- data/lib/pwss.rb +17 -127
- data/lib/pwss/cipher.rb +11 -74
- data/lib/pwss/cli/command_semantics.rb +402 -0
- data/lib/pwss/cli/command_syntax.rb +156 -0
- data/lib/pwss/fileops.rb +31 -22
- data/lib/pwss/generators/bank_account.rb +18 -0
- data/lib/pwss/generators/code.rb +13 -0
- data/lib/pwss/generators/credit_card.rb +23 -0
- data/lib/pwss/generators/entry.rb +36 -0
- data/lib/pwss/generators/fields.rb +119 -0
- data/lib/pwss/generators/sim.rb +15 -0
- data/lib/pwss/generators/software_license.rb +19 -0
- data/lib/pwss/password.rb +94 -0
- data/lib/pwss/safe.rb +183 -0
- data/lib/pwss/version.rb +1 -1
- data/pwss.gemspec +19 -13
- metadata +85 -25
- data/README.textile +0 -190
- data/bin/pwss +0 -445
- data/lib/pwss/bank_account.rb +0 -18
- data/lib/pwss/credit_card.rb +0 -23
- data/lib/pwss/entry.rb +0 -69
- data/lib/pwss/software_license.rb +0 -21
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'slop'
|
2
|
+
require 'pwss/password'
|
3
|
+
|
4
|
+
module Pwss
|
5
|
+
module CommandSyntax
|
6
|
+
# return a hash with all the commands and their options
|
7
|
+
def self.commands
|
8
|
+
h = Hash.new
|
9
|
+
self.methods.each do |method|
|
10
|
+
if method.to_s.include?("_opts") then
|
11
|
+
h = h.merge(eval(method.to_s))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
return h
|
15
|
+
end
|
16
|
+
|
17
|
+
# the default number of seconds password is available in the clipboard
|
18
|
+
DEFAULT_WAIT = 45
|
19
|
+
# the default password length
|
20
|
+
DEFAULT_LENGTH = Pwss::Password::DEFAULT_PASSWORD_LENGTH
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.version_opts
|
25
|
+
opts = Slop::Options.new
|
26
|
+
opts.banner = "version -- print version information"
|
27
|
+
return { :version => [opts, :version] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.init_opts
|
31
|
+
opts = Slop::Options.new
|
32
|
+
opts.banner = "init [options] -- init a new password file"
|
33
|
+
opts.string "-f", "--filename", "Password file to create. Use '.enc' or '.gpg' for encryption"
|
34
|
+
return { :init => [opts, :init] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.list_opts
|
38
|
+
opts = Slop::Options.new
|
39
|
+
opts.banner = "list [options] -- list all entries in a file"
|
40
|
+
opts.string "-f", "--filename", "Password file to use"
|
41
|
+
opts.bool "-c", "--clean", "Clean timestamps from entries"
|
42
|
+
return { :list => [opts, :list] }
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.get_opts
|
46
|
+
opts = Slop::Options.new
|
47
|
+
opts.banner = "get [options] -- get a stored field of a record (it defaults to password)"
|
48
|
+
|
49
|
+
opts.string "-f", "--filename", "Password file to use"
|
50
|
+
opts.bool "--stdout", "Output the password to standard output"
|
51
|
+
opts.bool "-h", "--hide", "Hide sensitive fields"
|
52
|
+
opts.integer "-w", "--wait", "Number of seconds the field is available in the clipboard (0 = wait for user input)", default: DEFAULT_WAIT
|
53
|
+
opts.string "--field", "Field to make available on stdout or clipboard (password by default)"
|
54
|
+
return { :get => [opts, :get] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.add_and_new_opts
|
58
|
+
opts = Slop::Options.new
|
59
|
+
opts.banner = "add|new [options] [entry title] -- add an entry and copy its password in the clipboard"
|
60
|
+
opts.string "-f", "--filename", "Password file to use"
|
61
|
+
opts.integer "-w", "--wait", "Seconds password is available in the clipboard (0 = interactive)", default: DEFAULT_WAIT
|
62
|
+
opts.string "-t", "--type", "Create an entry of type TYPE (Entry, CreditCard, BankAccount, SoftwareLicense, Sim).\n Default to 'Entry', which is good enough for websites credentials"
|
63
|
+
opts.string "-m", "--method", "Method to generate the password (one of: random, alpha, ask; default to random)"
|
64
|
+
opts.bool "--ask", "A shortcut for --method ask"
|
65
|
+
opts.integer "-l", "--length", "Password length (when random or alpha; default #{DEFAULT_LENGTH})", default: DEFAULT_LENGTH
|
66
|
+
return { :add => [opts, :add_entry],
|
67
|
+
:new => [opts, :add_entry] }
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.update_opts
|
71
|
+
opts = Slop::Options.new
|
72
|
+
opts.banner = "update [options] string -- Update specified field of (user selected) entry matching <string>"
|
73
|
+
opts.string "-f", "--filename", "Password file to use"
|
74
|
+
opts.string "--field", "Field to update"
|
75
|
+
opts.bool "-p", "--password", "an alias for --field password"
|
76
|
+
opts.string "-m", "--method", "Method to generate the password (one of: random, alpha, ask; default to random)"
|
77
|
+
opts.bool "--ask", "A shortcut for [--field password] --method ask"
|
78
|
+
opts.integer "-l", "--length", "Password length (when random or alpha; default #{DEFAULT_LENGTH})", default: DEFAULT_LENGTH
|
79
|
+
opts.integer "-w", "--wait", "Seconds new field is available in the clipboard for (0 = interactive)", default: DEFAULT_WAIT
|
80
|
+
return { :update => [opts, :update] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.destroy_opts
|
84
|
+
opts = Slop::Options.new
|
85
|
+
opts.banner = "destroy|rm [options] string -- Destroy a user-selected entry matching <string>, after user confirmation"
|
86
|
+
opts.string "-f", "--filename", "Password file to create. Use extension '.enc' to encrypt it"
|
87
|
+
return { :destroy => [opts, :destroy],
|
88
|
+
:rm => [opts, :destroy] }
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.encrypt_opts
|
92
|
+
opts = Slop::Options.new
|
93
|
+
opts.banner = "encrypt [options] -- Encrypt a password file"
|
94
|
+
opts.bool "--symmetric", "Use symmetric encryption"
|
95
|
+
opts.bool "--gpg", "Use gpg (default: no need to specify it)"
|
96
|
+
opts.string "-f", "--filename", "Password file to encrypt. Write to <file>.[enc,gpg]"
|
97
|
+
return { :encrypt => [opts, :encrypt] }
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.decrypt_opts
|
101
|
+
opts = Slop::Options.new
|
102
|
+
opts.banner = "decrypt [options] -- Decrypt a password file"
|
103
|
+
opts.string "-f", "--filename", "Password file to encrypt. Write to <file>.enc"
|
104
|
+
return { :decrypt => [opts, :decrypt] }
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.console_opts
|
108
|
+
opts = Slop::Options.new
|
109
|
+
opts.banner = "console [options] -- Enter the console"
|
110
|
+
opts.string "-f", "--filename", "Password file to encrypt. Write to <file>.enc"
|
111
|
+
return { :console => [opts, :console] }
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.man_opts
|
115
|
+
opts = Slop::Options.new
|
116
|
+
opts.banner = "man -- print a manual page"
|
117
|
+
return { :man => [opts, :man] }
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.help_opts
|
121
|
+
opts = Slop::Options.new
|
122
|
+
opts.banner = "help [command] -- print usage string"
|
123
|
+
return { :help => [opts, :help] }
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.describe_opts
|
127
|
+
opts = Slop::Options.new
|
128
|
+
opts.banner = "describe [options] -- describe fields of an entry type or all types"
|
129
|
+
opts.string "-t", "--type", "Type to describe"
|
130
|
+
return { :describe => [opts, :describe] }
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# COMMANDS WHICH MAKE SENSE ONLY WITH THE CONSOLE
|
135
|
+
#
|
136
|
+
|
137
|
+
# change the default file
|
138
|
+
def self.open_opts
|
139
|
+
opts = Slop::Options.new
|
140
|
+
opts.banner = "open [options] -- change the default file used in the console"
|
141
|
+
opts.banner = " (makes sense only when launched from the console) "
|
142
|
+
opts.string "-f", "--filename", "Password file to use"
|
143
|
+
return { :open => [opts, :open] }
|
144
|
+
end
|
145
|
+
|
146
|
+
# which is the default file?
|
147
|
+
def self.default_opts
|
148
|
+
opts = Slop::Options.new
|
149
|
+
opts.banner = "default -- which file is the console currently operating on?"
|
150
|
+
opts.banner = " (makes sense only when launched from the console) "
|
151
|
+
return { :default => [opts, :default] }
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
data/lib/pwss/fileops.rb
CHANGED
@@ -1,33 +1,42 @@
|
|
1
1
|
require 'fileutils'
|
2
|
+
require 'gpgme'
|
2
3
|
|
3
4
|
#
|
4
5
|
# From file to string and back
|
5
6
|
# There is no lower level than this
|
6
7
|
#
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
module Pwss
|
9
|
+
module FileOps
|
10
|
+
# load a file into a string
|
11
|
+
def self.load filename
|
12
|
+
file = File.open(filename, "rb")
|
13
|
+
file.read
|
14
|
+
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
16
|
+
# save a string to a file
|
17
|
+
def self.save filename, data
|
18
|
+
file = File.open(filename, "wb")
|
19
|
+
file.write data
|
20
|
+
file.close
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
# check if the extension is ".enc"
|
24
|
+
def self.encrypted? filename
|
25
|
+
gpg? filename or symmetric? filename
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
def self.symmetric? filename
|
29
|
+
File.extname(filename) == ".enc"
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.gpg? filename
|
33
|
+
File.extname(filename) == ".gpg"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.backup filename
|
37
|
+
FileUtils::cp filename, filename + "~"
|
38
|
+
puts "Backup copy of password safe created in #{filename}~."
|
39
|
+
end
|
40
|
+
|
30
41
|
end
|
31
|
-
|
32
42
|
end
|
33
|
-
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'pwss/generators/entry'
|
2
|
+
|
3
|
+
module Pwss
|
4
|
+
class CreditCard < Entry
|
5
|
+
def initialize
|
6
|
+
super
|
7
|
+
@fields = [
|
8
|
+
"title",
|
9
|
+
"issuer",
|
10
|
+
"name_on_card",
|
11
|
+
"card_number",
|
12
|
+
"valid_from",
|
13
|
+
"valid_till",
|
14
|
+
"verification_number",
|
15
|
+
"pin",
|
16
|
+
"url",
|
17
|
+
"username",
|
18
|
+
"password",
|
19
|
+
"description"
|
20
|
+
]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Pwss
|
2
|
+
#
|
3
|
+
# Entry generates an entry for the password safe
|
4
|
+
# It is a wrapper to a Hash
|
5
|
+
#
|
6
|
+
class Entry
|
7
|
+
attr_reader :entry
|
8
|
+
attr_reader :fields
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@entry = Hash.new
|
12
|
+
@fields = [
|
13
|
+
"title",
|
14
|
+
"url",
|
15
|
+
"username",
|
16
|
+
"password",
|
17
|
+
"recovery_email",
|
18
|
+
"description"
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
# interactively ask the fields specified in +@fields+
|
23
|
+
#
|
24
|
+
# optional hash +arguments+ allows to pass arguments to the
|
25
|
+
# input-asking functions (including the default value for a key)
|
26
|
+
# See the documentation of Pwss::Fields::ask for more details.
|
27
|
+
#
|
28
|
+
def ask arguments = {}
|
29
|
+
@entry = Hash.new
|
30
|
+
@fields.each do |key|
|
31
|
+
@entry[key] = Fields.ask key, arguments
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'readline'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
require 'pwss/password'
|
5
|
+
|
6
|
+
module Pwss
|
7
|
+
module Fields
|
8
|
+
INPUT_F = 0
|
9
|
+
DEFAULT = 1
|
10
|
+
HIDDEN = 2
|
11
|
+
|
12
|
+
# this is a set of fields useful for different types of entries
|
13
|
+
# each entry will reference the symbols it needs to make sense
|
14
|
+
FIELDS = {
|
15
|
+
# everyone has...
|
16
|
+
"title" => ["Readline.readline('title: ')", "''", false],
|
17
|
+
"url" => ["Readline.readline('url: ')", "''", false],
|
18
|
+
"username" => ["Readline.readline('username: ')", "''", false],
|
19
|
+
"recovery_email" => ["Readline.readline('email: ')", "''", false],
|
20
|
+
"password" => ["Pwss::Password.password(arguments)", "Pwss::Password.password", true],
|
21
|
+
"description" => ["get_lines", "''", false],
|
22
|
+
|
23
|
+
# banks also have ...
|
24
|
+
"name" => ["Readline.readline('name: ')", "''", false],
|
25
|
+
"iban" => ["Readline.readline('iban: ')", "ITkk xaaa aabb bbbc cccc cccc ccc", false],
|
26
|
+
|
27
|
+
# cards also have
|
28
|
+
"issuer" => ["Readline.readline('issuer: ')", "''", false],
|
29
|
+
"name_on_card" => ["Readline.readline('name on card: ')", "''", false],
|
30
|
+
"card_number" => ["Readline.readline('number: ')", "''", true],
|
31
|
+
"valid_from" => ["Readline.readline('valid from: ')", "''", false],
|
32
|
+
"valid_till" => ["Readline.readline('valid till: ')", "''", false],
|
33
|
+
"verification_number" => ["Readline.readline('verification number: ')", "''", true],
|
34
|
+
"pin" => ["Readline.readline('pin: ')", "''", true],
|
35
|
+
|
36
|
+
# SIMs also have
|
37
|
+
"puk" => ["Readline.readline('puk: ')", "XXXX", true],
|
38
|
+
"phone" => ["Readline.readline('phone: ')", "NNN NNN NNNN", false],
|
39
|
+
|
40
|
+
# Code has only title and code
|
41
|
+
"code" => ["Readline.readline('code: ')", "XXXX", true],
|
42
|
+
|
43
|
+
# useful for software licenses
|
44
|
+
"version" => ["Readline.readline('version: ')", "''", false],
|
45
|
+
"licensed_to" => ["Readline.readline('licensed to: ')", "''", false],
|
46
|
+
"license_number" => ["Readline.readline('license number: ')", "''", true],
|
47
|
+
"purchased_on" => ["Readline.readline('purchased on: ')", "Date.today", false],
|
48
|
+
}
|
49
|
+
|
50
|
+
# ask the value for +key+
|
51
|
+
#
|
52
|
+
# This is performed by invoking the function defined for +key+ in the
|
53
|
+
# +FIELDS+, which typically asks for user input. If the user enters a
|
54
|
+
# value this is the one the function returns, otherwise we return the
|
55
|
+
# default value defined for +key+ in +FIELDS+.
|
56
|
+
#
|
57
|
+
# Optional hash +arguments+ contains a list of arguments to be passed to
|
58
|
+
# the function in +FIELDS+. This allows to customize the behaviour of the
|
59
|
+
# user-input function.
|
60
|
+
#
|
61
|
+
# **As a special case, if +arguments+ contains +key+, this is returned as
|
62
|
+
# the value. This allows to set the default for a +key+ outside this
|
63
|
+
# module.**
|
64
|
+
#
|
65
|
+
# Thus, for instance: ask 'username', {'username' => 'a'} will return 'a'.
|
66
|
+
#
|
67
|
+
def self.ask key, arguments
|
68
|
+
# if the default is specified outside this class, return it!
|
69
|
+
return arguments[key] if arguments[key]
|
70
|
+
|
71
|
+
# ... otherwise, do some work and ask for the value!
|
72
|
+
input_f = FIELDS[key] ? FIELDS[key][INPUT_F] : "Readline.readline('#{key}: ')"
|
73
|
+
default = FIELDS[key] ? FIELDS[key][DEFAULT] : nil
|
74
|
+
value = eval input_f
|
75
|
+
if value != nil and value != "" then
|
76
|
+
value
|
77
|
+
else
|
78
|
+
default
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# read n-lines (terminated by a ".")
|
83
|
+
def self.get_lines
|
84
|
+
puts "description (terminate with '.'):"
|
85
|
+
lines = []
|
86
|
+
line = ""
|
87
|
+
until line == "."
|
88
|
+
line = Readline.readline
|
89
|
+
lines << line if line != "."
|
90
|
+
end
|
91
|
+
lines.join("\n")
|
92
|
+
end
|
93
|
+
|
94
|
+
# take a hash as input and reorder the fields according to the order in
|
95
|
+
# which the fields are defined in the +FIELDS+ variable
|
96
|
+
#
|
97
|
+
# this function is used to present records in the YAML file always in the
|
98
|
+
# same order.
|
99
|
+
def self.to_clean_hash hash
|
100
|
+
output = Hash.new
|
101
|
+
FIELDS.keys.each do |field|
|
102
|
+
output[field] = hash[field] if hash[field]
|
103
|
+
end
|
104
|
+
# all the remaining fields (i.e., user-defined fields in records)
|
105
|
+
(hash.keys - FIELDS.keys).each do |field|
|
106
|
+
output[field] = hash[field]
|
107
|
+
end
|
108
|
+
output
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.sensitive? field
|
112
|
+
FIELDS[field][HIDDEN]
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.sensitive
|
116
|
+
FIELDS.select { |x| FIELDS[x][HIDDEN] }.keys
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|