keystores 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0ecbd40c0a3022aeae1e8f6257e96160909008e5
4
+ data.tar.gz: c6d278eca9d05a4ca80c0cfa96274b6c3ddfcab4
5
+ SHA512:
6
+ metadata.gz: 16f5d801475e7d1e13c64648c73205fc02e7fd9364354f95e0c714dd94a7b8724169582e9fca2b01ffe492a696aa2534b0a213c9322c41191dc8557d963f9d8a
7
+ data.tar.gz: 67d510a0f27ef57792e6e6e1fda4dfcde58f5d70db3be421caaea5360dee55de39a396e1c3c097361534687275938074d48b9e7461599ffe7a06e819f0bd7454
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ before_install: gem install bundler -v 1.10.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in keystores.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ryan Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Keystores
2
+
3
+ This gem provides ruby implementations of different key stores. This was primarily created to provide the ability
4
+ to use many of the good Java key stores from ruby.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'keystores'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install keystores
21
+
22
+ ## Usage
23
+
24
+ The API for this gem is modeled after the Java `KeyStore` class. All of the `KeyStore` implementations provided by this
25
+ gem conform to the `Keystores::Keystore` interface.
26
+
27
+ The certificate and key objects that these keystores return and expect are `OpenSSL::X509::Certificate` and
28
+ `OpenSSL::PKey` objects, respectively.
29
+
30
+ ### Supported Key Store types
31
+
32
+ #### Java Key Store (jks) format
33
+
34
+ ##### Reading
35
+
36
+ This gem supports reading trusted certificate entries and private key entries. It can read
37
+ and decrypt RSA, DSA, and EC keys.
38
+
39
+ Example usage:
40
+
41
+ ```
42
+ require 'keystores/java_keystore'
43
+ keystore = Keystores::JavaKeystore.new
44
+
45
+ # Load can take any IO object, or a path to a file
46
+ key_store_password = 'keystores'
47
+ keystore.load('/tmp/keystore.jks', key_store_password)
48
+
49
+ certificate = keystore.get_certificate('my_certificate')
50
+ key = keystore.get_key('my_key', key_store_password)
51
+
52
+ certificate.check_private_key(key)
53
+
54
+ certificate_chain = keystore.get_certificate_chain('my_key')
55
+ ```
56
+
57
+ ##### Writing
58
+
59
+ This gem supports writing trusted certificate entries and private key entries. It currently supports
60
+ writing DSA, RSA, and EC private key entries.
61
+
62
+ ## Contributing
63
+
64
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rylarson/keystores.
65
+
66
+ ## License
67
+
68
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
69
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubygems/tasks'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
9
+ #This gives us build, install, and release
10
+ Gem::Tasks.new(:console => false, :sign => false)
data/keystores.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'keystores/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'keystores'
8
+ spec.version = Keystores::VERSION
9
+ spec.authors = ['Ryan Larson']
10
+ spec.email = ['ryan.mango.larson@gmail.com']
11
+
12
+ spec.summary = 'This gem allows applications to interact with different types of keystores'
13
+ spec.description = spec.summary
14
+ spec.homepage = 'https://github.com/rylarson/keystores'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec'
25
+ spec.add_development_dependency 'rubygems-tasks'
26
+ end
data/lib/keystores.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "keystores/version"
2
+
3
+ module Keystores
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,355 @@
1
+ require 'keystores/keystore'
2
+ require 'keystores/jks/key_protector'
3
+ require 'keystores/jks/encrypted_private_key_info'
4
+ require 'thread'
5
+ require 'openssl'
6
+ require 'date'
7
+
8
+ module Keystores
9
+ # An implementation of a Java Key Store (JKS) Format
10
+ class JavaKeystore < Keystore
11
+
12
+ TYPE = 'JKS'
13
+
14
+ # Defined by JavaKeyStore.java
15
+ MAGIC = 0xfeedfeed
16
+ VERSION_1 = 0x01
17
+ VERSION_2 = 0x02
18
+ KEY_ENTRY_TAG = 1
19
+ TRUSTED_CERTIFICATE_ENTRY_TAG = 2
20
+
21
+ def initialize
22
+ @entries = {}
23
+ @entries_mutex = Mutex.new
24
+ end
25
+
26
+ def aliases
27
+ @entries.keys
28
+ end
29
+
30
+ def contains_alias(aliaz)
31
+ @entries.has_key?(aliaz)
32
+ end
33
+
34
+ def delete_entry(aliaz)
35
+ @entries_mutex.synchronize { @entries.delete(aliaz) }
36
+ end
37
+
38
+ def get_certificate(aliaz)
39
+ entry = @entries[aliaz]
40
+ unless entry.nil?
41
+ if entry.is_a? TrustedCertificateEntry
42
+ entry.certificate
43
+ elsif entry.is_a? KeyEntry
44
+ entry.certificate_chain[0]
45
+ else
46
+ nil
47
+ end
48
+ end
49
+ end
50
+
51
+ def get_certificate_alias(certificate)
52
+ @entries.each do |aliaz, entry|
53
+ if entry.is_a? TrustedCertificateEntry
54
+ # We have to DER encode both of the certificates because OpenSSL::X509::Certificate doesn't implement equal?
55
+ return aliaz if certificate.to_der == entry.certificate.to_der
56
+ elsif entry.is_a? KeyEntry
57
+ # We have to DER encode both of the certificates because OpenSSL::X509::Certificate doesn't implement equal?
58
+ return aliaz if certificate.to_der == entry.certificate_chain[0].to_der
59
+ end
60
+ end
61
+ nil
62
+ end
63
+
64
+ def get_certificate_chain(aliaz)
65
+ entry = @entries[aliaz]
66
+ if !entry.nil? && entry.is_a?(KeyEntry)
67
+ entry.certificate_chain
68
+ else
69
+ nil
70
+ end
71
+ end
72
+
73
+ def get_key(aliaz, password)
74
+ entry = @entries[aliaz]
75
+
76
+ # This somewhat odd control flow mirrors the Java code for ease of porting
77
+ # TODO clean this up
78
+ if entry.nil? || !entry.is_a?(KeyEntry)
79
+ return nil
80
+ end
81
+
82
+ if password.nil?
83
+ raise IOError.new('Password must not be nil')
84
+ end
85
+
86
+ encrypted_private_key = entry.encrypted_private_key
87
+ encrypted_private_key_info = Keystores::Jks::EncryptedPrivateKeyInfo.new(:encoded => encrypted_private_key)
88
+ Keystores::Jks::KeyProtector.new(password).recover(encrypted_private_key_info)
89
+ end
90
+
91
+ def get_type
92
+ TYPE
93
+ end
94
+
95
+ def is_certificate_entry(aliaz)
96
+ !@entries[aliaz].nil? && @entries[aliaz].is_a?(TrustedCertificateEntry)
97
+ end
98
+
99
+ def is_key_entry(aliaz)
100
+ !@entries[aliaz].nil? && @entries[aliaz].is_a?(KeyEntry)
101
+ end
102
+
103
+ def load(key_store_file, password)
104
+ @entries_mutex.synchronize do
105
+ key_store_bytes = key_store_file.respond_to?(:read) ? key_store_file.read : IO.binread(key_store_file)
106
+ # We pass this Message Digest around and add all of the bytes we read to it so we can verify integrity
107
+ md = get_pre_keyed_hash(password)
108
+
109
+ magic = read_int!(key_store_bytes, md)
110
+ version = read_int!(key_store_bytes, md)
111
+
112
+ if magic != MAGIC || (version != VERSION_1 && version != VERSION_2)
113
+ raise IOError.new('Invalid keystore format')
114
+ end
115
+
116
+ count = read_int!(key_store_bytes, md)
117
+
118
+ count.times do
119
+ tag = read_int!(key_store_bytes, md)
120
+
121
+ if tag == KEY_ENTRY_TAG
122
+ key_entry = KeyEntry.new
123
+ aliaz = read_utf!(key_store_bytes, md)
124
+ time = read_long!(key_store_bytes, md)
125
+
126
+ key_entry.creation_date = time
127
+
128
+ private_key_length = read_int!(key_store_bytes, md)
129
+ encrypted_private_key = key_store_bytes.slice!(0..(private_key_length - 1))
130
+ md << encrypted_private_key
131
+
132
+ key_entry.encrypted_private_key = encrypted_private_key
133
+
134
+ number_of_certs = read_int!(key_store_bytes, md)
135
+
136
+ certificate_chain = []
137
+
138
+ number_of_certs.times do
139
+ certificate_chain << read_certificate(key_store_bytes, version, md)
140
+ end
141
+
142
+ key_entry.certificate_chain = certificate_chain
143
+ @entries[aliaz] = key_entry
144
+ elsif tag == TRUSTED_CERTIFICATE_ENTRY_TAG
145
+ trusted_cert_entry = TrustedCertificateEntry.new
146
+ aliaz = read_utf!(key_store_bytes, md)
147
+ time = read_long!(key_store_bytes, md)
148
+
149
+ trusted_cert_entry.creation_date = time
150
+ certificate = read_certificate(key_store_bytes, version, md)
151
+ trusted_cert_entry.certificate = certificate
152
+ @entries[aliaz] = trusted_cert_entry
153
+ else
154
+ raise IOError.new('Unrecognized keystore entry')
155
+ end
156
+ end
157
+
158
+ unless password.nil?
159
+ verify_key_store_integrity(key_store_bytes, md)
160
+ end
161
+ end
162
+ end
163
+
164
+ def set_certificate_entry(aliaz, certificate)
165
+ @entries_mutex.synchronize do
166
+ entry = @entries[aliaz]
167
+ if !entry.nil? && entry.is_a?(KeyEntry)
168
+ raise ArgumentError.new('Cannot overwrite own certificate')
169
+ end
170
+
171
+ entry = TrustedCertificateEntry.new
172
+ entry.certificate = certificate
173
+ # Java uses new Date().getTime() which returns milliseconds since epoch, so we do the same here with %Q
174
+ entry.creation_date = DateTime.now.strftime('%Q').to_i
175
+
176
+ @entries[aliaz] = entry
177
+ end
178
+ end
179
+
180
+ def set_key_entry(aliaz, key, certificate_chain, password=nil)
181
+ super
182
+ end
183
+
184
+ def size
185
+ @entries.size
186
+ end
187
+
188
+ def store(key_store_file, password)
189
+ @entries_mutex.synchronize do
190
+ # password is mandatory when storing
191
+ if password.nil?
192
+ raise ArgumentError.new("password can't be null")
193
+ end
194
+
195
+ md = get_pre_keyed_hash(password)
196
+
197
+ io = key_store_file.respond_to?(:write) ? key_store_file : File.open(key_store_file, 'w')
198
+
199
+ write_int(io, MAGIC, md)
200
+ # Always write the latest version
201
+ write_int(io, VERSION_2, md)
202
+ write_int(io, @entries.size, md)
203
+
204
+ @entries.each do |aliaz, entry|
205
+ if entry.is_a? KeyEntry
206
+ write_int(io, KEY_ENTRY_TAG, md)
207
+ write_utf(io, aliaz, md)
208
+ write_long(io, entry.creation_date, md)
209
+ write_int(io, entry.encrypted_private_key.length, md)
210
+ write(io, entry.encrypted_private_key, md)
211
+
212
+ certificate_chain = entry.certificate_chain
213
+ chain_length = certificate_chain.nil? ? 0 : certificate_chain.length
214
+
215
+ write_int(io, chain_length, md)
216
+
217
+ unless certificate_chain.nil?
218
+ certificate_chain.each { |certificate| write_certificate(io, certificate, md) }
219
+ end
220
+ elsif entry.is_a? TrustedCertificateEntry
221
+ write_int(io, TRUSTED_CERTIFICATE_ENTRY_TAG, md)
222
+ write_utf(io, aliaz, md)
223
+ write_long(io, entry.creation_date, md)
224
+ write_certificate(io, entry.certificate, md)
225
+ else
226
+ raise IOError.new('Unrecognized keystore entry')
227
+ end
228
+ end
229
+ # Write the keyed hash which is used to detect tampering with
230
+ # the keystore (such as deleting or modifying key or
231
+ # certificate entries).
232
+ io.write(md.digest)
233
+ io.flush
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def read_certificate(key_store_bytes, version, md)
240
+ # If we are a version 2 JKS, we check to see if we have the right certificate type
241
+ # Version 1 JKS format unconditionally assumed X509
242
+ if version == 2
243
+ cert_type = read_utf!(key_store_bytes, md)
244
+ if cert_type != 'X.509' && cert_type != 'X509'
245
+ raise IOError.new("Unrecognized certificate type: #{cert_type}")
246
+ end
247
+ end
248
+ certificate_length = read_int!(key_store_bytes, md)
249
+ certificate = key_store_bytes.slice!(0..(certificate_length - 1))
250
+ md << certificate
251
+ OpenSSL::X509::Certificate.new(certificate)
252
+ end
253
+
254
+ def write_certificate(file, certificate, md)
255
+ encoded = certificate.to_der
256
+ write_utf(file, 'X.509', md)
257
+ write_int(file, encoded.length, md)
258
+ write(file, encoded, md)
259
+ end
260
+
261
+ # Derive a key in the same goofy way that Java does
262
+ def get_pre_keyed_hash(password)
263
+ md = OpenSSL::Digest::SHA1.new
264
+ passwd_bytes = []
265
+ password.unpack('c*').each do |byte|
266
+ passwd_bytes << (byte >> 8)
267
+ passwd_bytes << byte
268
+ end
269
+ md << passwd_bytes.pack('c*')
270
+ md << 'Mighty Aphrodite'.force_encoding('UTF-8')
271
+ md
272
+ end
273
+
274
+ def verify_key_store_integrity(key_store_bytes, md)
275
+ # The remaining key store bytes are the password based hash
276
+ actual_hash = key_store_bytes
277
+ computed_hash = md.digest
278
+
279
+ # TODO, change how we compare these to defend against timing attacks even though JAVA doesn't
280
+ if actual_hash != computed_hash
281
+ raise IOError.new('Keystore was tampered with, or password was incorrect')
282
+ end
283
+ end
284
+
285
+ # Java uses DataInputStream#readInt() which is defined as reading 4 bytes and interpreting it as an int
286
+ def read_int!(bytes, md)
287
+ bytes = bytes.slice!(0..3)
288
+ md << bytes
289
+ bytes.unpack('N')[0]
290
+ end
291
+
292
+ # Java uses DataInputStream#readUnsignedShort() which is defined as reading 2 bytes and interpreting it as an int
293
+ def read_unsigned_short!(bytes, md)
294
+ bytes = bytes.slice!(0..1)
295
+ md << bytes
296
+ bytes.unpack('n')[0]
297
+ end
298
+
299
+ # Java uses DataInputStream#readUTF which does a bunch of crap to read a modified UTF-8 format
300
+ # TODO, this is a bit of a hack, but seems to work fine. We just assume we get a string out of the array
301
+ def read_utf!(bytes, md)
302
+ utf_length = read_unsigned_short!(bytes, md)
303
+ bytes = bytes.slice!(0..(utf_length - 1))
304
+ md << bytes
305
+ bytes
306
+ end
307
+
308
+ # Java uses DataInputStream#readLong which is defined as reading 8 bytes and interpreting it as a signed long
309
+ def read_long!(bytes, md)
310
+ bytes = bytes.slice!(0..7)
311
+ md << bytes
312
+ bytes.unpack('q>')[0]
313
+ end
314
+
315
+ # Java uses DataOutputStream#writeUTF to write the length + string
316
+ def write_utf(file, string, md)
317
+ write_short(file, string.length, md)
318
+ write(file, string, md)
319
+ end
320
+
321
+ # Java uses DataInputStream#writeInt() which writes a 32 bit integer
322
+ def write_int(file, int, md)
323
+ int = [int].pack('N')
324
+ md << int
325
+ file.write(int)
326
+ end
327
+
328
+ # Java uses DataInputStream#writeShort() which writes a 16 bit integer
329
+ def write_short(file, short, md)
330
+ short = [short].pack('n')
331
+ md << short
332
+ file.write(short)
333
+ end
334
+
335
+ # Java uses DataInputStream#writeLong which writes a 64 bit integer
336
+ def write_long(file, long, md)
337
+ long = [long].pack('q>')
338
+ md << long
339
+ file.write(long)
340
+ end
341
+
342
+ def write(file, bytes, md)
343
+ md << bytes
344
+ file.write(bytes)
345
+ end
346
+
347
+ class KeyEntry
348
+ attr_accessor :creation_date, :encrypted_private_key, :certificate_chain
349
+ end
350
+
351
+ class TrustedCertificateEntry
352
+ attr_accessor :creation_date, :certificate
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,49 @@
1
+ # This class implements the EncryptedPrivateKeyInfo type,
2
+ # which is defined in PKCS #8 as follows:
3
+ #
4
+ # EncryptedPrivateKeyInfo ::= SEQUENCE {
5
+ # encryptionAlgorithm AlgorithmIdentifier,
6
+ # encryptedData OCTET STRING }
7
+ #
8
+
9
+ require 'openssl'
10
+
11
+ module Keystores
12
+ module Jks
13
+ class EncryptedPrivateKeyInfo
14
+ attr_accessor :encrypted_data, :algorithm, :encoded
15
+
16
+ def initialize(opts = {})
17
+ # Parses from encoded private key
18
+ if opts.has_key?(:encoded)
19
+ encoded = opts[:encoded]
20
+ @asn1 = OpenSSL::ASN1.decode(encoded)
21
+ @encrypted_data = @asn1.value[1].value
22
+ @algorithm = @asn1.value[0].value[0].value
23
+ @encoded = encoded
24
+ else
25
+ @algorithm = opts[:algorithm]
26
+ @encrypted_data = opts[:encrypted_data]
27
+ @encoded = encode(@algorithm, @encrypted_data)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ # Java actually encodes:
34
+ #
35
+ # EncryptedPrivateKeyInfo ::= SEQUENCE {
36
+ # SEQUENCE {
37
+ # null,
38
+ # encryptionAlgorithm AlgorithmIdentifier},
39
+ # encryptedData OCTET STRING }
40
+ def encode(algorithm, encrypted_data)
41
+ a = OpenSSL::ASN1::ObjectId.new(algorithm)
42
+ null = OpenSSL::ASN1::Null.new(nil)
43
+ oid_sequence = OpenSSL::ASN1::Sequence.new([a, null])
44
+ d = OpenSSL::ASN1::OctetString.new(encrypted_data)
45
+ OpenSSL::ASN1::Sequence.new([oid_sequence, d]).to_der
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,191 @@
1
+ require 'openssl'
2
+ require 'securerandom'
3
+ require 'keystores/jks/pkcs8_key'
4
+ require 'keystores/jks/encrypted_private_key_info'
5
+
6
+ # This is an implementation of a Sun proprietary, exportable algorithm
7
+ # intended for use when protecting (or recovering the cleartext version of)
8
+ # sensitive keys.
9
+ # This algorithm is not intended as a general purpose cipher.
10
+ #
11
+ # This is how the algorithm works for key protection:
12
+ # p - user password
13
+ # s - random salt
14
+ # X - xor key
15
+ # P - to-be-protected key
16
+ # Y - protected key
17
+ # R - what gets stored in the keystore
18
+ #
19
+ # Step 1:
20
+ # Take the user's password, append a random salt (of fixed size) to it,
21
+ # and hash it: d1 = digest(p, s)
22
+ # Store d1 in X.
23
+ #
24
+ # Step 2:
25
+ # Take the user's password, append the digest result from the previous step,
26
+ # and hash it: dn = digest(p, dn-1).
27
+ # Store dn in X (append it to the previously stored digests).
28
+ # Repeat this step until the length of X matches the length of the private key P.
29
+ #
30
+ # Step 3:
31
+ # XOR X and P, and store the result in Y: Y = X XOR P.
32
+ #
33
+ # Step 4:
34
+ # Store s, Y, and digest(p, P) in the result buffer R:
35
+ # R = s + Y + digest(p, P), where "+" denotes concatenation.
36
+ # (NOTE: digest(p, P) is stored in the result buffer, so that when the key is
37
+ # recovered, we can check if the recovered key indeed matches the original
38
+ # key.) R is stored in the keystore.
39
+ #
40
+ # The protected key is recovered as follows:
41
+ #
42
+ # Step1 and Step2 are the same as above, except that the salt is not randomly
43
+ # generated, but taken from the result R of step 4 (the first length(s)
44
+ # bytes).
45
+ #
46
+ # Step 3 (XOR operation) yields the plaintext key.
47
+ #
48
+ # Then concatenate the password with the recovered key, and compare with the
49
+ # last length(digest(p, P)) bytes of R. If they match, the recovered key is
50
+ # indeed the same key as the original key.
51
+
52
+ module Keystores
53
+ module Jks
54
+ class KeyProtector
55
+ SALT_LEN = 20
56
+ DIGEST_LEN = 20
57
+ KEY_PROTECTOR_OID = '1.3.6.1.4.1.42.2.17.1.1'
58
+
59
+ def initialize(password)
60
+ @password = password
61
+ @passwd_bytes = []
62
+ password.unpack('c*').each do |byte|
63
+ @passwd_bytes << (byte >> 8)
64
+ @passwd_bytes << byte
65
+ end
66
+ @message_digest = OpenSSL::Digest::SHA1.new
67
+ end
68
+
69
+ def protect(key)
70
+ if key.nil?
71
+ raise ArgumentError.new("plaintext key can't be null")
72
+ end
73
+
74
+ plain_key = key.to_pkcs8_der.unpack('c*')
75
+
76
+ # Determine the number of digest rounds
77
+ num_rounds = plain_key.length / DIGEST_LEN
78
+ num_rounds += 1 if (plain_key.length % DIGEST_LEN) != 0
79
+
80
+ salt = SecureRandom.random_bytes(SALT_LEN)
81
+ xor_key = Array.new(plain_key.length, 0)
82
+
83
+ xor_offset = 0
84
+ digest = salt
85
+
86
+ # Compute the digests, and store them in xor_key
87
+ for i in 1..num_rounds
88
+ @message_digest.update(@passwd_bytes.pack('c*'))
89
+ @message_digest.update(digest)
90
+ digest = @message_digest.digest
91
+ @message_digest.reset
92
+
93
+ if i < num_rounds
94
+ xor_key[xor_offset..(digest.length + xor_offset -1)] = digest.bytes
95
+ else
96
+ xor_key[xor_offset..-1] = digest[0..(xor_key.length - xor_offset - 1)].bytes
97
+ end
98
+ xor_offset += DIGEST_LEN
99
+ end
100
+
101
+ # XOR plain_key with xor_key, and store the result in tmpKey
102
+ tmp_key = []
103
+ for i in 0..(plain_key.length - 1)
104
+ tmp_key[i] = plain_key[i] ^ xor_key[i]
105
+ end
106
+
107
+ # Store salt and tmp_key in encr_key
108
+ encr_key = salt.unpack('c*') + tmp_key
109
+
110
+ # Append digest(password, plain_key) as an integrity check to encr_key
111
+ @message_digest << @passwd_bytes.pack('c*')
112
+ @passwd_bytes.fill(0)
113
+ @passwd_bytes = nil
114
+ @message_digest << plain_key.pack('c*')
115
+ digest = @message_digest.digest
116
+ @message_digest.reset
117
+
118
+ encr_key += digest.unpack('c*')
119
+ Keystores::Jks::EncryptedPrivateKeyInfo.new(:algorithm => KEY_PROTECTOR_OID,
120
+ :encrypted_data => encr_key.pack('c*')).encoded
121
+ end
122
+
123
+ def recover(encrypted_private_key_info)
124
+ unless encrypted_private_key_info.algorithm == KEY_PROTECTOR_OID
125
+ raise IOError.new("Unsupported key protection algorithm: #{encrypted_private_key_info.algorithm}")
126
+ end
127
+
128
+ protected_key = encrypted_private_key_info.encrypted_data
129
+
130
+ # Get the salt associated with this key (the first SALT_LEN bytes of protected_key)
131
+ salt = protected_key.slice(0..(SALT_LEN - 1))
132
+
133
+ # Determine the number of digest rounds
134
+ encr_key_len = protected_key.length - SALT_LEN - DIGEST_LEN
135
+ num_rounds = encr_key_len / DIGEST_LEN
136
+ num_rounds += 1 if (encr_key_len % DIGEST_LEN) != 0
137
+
138
+ # Get the encrypted key portion
139
+ encr_key = protected_key.slice(SALT_LEN..(encr_key_len + SALT_LEN - 1))
140
+
141
+ xor_key = Array.new(encr_key.size, 0)
142
+ xor_offset = 0
143
+
144
+ digest = salt
145
+ # Compute the digests, and store them in xor_key
146
+ for i in 1..num_rounds
147
+ @message_digest.update(@passwd_bytes.pack('c*'))
148
+ @message_digest.update(digest)
149
+ digest = @message_digest.digest
150
+ @message_digest.reset
151
+
152
+ if i < num_rounds
153
+ xor_key[xor_offset..(digest.length + xor_offset -1)] = digest.bytes
154
+ else
155
+ xor_key[xor_offset..-1] = digest[0..(xor_key.length - xor_offset - 1)].bytes
156
+ end
157
+ xor_offset += DIGEST_LEN
158
+ end
159
+
160
+ # XOR encr_key with xor_key, and store the result in plain_key
161
+ plain_key = []
162
+ encr_key_unpacked = encr_key.bytes
163
+
164
+ for i in 0..(encr_key.size - 1)
165
+ plain_key[i] = encr_key_unpacked[i] ^ xor_key[i]
166
+ end
167
+
168
+ # Check the integrity of the recovered key by concatenating it with
169
+ # the password, digesting the concatenation, and comparing the
170
+ # result of the digest operation with the digest provided at the end
171
+ # of protected_key. If the two digest values are
172
+ # * different, raise an error.
173
+ @message_digest << @passwd_bytes.pack('c*')
174
+ @passwd_bytes.fill(0)
175
+ @passwd_bytes = nil
176
+ @message_digest << plain_key.pack('c*')
177
+ digest = @message_digest.digest
178
+ @message_digest.reset
179
+
180
+ for i in 0..(digest.length - 1)
181
+ if digest[i] != protected_key[SALT_LEN + encr_key_len + i]
182
+ raise IOError.new('Cannot recover key')
183
+ end
184
+ end
185
+ OpenSSL::PKey.pkcs8_parse(plain_key.pack('c*'))
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+
@@ -0,0 +1,172 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module OpenSSL
5
+ module PKey
6
+ class EC
7
+ original_initialize = instance_method(:initialize)
8
+
9
+ define_method(:initialize) do |der_or_pem|
10
+ init = original_initialize.bind(self)
11
+ begin
12
+ init.(der_or_pem)
13
+ rescue Exception
14
+ # If we blow up trying to parse the key, we might be der encoded PKCS8, and if we are, convert ourselves
15
+ # to PEM and try again.
16
+ init.(OpenSSL::PKey.der_to_pem(der_or_pem))
17
+ end
18
+ end
19
+
20
+ def to_pkcs8
21
+ integer = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new('0'))
22
+ oid = OpenSSL::ASN1::ObjectId.new('id-ecPublicKey')
23
+ curve_name = OpenSSL::ASN1::ObjectId.new(self.group.curve_name)
24
+ sequence = OpenSSL::ASN1::Sequence.new([oid, curve_name])
25
+ octet_string = OpenSSL::ASN1::OctetString.new(encode_private_key.to_der)
26
+ OpenSSL::ASN1::Sequence.new([integer, sequence, octet_string])
27
+ end
28
+
29
+ def to_pkcs8_der
30
+ to_pkcs8.to_der
31
+ end
32
+
33
+ def to_pkcs8_pem
34
+ to_pkcs8.to_pem
35
+ end
36
+
37
+ private
38
+
39
+ # ASN.1 syntax for EC private keys from SEC 1 v1.5 (draft):
40
+ #
41
+ # ECPrivateKey ::= SEQUENCE {
42
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
43
+ # privateKey OCTET STRING,
44
+ # parameters [0] ECDomainParameters {{ SECGCurveNames }} OPTIONAL,
45
+ # publicKey [1] BIT STRING OPTIONAL
46
+ # }
47
+ #
48
+ # We currently ignore the optional parameters and publicKey fields.
49
+ # We encode the parameters are as part of the curve name,
50
+ # not in the private key structure.We do this because Java expects things
51
+ # to be encoded this way
52
+ def encode_private_key
53
+ version = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new('1'))
54
+ # The private key is stored as the twos complement binary representation
55
+ priv_key = OpenSSL::ASN1::OctetString(private_key.to_s(2))
56
+ OpenSSL::ASN1::Sequence.new([version, priv_key])
57
+ end
58
+ end
59
+
60
+ class RSA
61
+ original_initialize = instance_method(:initialize)
62
+
63
+ define_method(:initialize) do |der_or_pem|
64
+ init = original_initialize.bind(self)
65
+ begin
66
+ init.(der_or_pem)
67
+ rescue Exception
68
+ # If we blow up trying to parse the key, we might be der encoded PKCS8, and if we are, convert ourselves
69
+ # to PEM and try again.
70
+ init.(OpenSSL::PKey.der_to_pem(der_or_pem))
71
+ end
72
+ end
73
+
74
+ def to_pkcs8
75
+ integer = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new('0'))
76
+ oid = OpenSSL::ASN1::ObjectId.new('rsaEncryption')
77
+ sequence = OpenSSL::ASN1::Sequence.new([oid, OpenSSL::ASN1::Null.new(nil)])
78
+
79
+ params = self.params
80
+ version = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new('0'))
81
+ n = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['n']))
82
+ e = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['e']))
83
+ d = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['d']))
84
+ p = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['p']))
85
+ q = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['q']))
86
+ dmp1 = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['dmp1']))
87
+ dmq1 = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['dmq1']))
88
+ iqmp = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['iqmp']))
89
+
90
+ params_sequence = OpenSSL::ASN1::Sequence.new([version, n, e, d, p, q, dmp1, dmq1, iqmp])
91
+
92
+ octet_string = OpenSSL::ASN1::OctetString.new(params_sequence.to_der)
93
+ OpenSSL::ASN1::Sequence.new([integer, sequence, octet_string])
94
+ end
95
+
96
+ def to_pkcs8_der
97
+ to_pkcs8.to_der
98
+ end
99
+
100
+ def to_pkcs8_pem
101
+ to_pkcs8.to_pem
102
+ end
103
+ end
104
+
105
+ class DSA
106
+ original_initialize = instance_method(:initialize)
107
+
108
+ define_method(:initialize) do |der_or_pem|
109
+ init = original_initialize.bind(self)
110
+ begin
111
+ init.(der_or_pem)
112
+ rescue Exception
113
+ # If we blow up trying to parse the key, we might be der encoded PKCS8, and if we are, convert ourselves
114
+ # to PEM and try again.
115
+ init.(OpenSSL::PKey.der_to_pem(der_or_pem))
116
+ end
117
+ end
118
+
119
+ def to_pkcs8
120
+ params = self.params
121
+ integer = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new('0'))
122
+ oid = OpenSSL::ASN1::ObjectId.new('DSA')
123
+ p = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['p']))
124
+ q = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['q']))
125
+ g = OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['g']))
126
+ param_sequence = OpenSSL::ASN1::Sequence.new([p, q, g])
127
+ sequence = OpenSSL::ASN1::Sequence.new([oid, param_sequence])
128
+ octet_string = OpenSSL::ASN1::OctetString.new(OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(params['priv_key'])).to_der)
129
+ OpenSSL::ASN1::Sequence.new([integer, sequence, octet_string])
130
+ end
131
+
132
+ def to_pkcs8_der
133
+ to_pkcs8.to_der
134
+ end
135
+
136
+ def to_pkcs8_pem
137
+ to_pkcs8.to_pem
138
+ end
139
+ end
140
+
141
+ # Parse the correct type of OpenSSL::PKey from a der encoded PKCS8 private key
142
+ def self.pkcs8_parse(der_bytes)
143
+ key_type = extract_key_type(der_bytes)
144
+ # pem = der_to_pem(der_bytes)
145
+ OpenSSL::PKey.const_get(key_type).new(der_bytes)
146
+ end
147
+
148
+ private
149
+
150
+ def self.extract_key_type(der_bytes)
151
+ asn1 = OpenSSL::ASN1.decode(der_bytes)
152
+ algorithm = asn1.value[1].value[0].value.downcase
153
+ if algorithm.include? 'rsa'
154
+ 'RSA'
155
+ elsif algorithm.include? 'ec'
156
+ 'EC'
157
+ elsif algorithm.include? 'dsa'
158
+ 'DSA'
159
+ end
160
+ end
161
+
162
+ def self.der_to_pem(der)
163
+ box(Base64.strict_encode64(der).scan(/.{1,64}/))
164
+ end
165
+
166
+ def self.box(lines)
167
+ lines.unshift '-----BEGIN PRIVATE KEY-----'
168
+ lines.push '-----END PRIVATE KEY-----'
169
+ lines.join("\n")
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,91 @@
1
+ module Keystores
2
+ class Keystore
3
+
4
+ @@registry = {}
5
+
6
+ # Get an instance of a key store, given a key store algorithm string
7
+ def self.get_instance(key_store_algorithm)
8
+ @@registry[key_store_algorithm].new
9
+ end
10
+
11
+ # Register your key store algorithm
12
+ def self.register_algorithm(algorithm, clazz)
13
+ @@registry[algorithm] = clazz
14
+ end
15
+
16
+ # Lists all the alias names of this keystore.
17
+ def aliases
18
+
19
+ end
20
+
21
+ # Checks if the given alias exists in this keystore.
22
+ def contains_alias(aliaz)
23
+
24
+ end
25
+
26
+ # Deletes the entry identified by the given alias from this keystore.
27
+ def delete_entry(aliaz)
28
+
29
+ end
30
+
31
+ # Returns the certificate associated with the given alias.
32
+ def get_certificate(aliaz)
33
+
34
+ end
35
+
36
+ # Returns the (alias) name of the first keystore entry whose certificate matches the given certificate.
37
+ def get_certificate_alias(certificate)
38
+
39
+ end
40
+
41
+ # Returns the certificate chain associated with the given alias.
42
+ def get_certificate_chain(aliaz)
43
+
44
+ end
45
+
46
+ # Returns the key associated with the given alias, using the given password to recover it.
47
+ def get_key(aliaz, password)
48
+
49
+ end
50
+
51
+ # Returns the type of this keystore.
52
+ def get_type
53
+
54
+ end
55
+
56
+ # Returns true if the entry identified by the given alias was created by a call to #set_certificate_entry
57
+ def is_certificate_entry(aliaz)
58
+
59
+ end
60
+
61
+ # Returns true if the entry identified by the given alias was created by a call to #set_key_entry
62
+ def is_key_entry(aliaz)
63
+
64
+ end
65
+
66
+ # Loads this Keystore from the given path.
67
+ def load(key_store_file, password)
68
+
69
+ end
70
+
71
+ # Stores this keystore to the given path, and protects its integrity with the given password.
72
+ def store(key_store_file, password)
73
+
74
+ end
75
+
76
+ # Assigns the given trusted certificate to the given alias.
77
+ def set_certificate_entry(aliaz, certificate)
78
+
79
+ end
80
+
81
+ # Assigns the given key to the given alias. If password is nil, it is assumed that the key is already protected
82
+ def set_key_entry(aliaz, key, certificate_chain, password = nil)
83
+
84
+ end
85
+
86
+ # Retrieves the number of entries in this keystore.
87
+ def size
88
+
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Keystores
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keystores
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Larson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubygems-tasks
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: This gem allows applications to interact with different types of keystores
70
+ email:
71
+ - ryan.mango.larson@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rspec
78
+ - .travis.yml
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - keystores.gemspec
84
+ - lib/keystores.rb
85
+ - lib/keystores/java_key_store.rb
86
+ - lib/keystores/jks/encrypted_private_key_info.rb
87
+ - lib/keystores/jks/key_protector.rb
88
+ - lib/keystores/jks/pkcs8_key.rb
89
+ - lib/keystores/keystore.rb
90
+ - lib/keystores/version.rb
91
+ homepage: https://github.com/rylarson/keystores
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.4.8
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: This gem allows applications to interact with different types of keystores
115
+ test_files: []