pws 0.9.2 → 1.0.0.pre.1
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/README.md +18 -14
- data/features/add.feature +14 -2
- data/features/create.feature +23 -0
- data/features/in-out.feature +54 -0
- data/features/master.feature +1 -0
- data/features/misc.feature +14 -0
- data/features/namespaces.feature +6 -4
- data/features/remove.feature +1 -0
- data/features/resave.feature +37 -0
- data/features/show.feature +36 -3
- data/features/step_definitions/pws_steps.rb +42 -2
- data/features/support/env.rb +3 -2
- data/features/update.feature +60 -0
- data/lib/pws.rb +109 -80
- data/lib/pws/encryptor.rb +28 -24
- data/lib/pws/format.rb +103 -0
- data/lib/pws/format/0.9.rb +44 -0
- data/lib/pws/format/1.0.rb +183 -0
- data/lib/pws/runner.rb +21 -2
- data/lib/pws/version.rb +1 -1
- data/pws.gemspec +13 -7
- metadata +96 -9
- data/features/access.feature +0 -13
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: ascii
|
2
|
+
require_relative '../format'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
class PWS
|
6
|
+
module Format
|
7
|
+
# PWS file format reader for versions before 1.0.0
|
8
|
+
module V0_9
|
9
|
+
class << self
|
10
|
+
def write(_,_={})
|
11
|
+
raise NotImplementedError, 'Writing the legacy 0.9 format is not supported'
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(saved_data, options = {})
|
15
|
+
unmarshal(decrypt(saved_data, options))
|
16
|
+
rescue
|
17
|
+
fail NoAccess, %[Could not read the password safe!]
|
18
|
+
end
|
19
|
+
|
20
|
+
def decrypt(saved_data, options = {})
|
21
|
+
iv, encrypted_data = saved_data.unpack('a16 a*')
|
22
|
+
PWS::Encryptor.decrypt(
|
23
|
+
encrypted_data,
|
24
|
+
key: sha(options[:password]),
|
25
|
+
iv: iv,
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def unmarshal(unencrypted_data, options = {})
|
30
|
+
raw_data = Marshal.load(unencrypted_data)
|
31
|
+
application_data = raw_data[ raw_data[-1] ] # remove redundancy
|
32
|
+
Hash[application_data.map{ |k,v| [k, { password: v}] }] # patch to new internal format
|
33
|
+
end
|
34
|
+
|
35
|
+
def sha(plaintext)
|
36
|
+
OpenSSL::Digest::SHA512.new(plaintext).digest
|
37
|
+
end
|
38
|
+
|
39
|
+
end#self
|
40
|
+
end#V0_9
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# J-_-L
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# encoding: ascii
|
2
|
+
|
3
|
+
require_relative '../format'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'digest/hmac'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
class PWS
|
9
|
+
module Format
|
10
|
+
# PWS file format for versions ~> 1.0.0
|
11
|
+
# see at bottom block for a short format description
|
12
|
+
module V1_0
|
13
|
+
TEMPLATE = 'a64 a16 N a64 a*'.freeze
|
14
|
+
DEFAULT_ITERATIONS = 80_000
|
15
|
+
MAX_ITERATIONS = 10_000_000
|
16
|
+
MAX_ENTRY_LENGTH = 4_294_967_295 # N
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def write(application_data, options = {})
|
20
|
+
encrypt(marshal(application_data), options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def encrypt(unencrypted_data, options = {})
|
24
|
+
raise ArgumentError, 'No password given' if \
|
25
|
+
!options[:password]
|
26
|
+
|
27
|
+
iterations = ( options[:iterations] || DEFAULT_ITERATIONS ).to_i
|
28
|
+
raise ArgumentError, 'Invalid iteration count given' if \
|
29
|
+
iterations > MAX_ITERATIONS || iterations < 2
|
30
|
+
|
31
|
+
salt = SecureRandom.random_bytes(64)
|
32
|
+
iv = Encryptor.random_iv
|
33
|
+
|
34
|
+
encryption_key, hmac_key = kdf(
|
35
|
+
options[:password],
|
36
|
+
salt,
|
37
|
+
iterations,
|
38
|
+
).unpack('a256 a256')
|
39
|
+
|
40
|
+
sha = hmac(hmac_key, salt, iv, iterations, unencrypted_data)
|
41
|
+
|
42
|
+
encrypted_data = Encryptor.encrypt(
|
43
|
+
unencrypted_data,
|
44
|
+
key: encryption_key,
|
45
|
+
iv: iv,
|
46
|
+
)
|
47
|
+
|
48
|
+
[salt, iv, iterations, sha, encrypted_data].pack(TEMPLATE)
|
49
|
+
end
|
50
|
+
|
51
|
+
def marshal(application_data, options = {})
|
52
|
+
number_of_dummy_bytes = 100_000 + SecureRandom.random_number(1_000_000)
|
53
|
+
ordered_data = application_data.to_a
|
54
|
+
[
|
55
|
+
number_of_dummy_bytes,
|
56
|
+
application_data.size,
|
57
|
+
SecureRandom.random_bytes(number_of_dummy_bytes) +
|
58
|
+
array_to_data_string(ordered_data.map{ |_, e| e[:password].to_s }) +
|
59
|
+
array_to_data_string(ordered_data.map{ |k, _| k.to_s }) +
|
60
|
+
array_to_data_string(ordered_data.map{ |_, e| e[:timestamp].to_i }) +
|
61
|
+
SecureRandom.random_bytes(100_000 + SecureRandom.random_number(1_000_000))
|
62
|
+
].pack('N N a*')
|
63
|
+
end
|
64
|
+
|
65
|
+
# - - -
|
66
|
+
|
67
|
+
def read(encrypted_data, options = {})
|
68
|
+
unmarshal(decrypt(encrypted_data, options))
|
69
|
+
end
|
70
|
+
|
71
|
+
def decrypt(saved_data, options = {})
|
72
|
+
raise ArgumentError, 'No password given' if \
|
73
|
+
!options[:password]
|
74
|
+
raise ArgumentError, 'No data given' if \
|
75
|
+
!saved_data || saved_data.empty?
|
76
|
+
salt, iv, iterations, sha, encrypted_data = saved_data.unpack(TEMPLATE)
|
77
|
+
|
78
|
+
raise NoAccess, 'Password file invalid' if \
|
79
|
+
salt.size != 64 ||
|
80
|
+
iterations > MAX_ITERATIONS ||
|
81
|
+
iv.size != 16 ||
|
82
|
+
sha.size != 64
|
83
|
+
|
84
|
+
encryption_key, hmac_key = kdf(
|
85
|
+
options[:password],
|
86
|
+
salt,
|
87
|
+
iterations,
|
88
|
+
).unpack('a256 a256')
|
89
|
+
|
90
|
+
begin
|
91
|
+
unencrypted_data = Encryptor.decrypt(
|
92
|
+
encrypted_data,
|
93
|
+
key: encryption_key,
|
94
|
+
iv: iv,
|
95
|
+
)
|
96
|
+
rescue OpenSSL::Cipher::CipherError
|
97
|
+
raise NoAccess, 'Could not decrypt'
|
98
|
+
end
|
99
|
+
|
100
|
+
raise NoAccess, 'Password file invalid' unless \
|
101
|
+
sha == hmac(hmac_key, salt, iv, iterations, unencrypted_data)
|
102
|
+
|
103
|
+
unencrypted_data
|
104
|
+
end
|
105
|
+
|
106
|
+
def unmarshal(saved_data, options = {})
|
107
|
+
number_of_dummy_bytes, data_size, raw_data = saved_data.unpack('N N a*')
|
108
|
+
i = number_of_dummy_bytes
|
109
|
+
passwords, names, timestamps = 3.times.map{
|
110
|
+
data_size.times.map{
|
111
|
+
next_element, i = get_next_data_string(raw_data, i)
|
112
|
+
next_element
|
113
|
+
}
|
114
|
+
}
|
115
|
+
Hash[
|
116
|
+
names.zip(
|
117
|
+
passwords.zip(timestamps).map{ |e,f|
|
118
|
+
{ password: e.to_s, timestamp: f.to_i }
|
119
|
+
}
|
120
|
+
)
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def kdf(password, salt, iterations)
|
127
|
+
OpenSSL::PKCS5::pbkdf2_hmac(
|
128
|
+
password,
|
129
|
+
salt,
|
130
|
+
iterations,
|
131
|
+
512,
|
132
|
+
OpenSSL::Digest::SHA512.new,
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
def hmac(key, *strings)
|
137
|
+
Digest::HMAC.new(key, Digest::SHA512).update(
|
138
|
+
strings.map(&:to_s).join
|
139
|
+
).digest
|
140
|
+
end
|
141
|
+
|
142
|
+
def array_to_data_string(array)
|
143
|
+
array.map{ |e|
|
144
|
+
e = e.to_s
|
145
|
+
s = e.bytesize
|
146
|
+
raise(ArgumentError, 'Entry too long') if s > MAX_ENTRY_LENGTH
|
147
|
+
[s, e].pack('N a*')
|
148
|
+
}.join
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_next_data_string(string, pos)
|
152
|
+
res_length = string[pos..pos+4].unpack('N')[0]
|
153
|
+
new_pos = pos + 4 + res_length
|
154
|
+
res = string[pos+4...new_pos].unpack('a*')[0]
|
155
|
+
|
156
|
+
[res, new_pos]
|
157
|
+
end
|
158
|
+
end#self
|
159
|
+
end#V1_0
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
=begin ENCRYPTION FORMAT
|
164
|
+
|
165
|
+
Bytes Data Description
|
166
|
+
64 SALT Randomly generated, used by kdf
|
167
|
+
16 IV Randomly generated, used by aes
|
168
|
+
4 ITERATIONS How often the password gets hashed by the kdf
|
169
|
+
64 HMAC On everything
|
170
|
+
* ENCRYPTED_DATA
|
171
|
+
|
172
|
+
=end
|
173
|
+
|
174
|
+
=begin MARSHAL FORMAT
|
175
|
+
|
176
|
+
number of dummy bytes before real data
|
177
|
+
dummy bytes
|
178
|
+
passwords
|
179
|
+
names
|
180
|
+
timestamps
|
181
|
+
dummy bytes
|
182
|
+
|
183
|
+
=end
|
data/lib/pws/runner.rb
CHANGED
@@ -91,20 +91,39 @@ module PWS::Runner
|
|
91
91
|
HELP
|
92
92
|
else # redirect to safe
|
93
93
|
if PWS.public_instance_methods(false).include?(action)
|
94
|
-
PWS.new(options).public_send(action, *arguments
|
94
|
+
status = PWS.new(options).public_send(action, *arguments.map{ |a|
|
95
|
+
a.unpack('a*')[0] # ignore encoding
|
96
|
+
})
|
97
|
+
exit(status ? 0 : 2)
|
95
98
|
else
|
96
|
-
|
99
|
+
raise ArgumentError, "Unknown action: #{action}\nPlease see `pws --help` for a list of available commands!"
|
97
100
|
end
|
98
101
|
end
|
102
|
+
rescue PWS::NoLegacyAccess
|
103
|
+
pa "NO ACCESS", :red, :bold
|
104
|
+
pa 'The password safe you are trying to access migth be a version 0.9 password file', :red
|
105
|
+
pa 'If this is the case, you will need to convert it to a version 1.0 password file by calling:', :red
|
106
|
+
pa 'pws resave --in 0.9 --out 1.0', :red
|
107
|
+
exit(3)
|
99
108
|
rescue PWS::NoAccess
|
100
109
|
# pa $!.message.capitalize, :red, :bold
|
101
110
|
pa "NO ACCESS", :red, :bold
|
111
|
+
exit(3)
|
102
112
|
rescue ArgumentError
|
103
113
|
pa $!.message.capitalize, :red
|
114
|
+
exit(4)
|
104
115
|
rescue Interrupt
|
105
116
|
system 'stty echo' if $stdin.tty? # ensure terminal's working
|
106
117
|
pa "..canceled", :red
|
118
|
+
exit(5)
|
107
119
|
end
|
120
|
+
|
121
|
+
# exit status codes (not final, yet)
|
122
|
+
# 0 Success
|
123
|
+
# 2 Successfully run, but operation not successful
|
124
|
+
# 3 NoAccess
|
125
|
+
# 4 ArgumentError
|
126
|
+
# 5 Interrupt
|
108
127
|
end
|
109
128
|
end
|
110
129
|
|
data/lib/pws/version.rb
CHANGED
data/pws.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.email = "mail@janlelis.de"
|
12
12
|
s.homepage = 'https://github.com/janlelis/pws'
|
13
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."
|
14
|
+
s.description = "pws is a command-line password safe. Please run `pws --help` for usage information."
|
15
15
|
s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc} ext/**/*.{rb,c} features/**/*]) + %w{Rakefile pws.gemspec}
|
16
16
|
s.extra_rdoc_files = ["README.md", "LICENSE"]
|
17
17
|
s.license = 'MIT'
|
@@ -20,14 +20,20 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_dependency 'zucker', '>= 12.1'
|
21
21
|
s.add_dependency 'paint', '>= 0.8.4'
|
22
22
|
s.add_development_dependency 'rake'
|
23
|
-
s.add_development_dependency 'cucumber'
|
24
23
|
s.add_development_dependency 'aruba'
|
25
|
-
|
24
|
+
s.add_development_dependency 'cucumber'
|
25
|
+
s.add_development_dependency 'rspec'
|
26
|
+
s.add_development_dependency 'guard-cucumber'
|
27
|
+
s.add_development_dependency 'guard-rspec'
|
28
|
+
s.add_development_dependency 'ripltools'
|
29
|
+
s.add_development_dependency 'irbtools'
|
30
|
+
# s.add_development_dependency 'ruby-debug19'
|
31
|
+
|
26
32
|
len = s.homepage.size
|
27
33
|
s.post_install_message = \
|
28
|
-
(" ┌── " + "info ".ljust(len-2,'
|
34
|
+
(" ┌── " + "info ".ljust(len-2,'─') + "─┐\n" +
|
29
35
|
" J-_-L │ " + s.homepage + " │\n" +
|
30
|
-
" ├── " + "usage ".ljust(len-2,'
|
31
|
-
" │ " + "pws help".ljust(len,' ')
|
32
|
-
" └─" + '─'*len + "─┘")
|
36
|
+
" ├── " + "usage ".ljust(len-2,'─') + "─┤\n" +
|
37
|
+
" │ " + "pws --help".ljust(len,' ') + " │\n" +
|
38
|
+
" └─" + '─'*len + "─┘")
|
33
39
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pws
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.0.pre.1
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Jan Lelis
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-05-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: clipboard
|
@@ -75,6 +75,22 @@ dependencies:
|
|
75
75
|
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: aruba
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
78
94
|
- !ruby/object:Gem::Dependency
|
79
95
|
name: cucumber
|
80
96
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,7 +108,39 @@ dependencies:
|
|
92
108
|
- !ruby/object:Gem::Version
|
93
109
|
version: '0'
|
94
110
|
- !ruby/object:Gem::Dependency
|
95
|
-
name:
|
111
|
+
name: rspec
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: guard-cucumber
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: guard-rspec
|
96
144
|
requirement: !ruby/object:Gem::Requirement
|
97
145
|
none: false
|
98
146
|
requirements:
|
@@ -107,7 +155,39 @@ dependencies:
|
|
107
155
|
- - ! '>='
|
108
156
|
- !ruby/object:Gem::Version
|
109
157
|
version: '0'
|
110
|
-
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: ripltools
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: irbtools
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
description: pws is a command-line password safe. Please run `pws --help` for usage
|
111
191
|
information.
|
112
192
|
email: mail@janlelis.de
|
113
193
|
executables:
|
@@ -117,20 +197,26 @@ extra_rdoc_files:
|
|
117
197
|
- README.md
|
118
198
|
- LICENSE
|
119
199
|
files:
|
200
|
+
- lib/pws/format.rb
|
201
|
+
- lib/pws/format/1.0.rb
|
202
|
+
- lib/pws/format/0.9.rb
|
120
203
|
- lib/pws/version.rb
|
121
204
|
- lib/pws/encryptor.rb
|
122
205
|
- lib/pws/runner.rb
|
123
206
|
- lib/pws.rb
|
124
207
|
- bin/pws
|
208
|
+
- features/in-out.feature
|
209
|
+
- features/update.feature
|
125
210
|
- features/show.feature
|
126
|
-
- features/access.feature
|
127
211
|
- features/misc.feature
|
128
212
|
- features/get.feature
|
129
213
|
- features/master.feature
|
130
214
|
- features/remove.feature
|
131
215
|
- features/namespaces.feature
|
216
|
+
- features/resave.feature
|
132
217
|
- features/add.feature
|
133
218
|
- features/support/env.rb
|
219
|
+
- features/create.feature
|
134
220
|
- features/rename.feature
|
135
221
|
- features/generate.feature
|
136
222
|
- features/step_definitions/pws_steps.rb
|
@@ -142,7 +228,7 @@ homepage: https://github.com/janlelis/pws
|
|
142
228
|
licenses:
|
143
229
|
- MIT
|
144
230
|
post_install_message: ! " ┌── info ─────────────────────────┐\n J-_-L │ https://github.com/janlelis/pws
|
145
|
-
│\n ├── usage ────────────────────────┤\n │ pws help
|
231
|
+
│\n ├── usage ────────────────────────┤\n │ pws --help │\n
|
146
232
|
\ └─────────────────────────────────┘"
|
147
233
|
rdoc_options: []
|
148
234
|
require_paths:
|
@@ -156,9 +242,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
156
242
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
243
|
none: false
|
158
244
|
requirements:
|
159
|
-
- - ! '
|
245
|
+
- - ! '>'
|
160
246
|
- !ruby/object:Gem::Version
|
161
|
-
version:
|
247
|
+
version: 1.3.1
|
162
248
|
requirements: []
|
163
249
|
rubyforge_project:
|
164
250
|
rubygems_version: 1.8.23
|
@@ -166,3 +252,4 @@ signing_key:
|
|
166
252
|
specification_version: 3
|
167
253
|
summary: pws is a cli password safe.
|
168
254
|
test_files: []
|
255
|
+
has_rdoc:
|