keystores 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []