akero 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +10 -3
- data/Rakefile +4 -0
- data/benchmark/bm_rate.png +0 -0
- data/benchmark/bm_size.png +0 -0
- data/lib/akero.rb +18 -11
- data/lib/akero/benchmark.rb +25 -25
- data/lib/akero/version.rb +1 -1
- data/spec/akero_spec.rb +134 -129
- metadata +1 -7
data/README.md
CHANGED
@@ -4,13 +4,13 @@ Akero ([ἄγγελος](http://en.wiktionary.org/wiki/%F0%90%80%80%F0%90%80%90%
|
|
4
4
|
|
5
5
|
Under the hood Akero uses standard OpenSSL primitives. Each instance wraps a [RSA](http://en.wikipedia.org/wiki/RSA)-keypair, a corresponding [X.509 certificate](http://en.wikipedia.org/wiki/X.509) and exchanges self-signed messages ([PKCS#7](https://tools.ietf.org/html/rfc2315)) with other instances.
|
6
6
|
|
7
|
-
Akero does not try to be a substitute for a fully featured [PKI](http://en.wikipedia.org/wiki/Public_key_infrastructure). It is meant to be used as a building block in scenarios where trust-relationships and keyrings can be externally managed, and where the complexity of traditional solutions (X.509 PKI, OpenPGP,
|
7
|
+
Akero does not try to be a substitute for a fully featured [PKI](http://en.wikipedia.org/wiki/Public_key_infrastructure). It is meant to be used as a building block in scenarios where trust-relationships and keyrings can be externally managed, and where the complexity of traditional solutions (X.509 PKI, OpenPGP, custom RSA) yields no tangible benefits.
|
8
8
|
|
9
9
|
## Features
|
10
10
|
|
11
11
|
* Secure 1-to-n messaging (sign-only -or- sign->encrypt->sign)
|
12
|
-
* Low complexity; easy to use, understand and review (only
|
13
|
-
* Transport agnostic; messages and certificates are self-contained
|
12
|
+
* Low complexity; easy to use, understand and review (only 192 lines of code)
|
13
|
+
* Transport agnostic; messages and certificates are self-contained and optionally ascii-armored (base64)
|
14
14
|
* Built on standard OpenSSL primitives, no homegrown algorithms
|
15
15
|
* [100%](https://busyloop.net/oss/akero/coverage/) test coverage
|
16
16
|
|
@@ -64,6 +64,13 @@ File.open('/tmp/alice.akr', 'w') { |f| f.write(alice.private_key) }
|
|
64
64
|
# And load her again
|
65
65
|
new_alice = Akero.load(File.read('/tmp/alice.akr'))
|
66
66
|
|
67
|
+
# By default all messages are ascii armored.
|
68
|
+
# In production Alice disables the armoring
|
69
|
+
# for better performance.
|
70
|
+
signed_msg = alice.sign("Hello world!", false)
|
71
|
+
msg = alice.encrypt(alice.public_key, "Hello!", false)
|
72
|
+
puts alice.receive(msg).body # => "Hello!"
|
73
|
+
|
67
74
|
```
|
68
75
|
|
69
76
|
## Documentation
|
data/Rakefile
CHANGED
data/benchmark/bm_rate.png
CHANGED
Binary file
|
data/benchmark/bm_size.png
CHANGED
Binary file
|
data/lib/akero.rb
CHANGED
@@ -81,6 +81,7 @@ class Akero
|
|
81
81
|
PLATE_CRYPTED = ['PKCS7', 'AKERO SECRET MESSAGE'] # @private
|
82
82
|
|
83
83
|
DEFAULT_RSA_BITS = 2048
|
84
|
+
DEFAULT_DIGEST = OpenSSL::Digest::SHA512
|
84
85
|
|
85
86
|
# Unique fingerprint of this Akero keypair.
|
86
87
|
#
|
@@ -101,10 +102,10 @@ class Akero
|
|
101
102
|
# Akero.new(4096, OpenSSL::Digest::SHA512)
|
102
103
|
#
|
103
104
|
# @param [Integer] rsa_bits RSA key length
|
104
|
-
# @param [OpenSSL::Digest]
|
105
|
+
# @param [OpenSSL::Digest] digest Signature digest
|
105
106
|
# @return [Akero] New Akero instance
|
106
|
-
def initialize(rsa_bits=DEFAULT_RSA_BITS,
|
107
|
-
@key, @cert = generate_keypair(rsa_bits,
|
107
|
+
def initialize(rsa_bits=DEFAULT_RSA_BITS, digest=DEFAULT_DIGEST)
|
108
|
+
@key, @cert = generate_keypair(rsa_bits, digest) unless rsa_bits.nil?
|
108
109
|
end
|
109
110
|
|
110
111
|
# Load an Akero identity.
|
@@ -161,9 +162,11 @@ class Akero
|
|
161
162
|
# Sign a message.
|
162
163
|
#
|
163
164
|
# @param [String] plaintext The message to sign (binary safe)
|
165
|
+
# @param [Boolean] ascii_armor Convert the output in base64?
|
164
166
|
# @return [String] Akero signed message
|
165
|
-
def sign(plaintext)
|
166
|
-
|
167
|
+
def sign(plaintext, ascii_armor=true)
|
168
|
+
out = _sign(plaintext)
|
169
|
+
ascii_armor ? Akero.replate(out.to_s, Akero::PLATE_SIGNED) : out.to_der
|
167
170
|
end
|
168
171
|
|
169
172
|
# Sign->encrypt->sign a message for 1 or more recipients.
|
@@ -184,8 +187,9 @@ class Akero
|
|
184
187
|
#
|
185
188
|
# @param [Array] to Akero public keys of recipients
|
186
189
|
# @param [String] plaintext The message to encrypt (binary safe)
|
190
|
+
# @param [Boolean] ascii_armor Convert the output to base64?
|
187
191
|
# @return [String] Akero secret message
|
188
|
-
def encrypt(to, plaintext)
|
192
|
+
def encrypt(to, plaintext, ascii_armor=true)
|
189
193
|
to = [to] unless to.is_a? Array
|
190
194
|
to = to.map { |e|
|
191
195
|
case e
|
@@ -199,7 +203,8 @@ class Akero
|
|
199
203
|
raise RuntimeError, ERR_INVALID_RECIPIENT
|
200
204
|
end
|
201
205
|
}
|
202
|
-
|
206
|
+
out = _sign(_encrypt(to, _sign(plaintext, false)))
|
207
|
+
ascii_armor ? Akero.replate(out.to_s, PLATE_CRYPTED) : out.to_der
|
203
208
|
end
|
204
209
|
|
205
210
|
# Receive an Akero message.
|
@@ -207,7 +212,9 @@ class Akero
|
|
207
212
|
# @param [String] ciphertext Akero Message
|
208
213
|
# @return [Akero::Message] Message_body, signer_certificate, body_type
|
209
214
|
def receive(ciphertext)
|
210
|
-
ciphertext
|
215
|
+
if ciphertext.start_with? '-----BEGIN '
|
216
|
+
ciphertext = Akero.replate(ciphertext, Akero::PLATE_CRYPTED, true)
|
217
|
+
end
|
211
218
|
begin
|
212
219
|
body, signer_cert, body_type = verify(ciphertext, nil)
|
213
220
|
rescue ArgumentError
|
@@ -316,9 +323,9 @@ class Akero
|
|
316
323
|
# Generate new RSA keypair and certificate.
|
317
324
|
#
|
318
325
|
# @param [Integer] rsa_bits RSA key length
|
319
|
-
# @param [OpenSSL::Digest]
|
326
|
+
# @param [OpenSSL::Digest] digest Signature digest
|
320
327
|
# @return [Array] rsa_keypair, certificate
|
321
|
-
def generate_keypair(rsa_bits=DEFAULT_RSA_BITS,
|
328
|
+
def generate_keypair(rsa_bits=DEFAULT_RSA_BITS, digest=DEFAULT_DIGEST)
|
322
329
|
cn = "Akero #{Akero::VERSION}"
|
323
330
|
rsa = OpenSSL::PKey::RSA.new(rsa_bits)
|
324
331
|
|
@@ -342,7 +349,7 @@ class Akero
|
|
342
349
|
aki = ef.create_extension("authorityKeyIdentifier",
|
343
350
|
"keyid:always,issuer:always")
|
344
351
|
cert.add_extension(aki)
|
345
|
-
cert.sign(rsa,
|
352
|
+
cert.sign(rsa, digest.new)
|
346
353
|
[rsa, cert]
|
347
354
|
end
|
348
355
|
end
|
data/lib/akero/benchmark.rb
CHANGED
@@ -15,16 +15,16 @@ class Akero
|
|
15
15
|
puts "Running size benchmark..."
|
16
16
|
|
17
17
|
rnd = Random.new
|
18
|
-
msg_sizes = (8..
|
19
|
-
|
18
|
+
msg_sizes = (8..14).map{|x| 2**x}
|
19
|
+
modes = [[2048, false], [2048, true]]
|
20
20
|
results = {}
|
21
|
-
|
22
|
-
alice = Akero.new(
|
23
|
-
bob = Akero.new(
|
21
|
+
modes.each do |mode|
|
22
|
+
alice = Akero.new(mode[0])
|
23
|
+
bob = Akero.new(mode[0])
|
24
24
|
msg_sizes.each do |msize|
|
25
25
|
msg = rnd.bytes(msize)
|
26
|
-
ciphertext = alice.encrypt(bob.public_key, msg)
|
27
|
-
(results["ENCRYPT #{
|
26
|
+
ciphertext = alice.encrypt(bob.public_key, msg, mode[1])
|
27
|
+
(results["ENCRYPT #{mode[0]} bits #{mode[1] ? 'ascii_armor=1' : 'ascii_armor=0'}"] ||= []) << [msize, ciphertext.length / msg.length.to_f]
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -34,48 +34,48 @@ class Akero
|
|
34
34
|
def b_timing
|
35
35
|
puts "Running timing benchmark..."
|
36
36
|
|
37
|
-
msg_sizes = (
|
38
|
-
|
37
|
+
msg_sizes = (8..18).map{|x| 2**x}
|
38
|
+
modes = [[2048, false], [2048, true]]
|
39
39
|
|
40
40
|
rnd = Random.new
|
41
41
|
|
42
42
|
rounds = 50
|
43
43
|
results = []
|
44
|
-
|
45
|
-
results << B.enchmark("ENCRYPT #{
|
46
|
-
alice = Akero.new(
|
47
|
-
bob = Akero.new(
|
44
|
+
modes.each do |mode|
|
45
|
+
results << B.enchmark("ENCRYPT #{mode[0]} bits, ascii_armor=#{mode[1]?1:0}", :rounds => rounds, :compare => :mean) do
|
46
|
+
alice = Akero.new(mode[0])
|
47
|
+
bob = Akero.new(mode[0])
|
48
48
|
msg_sizes.each_with_index do |msize, i|
|
49
49
|
msg = rnd.bytes(msize)
|
50
50
|
job "msg_size #{msize}" do
|
51
|
-
alice.encrypt(bob.public_key, msg)
|
51
|
+
alice.encrypt(bob.public_key, msg, mode[1])
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
results << B.enchmark("
|
59
|
-
alice = Akero.new(
|
60
|
-
bob = Akero.new(
|
57
|
+
modes.each do |mode|
|
58
|
+
results << B.enchmark("DECRYPT #{mode[0]} bits, ascii_armor=#{mode[1]?1:0}", :rounds => rounds, :compare => :mean) do
|
59
|
+
alice = Akero.new(mode[0])
|
60
|
+
bob = Akero.new(mode[0])
|
61
61
|
msg_sizes.each_with_index do |msize, i|
|
62
62
|
msg = rnd.bytes(msize)
|
63
|
+
msg = alice.encrypt(bob.public_key, msg, mode[1])
|
63
64
|
job "msg_size #{msize}" do
|
64
|
-
|
65
|
+
bob.receive(msg)
|
65
66
|
end
|
66
67
|
end
|
67
68
|
end
|
68
69
|
end
|
69
70
|
|
70
|
-
|
71
|
-
results << B.enchmark("
|
72
|
-
alice = Akero.new(
|
73
|
-
bob = Akero.new(
|
71
|
+
modes.each do |mode|
|
72
|
+
results << B.enchmark("SIGN #{mode[0]} bits, ascii_armor=#{mode[1]?1:0}", :rounds => rounds, :compare => :mean) do
|
73
|
+
alice = Akero.new(mode[0])
|
74
|
+
bob = Akero.new(mode[0])
|
74
75
|
msg_sizes.each_with_index do |msize, i|
|
75
76
|
msg = rnd.bytes(msize)
|
76
|
-
msg = alice.encrypt(bob.public_key, msg)
|
77
77
|
job "msg_size #{msize}" do
|
78
|
-
|
78
|
+
alice.sign(msg, mode[1])
|
79
79
|
end
|
80
80
|
end
|
81
81
|
end
|
data/lib/akero/version.rb
CHANGED
data/spec/akero_spec.rb
CHANGED
@@ -56,145 +56,150 @@ describe Akero do
|
|
56
56
|
end
|
57
57
|
|
58
58
|
describe '#sign' do
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
59
|
+
([true, false]).each do |ascii_armor|
|
60
|
+
describe "ascii_armor=#{ascii_armor}" do
|
61
|
+
describe 'return value' do
|
62
|
+
if ascii_armor
|
63
|
+
it "is a String that looks like an Akero signed message" do
|
64
|
+
plaintext = "Hello world!"
|
65
|
+
signed_msg = subject.sign(plaintext, ascii_armor)
|
66
|
+
signed_msg.should be_a String
|
67
|
+
signed_msg.should match /^-----BEGIN #{Akero::PLATE_SIGNED[1]}-----\n/
|
68
|
+
signed_msg.should match /\n-----END #{Akero::PLATE_SIGNED[1]}-----\n$/
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "contains valid signature" do
|
73
|
+
plaintext = "Hello world!"
|
74
|
+
signed_msg = subject.sign(plaintext, ascii_armor)
|
75
|
+
bob = Akero.new
|
76
|
+
msg = bob.receive(signed_msg)
|
77
|
+
msg.from.should == subject.id
|
78
|
+
msg.from_pk.should == subject.public_key
|
79
|
+
msg.body.should == plaintext
|
80
|
+
msg.type.should == :signed
|
81
|
+
end
|
82
|
+
end
|
77
83
|
end
|
78
84
|
end
|
79
85
|
end
|
80
86
|
|
81
87
|
describe '#encrypt' do
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
it "raises RuntimeError when message is not String" do
|
117
|
-
lambda {
|
118
|
-
msg = "Hello world!"
|
119
|
-
ciphertext = subject.encrypt(subject.public_key, 42)
|
120
|
-
}.should raise_error RuntimeError, Akero::ERR_MSG_NOT_STRING_NOR_PKCS7
|
88
|
+
([true, false]).each do |ascii_armor|
|
89
|
+
describe "ascii_armor=#{ascii_armor}" do
|
90
|
+
describe 'return value' do
|
91
|
+
if ascii_armor
|
92
|
+
it "is a String that looks like an Akero secret message" do
|
93
|
+
plaintext = "Hello world!"
|
94
|
+
ciphertext = subject.encrypt(subject.public_key, plaintext, ascii_armor)
|
95
|
+
ciphertext.should be_a String
|
96
|
+
ciphertext.should match /^-----BEGIN #{Akero::PLATE_CRYPTED[1]}-----\n/
|
97
|
+
ciphertext.should match /\n-----END #{Akero::PLATE_CRYPTED[1]}-----\n$/
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "raises RuntimeError on invalid recipient (invalid public key)" do
|
102
|
+
lambda {
|
103
|
+
msg = "Hello world!"
|
104
|
+
ciphertext = subject.encrypt([subject.public_key, 'foo'], msg)
|
105
|
+
}.should raise_error RuntimeError, Akero::ERR_INVALID_RECIPIENT_CERT
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises RuntimeError on invalid recipient (wrong type)" do
|
109
|
+
lambda {
|
110
|
+
msg = "Hello world!"
|
111
|
+
ciphertext = subject.encrypt([subject.public_key, 42], msg)
|
112
|
+
}.should raise_error RuntimeError, Akero::ERR_INVALID_RECIPIENT
|
113
|
+
end
|
114
|
+
|
115
|
+
it "raises RuntimeError when message is not String" do
|
116
|
+
lambda {
|
117
|
+
msg = "Hello world!"
|
118
|
+
ciphertext = subject.encrypt(subject.public_key, 42)
|
119
|
+
}.should raise_error RuntimeError, Akero::ERR_MSG_NOT_STRING_NOR_PKCS7
|
120
|
+
end
|
121
|
+
end
|
121
122
|
end
|
122
123
|
end
|
123
124
|
end
|
124
125
|
|
125
126
|
describe '#receive' do
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
127
|
+
([true, false]).each do |ascii_armor|
|
128
|
+
describe "ascii_armor=#{ascii_armor}" do
|
129
|
+
it "decrypts message that was encrypted for self" do
|
130
|
+
plaintext = "Hello world!"
|
131
|
+
ciphertext = subject.encrypt(subject.public_key, plaintext, ascii_armor)
|
132
|
+
msg = subject.receive(ciphertext)
|
133
|
+
msg.body.should == plaintext
|
134
|
+
msg.type.should == :encrypted
|
135
|
+
end
|
136
|
+
|
137
|
+
it "decrypts message that was encrypted for self and other recipients" do
|
138
|
+
plaintext = "Hello world!"
|
139
|
+
alice = Akero.new
|
140
|
+
bob = Akero.new
|
141
|
+
ciphertext = subject.encrypt([alice.public_key, subject.public_key, bob.public_key], plaintext, ascii_armor)
|
142
|
+
msg = subject.receive(ciphertext)
|
143
|
+
msg.body.should == plaintext
|
144
|
+
msg.type.should == :encrypted
|
145
|
+
end
|
146
|
+
|
147
|
+
it "fails to decrypt message that was encrypted only for other recipients" do
|
148
|
+
lambda {
|
149
|
+
plaintext = "Hello world!"
|
150
|
+
alice = Akero.new
|
151
|
+
bob = Akero.new
|
152
|
+
ciphertext = subject.encrypt([alice.public_key, bob.public_key], plaintext, ascii_armor)
|
153
|
+
msg = subject.receive(ciphertext)
|
154
|
+
msg.body.should == plaintext
|
155
|
+
msg.type.should == :encrypted
|
156
|
+
}.should raise_error RuntimeError, Akero::ERR_DECRYPT
|
157
|
+
end
|
158
|
+
|
159
|
+
it "extracts signature from signed message" do
|
160
|
+
plaintext = "Hello world!"
|
161
|
+
alice = Akero.new
|
162
|
+
signed_msg = subject.sign(plaintext, ascii_armor)
|
163
|
+
msg = alice.receive(signed_msg)
|
164
|
+
msg.body.should == plaintext
|
165
|
+
msg.type.should == :signed
|
166
|
+
end
|
167
|
+
|
168
|
+
it "raises RuntimeError on invalid message" do
|
169
|
+
lambda {
|
170
|
+
subject.receive("foobar")
|
171
|
+
}.should raise_error RuntimeError #, Akero::ERR_MSG_MALFORMED_ENV
|
172
|
+
end
|
173
|
+
|
174
|
+
it "raises RuntimeError when payload does not match envelope signature" do
|
175
|
+
lambda {
|
176
|
+
oscar = Akero.new
|
177
|
+
raw_key = subject.send(:instance_variable_get, '@cert')
|
178
|
+
a = subject.send(:_encrypt, [raw_key], subject.send(:_sign, 'foobar'))
|
179
|
+
b = oscar.send(:_sign, a)
|
180
|
+
c = ascii_armor ? Akero.replate(b.to_s, Akero::PLATE_CRYPTED) : b.to_der
|
181
|
+
subject.receive(c)
|
182
|
+
}.should raise_error RuntimeError, Akero::ERR_MSG_CORRUPT_CERT
|
183
|
+
end
|
184
|
+
|
185
|
+
it "raises RuntimeError on malformed inner message" do
|
186
|
+
lambda {
|
187
|
+
key, cert = subject.send(:generate_keypair, 1024)
|
188
|
+
env = OpenSSL::PKCS7::sign(cert, key, 0xff.chr, [], OpenSSL::PKCS7::BINARY)
|
189
|
+
broken_msg = Akero.replate(env.to_s, Akero::PLATE_CRYPTED)
|
190
|
+
subject.receive(broken_msg)
|
191
|
+
}.should raise_error RuntimeError, Akero::ERR_MSG_MALFORMED_BODY
|
192
|
+
end
|
193
|
+
|
194
|
+
it "raises RuntimeError on unsigned message" do
|
195
|
+
lambda {
|
196
|
+
raw_key = subject.send(:instance_variable_get, '@cert')
|
197
|
+
env = OpenSSL::PKCS7::encrypt([raw_key], 'foobar', OpenSSL::Cipher::new("AES-256-CFB"), OpenSSL::PKCS7::BINARY)
|
198
|
+
broken_msg = Akero.replate(env.to_s, Akero::PLATE_CRYPTED)
|
199
|
+
subject.receive(broken_msg)
|
200
|
+
}.should raise_error RuntimeError, Akero::ERR_MSG_TOO_MANY_SIGNERS
|
201
|
+
end
|
202
|
+
end
|
198
203
|
end
|
199
204
|
end
|
200
205
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: akero
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -190,18 +190,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
190
190
|
- - ! '>='
|
191
191
|
- !ruby/object:Gem::Version
|
192
192
|
version: '0'
|
193
|
-
segments:
|
194
|
-
- 0
|
195
|
-
hash: 1654579781693310372
|
196
193
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
194
|
none: false
|
198
195
|
requirements:
|
199
196
|
- - ! '>='
|
200
197
|
- !ruby/object:Gem::Version
|
201
198
|
version: '0'
|
202
|
-
segments:
|
203
|
-
- 0
|
204
|
-
hash: 1654579781693310372
|
205
199
|
requirements: []
|
206
200
|
rubyforge_project:
|
207
201
|
rubygems_version: 1.8.23
|