pws 0.1.0.pre
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/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/bin/pws +244 -0
- data/pws.gemspec +44 -0
- metadata +76 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jan Lelis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "pws"
|
8
|
+
gem.summary = %Q{pws is a little PasswordSafe}
|
9
|
+
gem.email = "mail@janlelis.de"
|
10
|
+
gem.homepage = "http://rbjl.net/41"
|
11
|
+
gem.authors = ["Jan Lelis"]
|
12
|
+
gem.executables = %w[pws]
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
17
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0.pre
|
data/bin/pws
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems' if RUBY_VERSION[2] == ?8
|
4
|
+
|
5
|
+
require 'openssl'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'clipboard' # gem install clipboard
|
8
|
+
require 'zucker/alias_for' # gem install zucker
|
9
|
+
require 'zucker/kernel'
|
10
|
+
require 'zucker/version'
|
11
|
+
|
12
|
+
class PasswordSafe
|
13
|
+
VERSION = "0.1.0".freeze
|
14
|
+
|
15
|
+
Entry = Struct.new :description, :password
|
16
|
+
|
17
|
+
class NoAccess < StandardError; end
|
18
|
+
|
19
|
+
# Creates a new password safe. Takes the path to the password file, by default: ~/.pws
|
20
|
+
def initialize( filename = File.expand_path('~/.pws') )
|
21
|
+
@pwfile = filename
|
22
|
+
|
23
|
+
access_safe
|
24
|
+
read_safe
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a password entry, params: name, description (optional), password (optional, opens prompt if not given)
|
28
|
+
def add(key, description = nil, password = nil)
|
29
|
+
if @pwdata[key]
|
30
|
+
print "Do you really want to overwrite the exisiting password entry? (press <Enter> to proceed)"
|
31
|
+
return unless (a = $stdin.getc) == 10 || a == 13
|
32
|
+
else
|
33
|
+
@pwdata[key] = Entry.new
|
34
|
+
end
|
35
|
+
@pwdata[key].password = password || ask_for_password( "please enter a password for #{key}" )
|
36
|
+
@pwdata[key].description = description
|
37
|
+
write_safe
|
38
|
+
|
39
|
+
puts "The password safe has been updated"
|
40
|
+
end
|
41
|
+
aliases_for :add, :a, :set, :create, :update, :[]= # using zucker/alias_for
|
42
|
+
|
43
|
+
# Gets the password entry and copies it to the clipboard. The second parameter is the time in seconds it stays there
|
44
|
+
def get(key, seconds = 20)
|
45
|
+
if pw_plaintext = @pwdata[key] && @pwdata[key].password
|
46
|
+
Clipboard.copy pw_plaintext
|
47
|
+
if seconds && seconds.to_i > 0
|
48
|
+
puts "The password is available in your clipboard for #{seconds.to_i} seconds"
|
49
|
+
sleep seconds.to_i
|
50
|
+
Clipboard.clear
|
51
|
+
else
|
52
|
+
puts "The password has been copied to your clipboard"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
puts "No password entry found for #{key}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
aliases_for :get, :g, :entry, :[]
|
59
|
+
|
60
|
+
# Removes a specific password entry
|
61
|
+
def remove(key)
|
62
|
+
if @pwdata.delete key
|
63
|
+
puts "#{key} has been removed"
|
64
|
+
else
|
65
|
+
puts "Nothing removed"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
aliases_for :remove, :r, :delete
|
69
|
+
|
70
|
+
# Shows a password entry list
|
71
|
+
def show
|
72
|
+
puts "Available passwords \n" +
|
73
|
+
|
74
|
+
if @pwdata.empty?
|
75
|
+
' (none)'
|
76
|
+
else
|
77
|
+
@pwdata.map{ |key, pwentry|
|
78
|
+
" #{key}" + if pwentry.description then ": #{pwentry.description}" else '' end
|
79
|
+
}*"\n"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
aliases_for :show, :s, :list
|
83
|
+
|
84
|
+
# Shows descriptions of some password entries
|
85
|
+
def description(*keys)
|
86
|
+
keys.each{ |key|
|
87
|
+
if @pwdata[key]
|
88
|
+
puts "#{key}: #{@pwdata[key].description || key}"
|
89
|
+
else
|
90
|
+
puts "No password entry found for #{key}"
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# Changes the master password
|
96
|
+
def master
|
97
|
+
@pwhash = Encryptor.hash ask_for_password 'please enter a new master password'
|
98
|
+
write_safe
|
99
|
+
puts 'The master password has been changed'
|
100
|
+
end
|
101
|
+
aliases_for :master, :m
|
102
|
+
|
103
|
+
# Adds a password entry with a fresh generated random password
|
104
|
+
def generate( key, description = nil, length = 128, chars = (32..126).map(&:chr) )
|
105
|
+
add key, (1..length).map{ chars.sample }.join
|
106
|
+
end
|
107
|
+
|
108
|
+
# Prevents accidental displaying, e.g. in irb
|
109
|
+
def to_s
|
110
|
+
'#<just another password safe>'
|
111
|
+
end
|
112
|
+
alias_for :to_s, :inspect
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# Tries to load and decrypt the password safe from the pwfile
|
117
|
+
def read_safe
|
118
|
+
pwdata_encrypted = File.read @pwfile
|
119
|
+
pwdata_dump = Encryptor.decrypt( pwdata_encrypted, @pwhash )
|
120
|
+
@pwdata = remove_dummy_data( Marshal.load(pwdata_dump) ) || {}
|
121
|
+
rescue
|
122
|
+
fail NoAccess, 'Could not decrypt/load the password safe!'
|
123
|
+
end
|
124
|
+
|
125
|
+
# Tries to encrypt and save the password safe into the pwfile
|
126
|
+
def write_safe
|
127
|
+
pwdata_dump = Marshal.dump add_dummy_data( @pwdata || {} )
|
128
|
+
pwdata_encrypted = Encryptor.encrypt pwdata_dump, @pwhash
|
129
|
+
File.open( @pwfile, 'w' ){ |f| f.write pwdata_encrypted }
|
130
|
+
rescue
|
131
|
+
fail NoAccess, 'Could not encrypt/safe the password safe!'
|
132
|
+
end
|
133
|
+
|
134
|
+
# Checks if the file is accessible or create a new one
|
135
|
+
def access_safe
|
136
|
+
if !File.file? @pwfile
|
137
|
+
puts "No password safe detected, creating one at #@pwfile"
|
138
|
+
FileUtils.touch @pwfile
|
139
|
+
@pwhash = Encryptor.hash ask_for_password 'please enter a new master password'
|
140
|
+
write_safe
|
141
|
+
else
|
142
|
+
@pwhash = Encryptor.hash ask_for_password 'master password'
|
143
|
+
end
|
144
|
+
rescue
|
145
|
+
raise NoAccess, "Could not access the password safe at #@pwfile!"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Adds some redundancy
|
149
|
+
def add_dummy_data(pwdata)
|
150
|
+
(5000 - pwdata.size).abs.times.map{ rand 42424242 } + # or whatever
|
151
|
+
[pwdata]
|
152
|
+
end
|
153
|
+
|
154
|
+
def remove_dummy_data(pwdata)
|
155
|
+
pwdata.last
|
156
|
+
end
|
157
|
+
|
158
|
+
# Prompts the user for a password
|
159
|
+
def ask_for_password(prompt = 'new password')
|
160
|
+
print "#{prompt}: ".capitalize
|
161
|
+
system 'stty -echo' # no more terminal output
|
162
|
+
pw_plaintext = ($stdin.gets||'').chop # gets without $stdin would mistakenly read_safe from ARGV
|
163
|
+
system 'stty echo' # restore terminal output
|
164
|
+
puts
|
165
|
+
|
166
|
+
pw_plaintext
|
167
|
+
end
|
168
|
+
|
169
|
+
class << Encryptor = Module.new
|
170
|
+
CIPHER = 'AES256'
|
171
|
+
|
172
|
+
def decrypt( data, pwhash )
|
173
|
+
crypt :decrypt, data, pwhash
|
174
|
+
end
|
175
|
+
|
176
|
+
def encrypt( data, pwhash )
|
177
|
+
crypt :encrypt, data, pwhash
|
178
|
+
end
|
179
|
+
|
180
|
+
def hash( plaintext )
|
181
|
+
OpenSSL::Digest::SHA512.new( plaintext ).digest
|
182
|
+
end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
# Encrypts or decrypts the data with the password hash as key
|
187
|
+
# NOTE: encryption exceptions do not get caught!
|
188
|
+
def crypt( decrypt_or_encrypt, data, pwhash )
|
189
|
+
c = OpenSSL::Cipher.new CIPHER
|
190
|
+
c.send decrypt_or_encrypt.to_sym
|
191
|
+
c.key = pwhash
|
192
|
+
c.update( data ) << c.final
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Command line action
|
198
|
+
if standalone? # using zucker/kernel (instead of __FILE__ == $0)
|
199
|
+
if $*.empty?
|
200
|
+
action = :show
|
201
|
+
else
|
202
|
+
action = $*.shift[/^-{0,2}(.*)$/, 1].to_sym # also accept first argument, if it is prefixed with - or --
|
203
|
+
args = [*$*]
|
204
|
+
end
|
205
|
+
|
206
|
+
begin
|
207
|
+
case action
|
208
|
+
when :h, :help, :commands
|
209
|
+
puts %q{Available commands
|
210
|
+
s/show/list shows all available entry names
|
211
|
+
g/get/entry( name, seconds = 20 )
|
212
|
+
copies the password of the entry into the clipboard
|
213
|
+
d/description( names ) shows a description for the password entries
|
214
|
+
a/add/set/create( name, description = nil, password = nil )
|
215
|
+
creates or updates an entry. second parameter is a
|
216
|
+
description. third parameter can be a password, but
|
217
|
+
it's recommended to not use it and enter it, when prompted
|
218
|
+
gen/generate( name, description = nil, length=128, chars = (32..126).map(&:chr) )
|
219
|
+
creates or updates an entry, but generates a new
|
220
|
+
random password. you can customize the length and
|
221
|
+
used characters
|
222
|
+
r/remove/delete( name ) deletes an password entry
|
223
|
+
m/master changes the master password
|
224
|
+
v/version displays version
|
225
|
+
h/help/commands displays this help}
|
226
|
+
when :v, :version
|
227
|
+
puts "pws #{PasswordSafe::VERSION}\n J-_-L"
|
228
|
+
else # redirect to safe
|
229
|
+
if PasswordSafe.public_instance_methods(false).include?(
|
230
|
+
if RubyVersion.is?(1.8) then action.to_s else action end ) # using zucker/version
|
231
|
+
|
232
|
+
pws = PasswordSafe.new
|
233
|
+
pws.send action, *args
|
234
|
+
else
|
235
|
+
puts "Unknown command: #{action}. Use 'help' to get a command list!"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
rescue PasswordSafe::NoAccess => e
|
240
|
+
warn e.message
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# J-_-L
|
data/pws.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{pws}
|
8
|
+
s.version = "0.1.0.pre"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jan Lelis"]
|
12
|
+
s.date = %q{2010-11-01}
|
13
|
+
s.default_executable = %q{pws}
|
14
|
+
s.email = %q{mail@janlelis.de}
|
15
|
+
s.executables = ["pws"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"bin/pws",
|
26
|
+
"pws.gemspec"
|
27
|
+
]
|
28
|
+
s.homepage = %q{http://rbjl.net/41}
|
29
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
30
|
+
s.require_paths = ["lib"]
|
31
|
+
s.rubygems_version = %q{1.3.7}
|
32
|
+
s.summary = %q{pws is a little PasswordSafe}
|
33
|
+
|
34
|
+
if s.respond_to? :specification_version then
|
35
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
36
|
+
s.specification_version = 3
|
37
|
+
|
38
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
39
|
+
else
|
40
|
+
end
|
41
|
+
else
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pws
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 961915980
|
5
|
+
prerelease: true
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
- pre
|
11
|
+
version: 0.1.0.pre
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- Jan Lelis
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-11-01 00:00:00 +01:00
|
20
|
+
default_executable: pws
|
21
|
+
dependencies: []
|
22
|
+
|
23
|
+
description:
|
24
|
+
email: mail@janlelis.de
|
25
|
+
executables:
|
26
|
+
- pws
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files:
|
30
|
+
- LICENSE
|
31
|
+
files:
|
32
|
+
- .document
|
33
|
+
- .gitignore
|
34
|
+
- LICENSE
|
35
|
+
- Rakefile
|
36
|
+
- VERSION
|
37
|
+
- bin/pws
|
38
|
+
- pws.gemspec
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://rbjl.net/41
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --charset=UTF-8
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
hash: 3
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 25
|
63
|
+
segments:
|
64
|
+
- 1
|
65
|
+
- 3
|
66
|
+
- 1
|
67
|
+
version: 1.3.1
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: pws is a little PasswordSafe
|
75
|
+
test_files: []
|
76
|
+
|