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.
@@ -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
- pa "Unknown action: #{action}\nPlease see `pws --help` for a list of available commands!", :red
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
@@ -1,3 +1,3 @@
1
1
  class PWS
2
- VERSION = '0.9.2'.dup
2
+ VERSION = '1.0.0.pre.1'
3
3
  end
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,'%') + "─┐\n" +
34
+ (" ┌── " + "info ".ljust(len-2,'') + "─┐\n" +
29
35
  " J-_-L │ " + s.homepage + " │\n" +
30
- " ├── " + "usage ".ljust(len-2,'%') + "─┤\n" +
31
- " │ " + "pws help".ljust(len,' ') + " │\n" +
32
- " └─" + '─'*len + "─┘").gsub('%', '─')
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.9.2
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-04-24 00:00:00.000000000 Z
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: aruba
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
- description: pws is a command-line password safe. Please run `pws help` for usage
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 │\n
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: '0'
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: