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 +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: []
|