pws 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/Rakefile +17 -0
  5. data/VERSION +1 -0
  6. data/bin/pws +244 -0
  7. data/pws.gemspec +44 -0
  8. metadata +76 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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
+