keystores 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +69 -0
- data/Rakefile +10 -0
- data/keystores.gemspec +26 -0
- data/lib/keystores.rb +5 -0
- data/lib/keystores/java_key_store.rb +355 -0
- data/lib/keystores/jks/encrypted_private_key_info.rb +49 -0
- data/lib/keystores/jks/key_protector.rb +191 -0
- data/lib/keystores/jks/pkcs8_key.rb +172 -0
- data/lib/keystores/keystore.rb +91 -0
- data/lib/keystores/version.rb +3 -0
- metadata +115 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
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,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
|
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: []
|