pwss 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|