pws 0.1.3 → 0.9.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.
- data/LICENSE +1 -1
- data/README +10 -2
- data/Rakefile +33 -18
- data/bin/pws +76 -246
- data/lib/pws.rb +213 -1
- data/lib/pws/encryptor.rb +40 -0
- data/lib/pws/version.rb +3 -0
- data/pws.gemspec +28 -47
- metadata +97 -74
- data/.gitignore +0 -21
- data/VERSION +0 -1
data/LICENSE
CHANGED
data/README
CHANGED
@@ -1,5 +1,13 @@
|
|
1
|
-
pws password safe
|
1
|
+
pws is a command-line password safe/manager written in Ruby. Install it with
|
2
2
|
|
3
|
-
|
3
|
+
$ gem install pws
|
4
|
+
|
5
|
+
Please run `pws help` for usage information.
|
6
|
+
|
7
|
+
Trust the code by reading the source! Originally based on: http://rbjl.net/41-tutorial-build-your-own-password-safe-with-ruby
|
8
|
+
|
9
|
+
Cucumber specs loosely based on https://github.com/thecatwasnot/passwordsafe/blob/master/features/add.feature by thecatwasnot - thanks.
|
10
|
+
|
11
|
+
Copyright: 2010-2012 Jan Lelis, MIT-LICENSE
|
4
12
|
|
5
13
|
J-_-L
|
data/Rakefile
CHANGED
@@ -1,19 +1,34 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
NAME = Dir['*.gemspec'].first
|
4
|
+
|
5
|
+
def gemspec
|
6
|
+
@gemspec ||= eval(File.read(NAME), binding, NAME)
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Build the gem"
|
10
|
+
task :gem => :gemspec do
|
11
|
+
sh "gem build #{NAME}"
|
12
|
+
FileUtils.mkdir_p 'pkg'
|
13
|
+
FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Install the gem locally"
|
17
|
+
task :install => :gem do
|
18
|
+
sh %{gem install pkg/#{gemspec.name}-#{gemspec.version} --no-rdoc --no-ri}
|
19
19
|
end
|
20
|
+
|
21
|
+
desc "Generate the gemspec"
|
22
|
+
task :generate do
|
23
|
+
puts gemspec.to_ruby
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Validate the gemspec"
|
27
|
+
task :gemspec do
|
28
|
+
gemspec.validate
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'cucumber/rake/task'
|
32
|
+
Cucumber::Rake::Task.new(:spec)
|
33
|
+
|
34
|
+
task :default => :spec
|
data/bin/pws
CHANGED
@@ -1,259 +1,89 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
# see http://rbjl.net/41-tutorial-build-your-own-password-safe-with-ruby for more information
|
3
|
+
require_relative '../lib/pws'
|
5
4
|
|
6
|
-
|
5
|
+
action_or_namespace = $*.shift
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
class PasswordSafe
|
16
|
-
VERSION = "0.1.3".freeze
|
17
|
-
|
18
|
-
Entry = Struct.new :description, :password
|
19
|
-
|
20
|
-
class NoAccess < StandardError; end
|
21
|
-
|
22
|
-
# Creates a new password safe. Takes the path to the password file, by default: ~/.pws
|
23
|
-
def initialize( filename = File.expand_path('~/.pws') )
|
24
|
-
@pwfile = filename
|
25
|
-
|
26
|
-
access_safe
|
27
|
-
read_safe
|
28
|
-
end
|
29
|
-
|
30
|
-
# Add a password entry, params: name, description (optional), password (optional, opens prompt if not given)
|
31
|
-
def add(key, description = nil, password = nil)
|
32
|
-
if @pwdata[key]
|
33
|
-
print "Do you really want to overwrite the exisiting password entry? (press <Enter> to proceed)"
|
34
|
-
return unless (a = $stdin.getc) == 10 || a == 13
|
35
|
-
else
|
36
|
-
@pwdata[key] = Entry.new
|
37
|
-
end
|
38
|
-
@pwdata[key].password = password || ask_for_password( "please enter a password for #{key}" )
|
39
|
-
@pwdata[key].description = description
|
40
|
-
write_safe
|
41
|
-
|
42
|
-
puts "The password safe has been updated"
|
43
|
-
end
|
44
|
-
aliases_for :add, :a, :set, :create, :update, :[]= # using zucker/alias_for
|
45
|
-
|
46
|
-
# Gets the password entry and copies it to the clipboard. The second parameter is the time in seconds it stays there
|
47
|
-
def get(key, seconds = 20)
|
48
|
-
if pw_plaintext = @pwdata[key] && @pwdata[key].password
|
49
|
-
Clipboard.copy pw_plaintext
|
50
|
-
if seconds && seconds.to_i > 0
|
51
|
-
puts "The password is available in your clipboard for #{seconds.to_i} seconds"
|
52
|
-
sleep seconds.to_i
|
53
|
-
Clipboard.clear
|
54
|
-
else
|
55
|
-
puts "The password has been copied to your clipboard"
|
56
|
-
end
|
57
|
-
else
|
58
|
-
puts "No password entry found for #{key}"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
aliases_for :get, :g, :entry, :[]
|
62
|
-
|
63
|
-
# Removes a specific password entry
|
64
|
-
def remove(key)
|
65
|
-
if @pwdata.delete key
|
66
|
-
puts "#{key} has been removed"
|
67
|
-
else
|
68
|
-
puts "Nothing removed"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
aliases_for :remove, :r, :delete
|
72
|
-
|
73
|
-
# Shows a password entry list
|
74
|
-
def show
|
75
|
-
puts "Available passwords \n" +
|
76
|
-
|
77
|
-
if @pwdata.empty?
|
78
|
-
' (none)'
|
79
|
-
else
|
80
|
-
@pwdata.map{ |key, pwentry|
|
81
|
-
" #{key}" + if pwentry.description then ": #{pwentry.description}" else '' end
|
82
|
-
}*"\n"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
aliases_for :show, :s, :list
|
86
|
-
|
87
|
-
# Shows descriptions of some password entries
|
88
|
-
def description(*keys)
|
89
|
-
keys.each{ |key|
|
90
|
-
if @pwdata[key]
|
91
|
-
puts "#{key}: #{@pwdata[key].description || key}"
|
92
|
-
else
|
93
|
-
puts "No password entry found for #{key}"
|
94
|
-
end
|
95
|
-
}
|
96
|
-
end
|
97
|
-
|
98
|
-
# Changes the master password
|
99
|
-
def master
|
100
|
-
@pwhash = Encryptor.hash ask_for_password 'please enter a new master password'
|
101
|
-
write_safe
|
102
|
-
puts 'The master password has been changed'
|
103
|
-
end
|
104
|
-
aliases_for :master, :m
|
105
|
-
|
106
|
-
# Adds a password entry with a fresh generated random password
|
107
|
-
def generate( key, description = nil, length = 128, chars = (32..126).map(&:chr) )
|
108
|
-
add key, nil, (1..length).map{ chars[rand chars.size] }.join # possible in 1.9: chars.sample
|
109
|
-
end
|
110
|
-
alias_for :generate, :gen
|
111
|
-
|
112
|
-
# Prevents accidental displaying, e.g. in irb
|
113
|
-
def to_s
|
114
|
-
'#<just another password safe>'
|
115
|
-
end
|
116
|
-
alias_for :to_s, :inspect
|
7
|
+
if action_or_namespace =~ /^-.*$/
|
8
|
+
@action = $*.shift
|
9
|
+
@namespace = $& unless $& == ?-
|
10
|
+
else
|
11
|
+
@action = action_or_namespace
|
12
|
+
@namespace = nil
|
13
|
+
end
|
117
14
|
|
118
|
-
|
15
|
+
@action = @action ? @action.to_sym : :show
|
16
|
+
@args = [*$*]
|
119
17
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
18
|
+
begin
|
19
|
+
case @action
|
20
|
+
when :v, :version
|
21
|
+
puts "pws #{PWS::VERSION} by " + Paint["J-_-L", :bold] + " <https://github.com/janlelis/pws>"
|
22
|
+
when :help, :actions, :commands
|
23
|
+
puts \
|
24
|
+
<<HELP
|
25
|
+
|
26
|
+
#{Paint["Usage", :underline]}
|
27
|
+
|
28
|
+
#{Paint['pws', :bold]} [-namespace] action [arguments]
|
29
|
+
|
30
|
+
#{Paint["Info", :underline]}
|
31
|
+
|
32
|
+
pws allows you to manage passwords in encryted password files (safes). It
|
33
|
+
operates on the file specified in the environment variable PWS or on "~/.pws".
|
34
|
+
You can apply a namespace as first parameter that will be appended to the
|
35
|
+
filename, e.g. `pws -work show` with usual env would use "~/.pws-work".
|
137
36
|
|
138
|
-
#
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
37
|
+
#{Paint["Available actions", :underline]}
|
38
|
+
|
39
|
+
#{Paint['ls', :bold]} / list / show / status
|
40
|
+
Lists all available password entries.
|
41
|
+
|
42
|
+
#{Paint['add', :bold]} / set / store / create ( name, password = nil )
|
43
|
+
Stores a new password entry. The second argument can be the password, but
|
44
|
+
it's recommended to not pass it, but enter it interactively.
|
45
|
+
|
46
|
+
#{Paint['get', :bold]} / entry / copy / password / for ( name, seconds = 10 )
|
47
|
+
Copies the password for <name> to the clipboard. The second argument specifies,
|
48
|
+
how long the password is kept in the clipboard (0 = no deletion).
|
49
|
+
|
50
|
+
#{Paint['gen', :bold]} / generate ( name, seconds = 10, length = 64, char_pool )
|
51
|
+
Generates a new password for <name> and then copies it to the clipboard, like
|
52
|
+
get (the second argument is the time - it gets passed to get). The third
|
53
|
+
argument sets the password length. The fourth argument allows you to pass a
|
54
|
+
character pool that is used for generating the passwords.
|
55
|
+
|
56
|
+
#{Paint['rm', :bold]} / remove / del / delete ( name )
|
57
|
+
Removes a password entry.
|
58
|
+
|
59
|
+
#{Paint['mv', :bold]} / move / rename ( old_name, new_name )
|
60
|
+
Renames a password entry.
|
61
|
+
|
62
|
+
#{Paint['master', :bold]} ( password = nil )
|
63
|
+
Changes the master password.
|
64
|
+
|
65
|
+
#{Paint['v', :bold]} / version
|
66
|
+
Displays version and website.
|
67
|
+
|
68
|
+
#{Paint['help', :bold]} / actions / commands
|
69
|
+
Displays this help.
|
70
|
+
|
71
|
+
HELP
|
72
|
+
else # redirect to safe
|
73
|
+
if PWS.public_instance_methods(false).include?(@action)
|
74
|
+
PWS.new(nil, @namespace).send @action, *@args
|
145
75
|
else
|
146
|
-
|
147
|
-
end
|
148
|
-
rescue
|
149
|
-
raise NoAccess, "Could not access the password safe at #@pwfile!"
|
150
|
-
end
|
151
|
-
|
152
|
-
# Adds some redundancy
|
153
|
-
def add_dummy_data(pwdata)
|
154
|
-
(5000 - pwdata.size).abs.times.map{ rand 42424242 } + # or whatever
|
155
|
-
[pwdata]
|
156
|
-
end
|
157
|
-
|
158
|
-
def remove_dummy_data(pwdata)
|
159
|
-
pwdata.last
|
160
|
-
end
|
161
|
-
|
162
|
-
# Prompts the user for a password
|
163
|
-
def ask_for_password(prompt = 'new password')
|
164
|
-
print "#{prompt}: ".capitalize
|
165
|
-
system 'stty -echo' # no more terminal output
|
166
|
-
pw_plaintext = ($stdin.gets||'').chop # gets without $stdin would mistakenly read_safe from ARGV
|
167
|
-
system 'stty echo' # restore terminal output
|
168
|
-
puts
|
169
|
-
|
170
|
-
pw_plaintext
|
171
|
-
end
|
172
|
-
|
173
|
-
class << Encryptor = Module.new
|
174
|
-
CIPHER = 'aes-256-cbc'
|
175
|
-
|
176
|
-
def decrypt( iv_and_data, pwhash )
|
177
|
-
iv, data = iv_and_data[0,16], iv_and_data[16..-1]
|
178
|
-
crypt :decrypt, data, pwhash, iv
|
179
|
-
end
|
180
|
-
|
181
|
-
def encrypt( data, pwhash )
|
182
|
-
iv = random_iv
|
183
|
-
encrypted_data = crypt :encrypt, data, pwhash, iv
|
184
|
-
iv + encrypted_data
|
185
|
-
end
|
186
|
-
|
187
|
-
def hash( plaintext )
|
188
|
-
OpenSSL::Digest::SHA512.new( plaintext ).digest
|
189
|
-
end
|
190
|
-
|
191
|
-
# you need a random iv for cbc mode. It is prepended to the encrypted text.
|
192
|
-
def random_iv
|
193
|
-
a = OpenSSL::Cipher.new CIPHER
|
194
|
-
a.random_iv
|
76
|
+
pa "Unknown action: #@action\nPlease see `pws help` for a list of available commands!", :red
|
195
77
|
end
|
196
|
-
|
197
|
-
private
|
198
|
-
|
199
|
-
# Encrypts or decrypts the data with the password hash as key
|
200
|
-
# NOTE: encryption exceptions do not get caught!
|
201
|
-
def crypt( decrypt_or_encrypt, data, pwhash, iv )
|
202
|
-
c = OpenSSL::Cipher.new CIPHER
|
203
|
-
c.send decrypt_or_encrypt.to_sym
|
204
|
-
c.key = pwhash
|
205
|
-
c.iv = iv
|
206
|
-
c.update( data ) << c.final
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
# Command line action
|
212
|
-
if standalone? # using zucker/kernel (instead of __FILE__ == $0)
|
213
|
-
if $*.empty?
|
214
|
-
action = :show
|
215
|
-
args = []
|
216
|
-
else
|
217
|
-
action = $*.shift[/^-{0,2}(.*)$/, 1].to_sym # also accept first argument, if it is prefixed with - or --
|
218
|
-
args = [*$*]
|
219
|
-
end
|
220
|
-
|
221
|
-
begin
|
222
|
-
case action
|
223
|
-
when :h, :help, :commands
|
224
|
-
puts %q{Available commands
|
225
|
-
s/show/list shows all available entry names
|
226
|
-
g/get/entry( name, seconds = 20 )
|
227
|
-
copies the password of the entry into the clipboard
|
228
|
-
d/description( names ) shows a description for the password entries
|
229
|
-
a/add/set/create( name, description = nil, password = nil )
|
230
|
-
creates or updates an entry. second parameter is a
|
231
|
-
description. third parameter can be a password, but
|
232
|
-
it's recommended to not use it and enter it, when prompted
|
233
|
-
gen/generate( name, description = nil, length=128, chars = (32..126).map(&:chr) )
|
234
|
-
creates or updates an entry, but generates a new
|
235
|
-
random password. you can customize the length and
|
236
|
-
used characters
|
237
|
-
r/remove/delete( name ) deletes an password entry
|
238
|
-
m/master changes the master password
|
239
|
-
v/version displays version
|
240
|
-
h/help/commands displays this help}
|
241
|
-
when :v, :version
|
242
|
-
puts "pws #{PasswordSafe::VERSION}\n J-_-L"
|
243
|
-
else # redirect to safe
|
244
|
-
if PasswordSafe.public_instance_methods(false).include?(
|
245
|
-
if RubyVersion.is?(1.8) then action.to_s else action end ) # using zucker/version
|
246
|
-
|
247
|
-
pws = PasswordSafe.new
|
248
|
-
pws.send action, *args
|
249
|
-
else
|
250
|
-
puts "Unknown command: #{action}. Use 'help' to get a command list!"
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
rescue PasswordSafe::NoAccess => e
|
255
|
-
warn e.message
|
256
78
|
end
|
79
|
+
rescue PWS::NoAccess
|
80
|
+
# pa $!.message.capitalize, :red, :bold
|
81
|
+
pa "NO ACCESS", :red, :bold
|
82
|
+
rescue ArgumentError
|
83
|
+
pa $!.message.capitalize, :red
|
84
|
+
rescue Interrupt
|
85
|
+
system 'stty echo' if $stdin.tty? # ensure terminal's working
|
86
|
+
pa "..canceled", :red
|
257
87
|
end
|
258
88
|
|
259
89
|
# J-_-L
|
data/lib/pws.rb
CHANGED
@@ -1 +1,213 @@
|
|
1
|
-
|
1
|
+
require_relative 'pws/version'
|
2
|
+
require_relative 'pws/encryptor'
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'clipboard'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'zucker/alias_for'
|
8
|
+
require 'paint/pa'
|
9
|
+
|
10
|
+
class PWS
|
11
|
+
class NoAccess < StandardError; end
|
12
|
+
|
13
|
+
# Creates a new password safe. Takes the path to the password file, by default: ~/.pws
|
14
|
+
# Second parameter allows namespaces that get appended to the file name (uses another safe)
|
15
|
+
# You can pass the master password as third parameter (not recommended)
|
16
|
+
def initialize(filename = nil, namespace = nil, password = nil)
|
17
|
+
@pw_file = File.expand_path(filename || ENV["PWS"] || '~/.pws')
|
18
|
+
@pw_file << namespace if namespace
|
19
|
+
access_safe(password)
|
20
|
+
read_safe
|
21
|
+
end
|
22
|
+
|
23
|
+
# Shows a password entry list
|
24
|
+
def show
|
25
|
+
if @pw_data.empty?
|
26
|
+
pa %[There aren't any passwords stored at #{@pw_file}, yet], :red
|
27
|
+
else
|
28
|
+
puts Paint["Entries", :underline] + %[ in ] + @pw_file
|
29
|
+
puts @pw_data.keys.sort.map{ |key| %[- #{key}\n] }.join
|
30
|
+
end
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
aliases_for :show, :ls, :list, :status
|
34
|
+
|
35
|
+
# Add a password entry, params: name, password (optional, opens prompt if not given)
|
36
|
+
def add(key, password = nil)
|
37
|
+
if @pw_data[key]
|
38
|
+
pa %[There is already a password stored for #{key}. You need to remove it before creating a new one!], :red
|
39
|
+
return false
|
40
|
+
else
|
41
|
+
@pw_data[key] = password || ask_for_password(%[please enter a password for #{key}], :yellow)
|
42
|
+
if @pw_data[key].empty?
|
43
|
+
pa %[Cannot add an empty password!], :red
|
44
|
+
return false
|
45
|
+
else
|
46
|
+
write_safe
|
47
|
+
pa %[The password for #{key} has been added], :green
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
aliases_for :add, :set, :store, :create, :[]= # using zucker/alias_for
|
53
|
+
|
54
|
+
# Gets the password entry and copies it to the clipboard. The second parameter is the time in seconds it stays there
|
55
|
+
def get(key, seconds = 10)
|
56
|
+
if pw_plaintext = @pw_data[key]
|
57
|
+
if seconds && seconds.to_i > 0
|
58
|
+
original_clipboard_content = Clipboard.paste
|
59
|
+
Clipboard.copy pw_plaintext
|
60
|
+
pa %[The password for #{key} is now available in your clipboard for #{seconds.to_i} second#{?s if seconds.to_i > 1}], :green
|
61
|
+
begin
|
62
|
+
sleep seconds.to_i
|
63
|
+
rescue Interrupt
|
64
|
+
Clipboard.copy original_clipboard_content
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
Clipboard.copy original_clipboard_content
|
68
|
+
return true
|
69
|
+
else
|
70
|
+
Clipboard.copy pw_plaintext
|
71
|
+
pa %[The password for #{key} has been copied to your clipboard], :green
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
else
|
75
|
+
pa %[No password found for #{key}!], :red
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
aliases_for :get, :entry, :copy, :password, :for, :[]
|
80
|
+
|
81
|
+
# Adds a password entry with a freshly generated random password
|
82
|
+
def generate(
|
83
|
+
key,
|
84
|
+
seconds = 10,
|
85
|
+
length = 64,
|
86
|
+
char_pool = (32..126).map(&:chr).join.gsub(/\s/, '')
|
87
|
+
)
|
88
|
+
char_pool_size = char_pool.size
|
89
|
+
new_pw = (1..length.to_i).map{
|
90
|
+
char_pool[SecureRandom.random_number(char_pool_size)]
|
91
|
+
}.join
|
92
|
+
|
93
|
+
if add(key, new_pw)
|
94
|
+
get(key, seconds)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
alias_for :generate, :gen
|
98
|
+
|
99
|
+
# Removes a specific password entry
|
100
|
+
def remove(key)
|
101
|
+
if @pw_data.delete key
|
102
|
+
write_safe
|
103
|
+
pa %[The password for #{key} has been removed], :green
|
104
|
+
return true
|
105
|
+
else
|
106
|
+
pa %[No password found for #{key}!], :red
|
107
|
+
return false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
aliases_for :remove, :rm, :del, :delete
|
111
|
+
|
112
|
+
# Removes a specific password entry
|
113
|
+
def rename(old_key, new_key)
|
114
|
+
if !@pw_data[old_key]
|
115
|
+
pa %[No password found for #{old_key}!], :red
|
116
|
+
return false
|
117
|
+
elsif @pw_data[new_key]
|
118
|
+
pa %[There is already a password stored for #{new_key}. You need to remove it before naming another one #{new_key}!], :red
|
119
|
+
return false
|
120
|
+
else
|
121
|
+
@pw_data[new_key] = @pw_data.delete(old_key)
|
122
|
+
write_safe
|
123
|
+
pa %[The password entry #{old_key} has been renamed to #{new_key}], :green
|
124
|
+
return true
|
125
|
+
end
|
126
|
+
end
|
127
|
+
aliases_for :rename, :mv, :move
|
128
|
+
|
129
|
+
# Changes the master password
|
130
|
+
def master(password = nil)
|
131
|
+
@pw_hash = Encryptor.hash password || ask_for_password(%[please enter a new master password], :yellow, :bold)
|
132
|
+
write_safe
|
133
|
+
pa %[The master password has been changed], :green
|
134
|
+
return true
|
135
|
+
end
|
136
|
+
|
137
|
+
# Prevents accidental displaying, e.g. in irb
|
138
|
+
def to_s
|
139
|
+
%[#<password safe>]
|
140
|
+
end
|
141
|
+
alias_for :to_s, :inspect
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# Tries to load and decrypt the password safe from the pwfile
|
146
|
+
def read_safe
|
147
|
+
pwdata_raw = File.read(@pw_file)
|
148
|
+
pwdata_encrypted = pwdata_raw.force_encoding("ascii")
|
149
|
+
pwdata_dump = Encryptor.decrypt(pwdata_encrypted, @pw_hash)
|
150
|
+
pwdata_with_redundancy = Marshal.load(pwdata_dump)
|
151
|
+
@pw_data = remove_redundancy(pwdata_with_redundancy)
|
152
|
+
pa %[ACCESS GRANTED], :green
|
153
|
+
rescue
|
154
|
+
fail NoAccess, %[Could not load and decrypt the password safe!]
|
155
|
+
end
|
156
|
+
|
157
|
+
# Tries to encrypt and save the password safe into the pwfile
|
158
|
+
def write_safe
|
159
|
+
pwdata_with_redundancy = add_redundancy(@pw_data || {})
|
160
|
+
pwdata_dump = Marshal.dump(pwdata_with_redundancy)
|
161
|
+
pwdata_encrypted = Encryptor.encrypt(pwdata_dump, @pw_hash)
|
162
|
+
File.open(@pw_file, 'w'){ |f| f.write(pwdata_encrypted) }
|
163
|
+
rescue
|
164
|
+
fail NoAccess, %[Could not encrypt and save the password safe!]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Checks if the file is accessible or create a new one
|
168
|
+
def access_safe(password = nil)
|
169
|
+
if !File.file? @pw_file
|
170
|
+
pa %[No password safe detected, creating one at #@pw_file], :blue, :bold
|
171
|
+
@pw_hash = Encryptor.hash password || ask_for_password(%[please enter a new master password], :yellow, :bold)
|
172
|
+
write_safe
|
173
|
+
else
|
174
|
+
print %[Access password safe at #@pw_file | ]
|
175
|
+
@pw_hash = Encryptor.hash password || ask_for_password(%[master password])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Adds some redundancy (to conceal how much you have stored)
|
180
|
+
def add_redundancy(pw_data)
|
181
|
+
entries = 8000 + SecureRandom.random_number(4000)
|
182
|
+
position = SecureRandom.random_number(entries)
|
183
|
+
|
184
|
+
ret = entries.times.map{ # or whatever... just create noise ;)
|
185
|
+
{ SecureRandom.uuid.chars.to_a.shuffle.join => SecureRandom.uuid.chars.to_a.shuffle.join }
|
186
|
+
}
|
187
|
+
ret[position] = pw_data
|
188
|
+
ret << position
|
189
|
+
|
190
|
+
ret
|
191
|
+
end
|
192
|
+
|
193
|
+
# And remove it
|
194
|
+
def remove_redundancy(pw_data)
|
195
|
+
position = pw_data[-1]
|
196
|
+
pw_data[position]
|
197
|
+
end
|
198
|
+
|
199
|
+
# Prompts the user for a password
|
200
|
+
def ask_for_password(prompt = 'new password', *colors)
|
201
|
+
print Paint["#{prompt}:".capitalize, *colors] + " "
|
202
|
+
system 'stty -echo' if $stdin.tty? # no more terminal output
|
203
|
+
pw_plaintext = ($stdin.gets||'').chop # gets without $stdin would mistakenly read_safe from ARGV
|
204
|
+
system 'stty echo' if $stdin.tty? # restore terminal output
|
205
|
+
puts "\e[999D\e[K\e[1A" if $stdin.tty? # re-use prompt line in terminal
|
206
|
+
|
207
|
+
pw_plaintext
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Command line action in bin/pws
|
212
|
+
|
213
|
+
# J-_-L
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module PWS::Encryptor
|
4
|
+
class << self
|
5
|
+
CIPHER = 'aes-256-cbc'
|
6
|
+
|
7
|
+
def decrypt( iv_and_data, pwhash )
|
8
|
+
iv, data = iv_and_data[0,16], iv_and_data[16..-1]
|
9
|
+
crypt :decrypt, data, pwhash, iv
|
10
|
+
end
|
11
|
+
|
12
|
+
def encrypt( data, pwhash )
|
13
|
+
iv = random_iv
|
14
|
+
encrypted_data = crypt :encrypt, data, pwhash, iv
|
15
|
+
iv + encrypted_data
|
16
|
+
end
|
17
|
+
|
18
|
+
def hash( plaintext )
|
19
|
+
OpenSSL::Digest::SHA512.new( plaintext ).digest
|
20
|
+
end
|
21
|
+
|
22
|
+
# you need a random iv for cbc mode. It is prepended to the encrypted text.
|
23
|
+
def random_iv
|
24
|
+
a = OpenSSL::Cipher.new CIPHER
|
25
|
+
a.random_iv
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Encrypts or decrypts the data with the password hash as key
|
31
|
+
# NOTE: encryption exceptions do not get caught here!
|
32
|
+
def crypt( decrypt_or_encrypt, data, pwhash, iv )
|
33
|
+
c = OpenSSL::Cipher.new CIPHER
|
34
|
+
c.send decrypt_or_encrypt.to_sym
|
35
|
+
c.key = pwhash
|
36
|
+
c.iv = iv
|
37
|
+
c.update( data ) << c.final
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/pws/version.rb
ADDED
data/pws.gemspec
CHANGED
@@ -1,52 +1,33 @@
|
|
1
|
-
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
name = 'pws'
|
5
3
|
|
4
|
+
require File.dirname(__FILE__) + "/lib/#{name}/version"
|
5
|
+
|
6
6
|
Gem::Specification.new do |s|
|
7
|
-
s.
|
8
|
-
s.
|
7
|
+
s.required_ruby_version = '>= 1.9'
|
8
|
+
s.name = name
|
9
|
+
s.version = PWS::VERSION
|
10
|
+
s.authors = ["Jan Lelis"]
|
11
|
+
s.email = "mail@janlelis.de"
|
12
|
+
s.homepage = 'https://github.com/janlelis/pws'
|
13
|
+
s.summary = "pws is a cli password safe."
|
14
|
+
s.description = "pws is a command-line password safe. Please run `pws help` for usage information."
|
15
|
+
s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc} ext/**/*.{rb,c}]) + %w{Rakefile pws.gemspec}
|
16
|
+
s.extra_rdoc_files = ["README", "LICENSE"]
|
17
|
+
s.license = 'MIT'
|
18
|
+
s.executables = ['pws']
|
19
|
+
s.add_dependency 'clipboard', '~> 1.0.0'
|
20
|
+
s.add_dependency 'zucker', '>= 12.1'
|
21
|
+
s.add_dependency 'paint', '>= 0.8.4'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
s.add_development_dependency 'cucumber'
|
24
|
+
s.add_development_dependency 'aruba'
|
9
25
|
|
10
|
-
|
11
|
-
s.
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"LICENSE",
|
18
|
-
"README"
|
19
|
-
]
|
20
|
-
s.files = [
|
21
|
-
".gitignore",
|
22
|
-
"LICENSE",
|
23
|
-
"README",
|
24
|
-
"Rakefile",
|
25
|
-
"VERSION",
|
26
|
-
"bin/pws",
|
27
|
-
"lib/pws.rb",
|
28
|
-
"pws.gemspec"
|
29
|
-
]
|
30
|
-
s.homepage = %q{http://rbjl.net/41-tutorial-build-your-own-password-safe-with-ruby}
|
31
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
32
|
-
s.require_paths = ["lib"]
|
33
|
-
s.rubygems_version = %q{1.3.7}
|
34
|
-
s.summary = %q{pws is a little password safe. See pws help for more information}
|
35
|
-
|
36
|
-
if s.respond_to? :specification_version then
|
37
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
38
|
-
s.specification_version = 3
|
39
|
-
|
40
|
-
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
41
|
-
s.add_runtime_dependency(%q<clipboard>, [">= 0"])
|
42
|
-
s.add_runtime_dependency(%q<zucker>, [">= 0"])
|
43
|
-
else
|
44
|
-
s.add_dependency(%q<clipboard>, [">= 0"])
|
45
|
-
s.add_dependency(%q<zucker>, [">= 0"])
|
46
|
-
end
|
47
|
-
else
|
48
|
-
s.add_dependency(%q<clipboard>, [">= 0"])
|
49
|
-
s.add_dependency(%q<zucker>, [">= 0"])
|
50
|
-
end
|
26
|
+
len = s.homepage.size
|
27
|
+
s.post_install_message = \
|
28
|
+
(" ┌── " + "info ".ljust(len-2,'%') + "─┐\n" +
|
29
|
+
" J-_-L │ " + s.homepage + " │\n" +
|
30
|
+
" ├── " + "usage ".ljust(len-2,'%') + "─┤\n" +
|
31
|
+
" │ " + "pws help".ljust(len,' ') + " │\n" +
|
32
|
+
" └─" + '─'*len + "─┘").gsub('%', '─')
|
51
33
|
end
|
52
|
-
|
metadata
CHANGED
@@ -1,102 +1,125 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: pws
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 1
|
9
|
-
- 3
|
10
|
-
version: 0.1.3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
prerelease:
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
7
|
+
authors:
|
13
8
|
- Jan Lelis
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2012-01-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
22
15
|
name: clipboard
|
23
|
-
|
24
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: &17531100 !ruby/object:Gem::Requirement
|
25
17
|
none: false
|
26
|
-
requirements:
|
27
|
-
- -
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
|
30
|
-
segments:
|
31
|
-
- 0
|
32
|
-
version: "0"
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
33
22
|
type: :runtime
|
34
|
-
|
35
|
-
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *17531100
|
25
|
+
- !ruby/object:Gem::Dependency
|
36
26
|
name: zucker
|
27
|
+
requirement: &17530380 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '12.1'
|
33
|
+
type: :runtime
|
37
34
|
prerelease: false
|
38
|
-
|
35
|
+
version_requirements: *17530380
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: paint
|
38
|
+
requirement: &17529720 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
|
-
requirements:
|
41
|
-
- -
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
|
44
|
-
segments:
|
45
|
-
- 0
|
46
|
-
version: "0"
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.8.4
|
47
44
|
type: :runtime
|
48
|
-
|
49
|
-
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *17529720
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &17528920 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *17528920
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: cucumber
|
60
|
+
requirement: &17528420 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *17528420
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: aruba
|
71
|
+
requirement: &17527960 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *17527960
|
80
|
+
description: pws is a command-line password safe. Please run `pws help` for usage
|
81
|
+
information.
|
50
82
|
email: mail@janlelis.de
|
51
|
-
executables:
|
83
|
+
executables:
|
52
84
|
- pws
|
53
85
|
extensions: []
|
54
|
-
|
55
|
-
extra_rdoc_files:
|
56
|
-
- LICENSE
|
86
|
+
extra_rdoc_files:
|
57
87
|
- README
|
58
|
-
files:
|
59
|
-
- .gitignore
|
60
88
|
- LICENSE
|
61
|
-
|
62
|
-
-
|
63
|
-
-
|
64
|
-
- bin/pws
|
89
|
+
files:
|
90
|
+
- lib/pws/encryptor.rb
|
91
|
+
- lib/pws/version.rb
|
65
92
|
- lib/pws.rb
|
93
|
+
- bin/pws
|
94
|
+
- Rakefile
|
66
95
|
- pws.gemspec
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
96
|
+
- README
|
97
|
+
- LICENSE
|
98
|
+
homepage: https://github.com/janlelis/pws
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
post_install_message: ! " ┌── info ─────────────────────────┐\n J-_-L │ https://github.com/janlelis/pws
|
102
|
+
│\n ├── usage ────────────────────────┤\n │ pws help │\n
|
103
|
+
\ └─────────────────────────────────┘"
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
75
106
|
- lib
|
76
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
108
|
none: false
|
78
|
-
requirements:
|
79
|
-
- -
|
80
|
-
- !ruby/object:Gem::Version
|
81
|
-
|
82
|
-
|
83
|
-
- 0
|
84
|
-
version: "0"
|
85
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '1.9'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
114
|
none: false
|
87
|
-
requirements:
|
88
|
-
- -
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
|
91
|
-
segments:
|
92
|
-
- 0
|
93
|
-
version: "0"
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
94
119
|
requirements: []
|
95
|
-
|
96
120
|
rubyforge_project:
|
97
|
-
rubygems_version: 1.
|
121
|
+
rubygems_version: 1.8.11
|
98
122
|
signing_key:
|
99
123
|
specification_version: 3
|
100
|
-
summary: pws is a
|
124
|
+
summary: pws is a cli password safe.
|
101
125
|
test_files: []
|
102
|
-
|
data/.gitignore
DELETED
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.1.3
|