ccrypto-java 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/.java-version +1 -0
- data/.rspec +3 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +94 -0
- data/README.md +150 -0
- data/Rakefile +10 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/ccrypto-java.gemspec +44 -0
- data/jars/bcmail-jdk15on-165.jar +0 -0
- data/jars/bcpg-jdk15on-165.jar +0 -0
- data/jars/bcpkix-jdk15on-165.jar +0 -0
- data/jars/bcprov-ext-jdk15on-165.jar +0 -0
- data/jars/bcprov-jdk15on-165.jar +0 -0
- data/jars/bctls-jdk15on-165.jar +0 -0
- data/jars/shamir-0.6.1-p.jar +0 -0
- data/lib/ccrypto/java/data_conversion.rb +80 -0
- data/lib/ccrypto/java/engines/asn1_engine.rb +161 -0
- data/lib/ccrypto/java/engines/asn1_object.rb +12 -0
- data/lib/ccrypto/java/engines/cipher_engine.rb +255 -0
- data/lib/ccrypto/java/engines/compression_engine.rb +92 -0
- data/lib/ccrypto/java/engines/data_conversion_engine.rb +9 -0
- data/lib/ccrypto/java/engines/decompression_engine.rb +48 -0
- data/lib/ccrypto/java/engines/digest_engine.rb +208 -0
- data/lib/ccrypto/java/engines/ecc_engine.rb +263 -0
- data/lib/ccrypto/java/engines/hkdf_engine.rb +72 -0
- data/lib/ccrypto/java/engines/hmac_engine.rb +75 -0
- data/lib/ccrypto/java/engines/pbkdf2_engine.rb +87 -0
- data/lib/ccrypto/java/engines/pkcs7_engine.rb +558 -0
- data/lib/ccrypto/java/engines/rsa_engine.rb +572 -0
- data/lib/ccrypto/java/engines/scrypt_engine.rb +35 -0
- data/lib/ccrypto/java/engines/secret_key_engine.rb +44 -0
- data/lib/ccrypto/java/engines/secret_sharing_engine.rb +59 -0
- data/lib/ccrypto/java/engines/secure_random_engine.rb +76 -0
- data/lib/ccrypto/java/engines/x509_engine.rb +311 -0
- data/lib/ccrypto/java/ext/secret_key.rb +75 -0
- data/lib/ccrypto/java/ext/x509_cert.rb +48 -0
- data/lib/ccrypto/java/jce_provider.rb +52 -0
- data/lib/ccrypto/java/keybundle_store/pkcs12.rb +125 -0
- data/lib/ccrypto/java/utils/comparator.rb +20 -0
- data/lib/ccrypto/java/utils/memory_buffer.rb +77 -0
- data/lib/ccrypto/java/utils/native_helper.rb +19 -0
- data/lib/ccrypto/java/version.rb +7 -0
- data/lib/ccrypto/java.rb +30 -0
- data/lib/ccrypto/provider.rb +132 -0
- metadata +144 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
|
2
|
+
require_relative '../data_conversion'
|
3
|
+
|
4
|
+
require_relative '../keybundle_store/pkcs12'
|
5
|
+
|
6
|
+
module Ccrypto
|
7
|
+
module Java
|
8
|
+
|
9
|
+
class ECCPublicKey < Ccrypto::ECCPublicKey
|
10
|
+
include DataConversion
|
11
|
+
|
12
|
+
def to_bin
|
13
|
+
@native_pubKey.encoded
|
14
|
+
end
|
15
|
+
|
16
|
+
def encoded
|
17
|
+
to_bin
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.to_key(bin)
|
21
|
+
bin = to_java_bytes(bin) if not bin.is_a?(::Java::byte[])
|
22
|
+
pubKey = java.security.KeyFactory.getInstance("ECDSA", "BC").generatePublic(java.security.spec.X509EncodedKeySpec.new(bin))
|
23
|
+
ECCPublicKey.new(pubKey)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class ECCKeyBundle
|
29
|
+
include Ccrypto::ECCKeyBundle
|
30
|
+
include TR::CondUtils
|
31
|
+
include DataConversion
|
32
|
+
|
33
|
+
include PKCS12
|
34
|
+
|
35
|
+
include TeLogger::TeLogHelper
|
36
|
+
|
37
|
+
teLogger_tag :j_ecc_keybundle
|
38
|
+
|
39
|
+
def initialize(kp)
|
40
|
+
@keypair = kp
|
41
|
+
end
|
42
|
+
|
43
|
+
def native_keypair
|
44
|
+
@keypair
|
45
|
+
end
|
46
|
+
|
47
|
+
def public_key
|
48
|
+
if @pubKey.nil?
|
49
|
+
@pubKey = ECCPublicKey.new(@keypair.public)
|
50
|
+
end
|
51
|
+
@pubKey
|
52
|
+
end
|
53
|
+
|
54
|
+
def private_key
|
55
|
+
ECCPrivateKey.new(@keypair.private)
|
56
|
+
end
|
57
|
+
|
58
|
+
def derive_dh_shared_secret(pubKey, &block)
|
59
|
+
|
60
|
+
JCEProvider.instance.add_bc_provider
|
61
|
+
|
62
|
+
ka = javax.crypto.KeyAgreement.getInstance("ECDH", JCEProvider::DEFProv)
|
63
|
+
ka.init(@keypair.private)
|
64
|
+
|
65
|
+
case pubKey
|
66
|
+
when ECCPublicKey
|
67
|
+
pub = pubKey.native_pubKey
|
68
|
+
when java.security.PublicKey
|
69
|
+
pub = pubKey
|
70
|
+
else
|
71
|
+
raise KeypairEngineException, "Unsupported public key type #{pubKey.class}"
|
72
|
+
end
|
73
|
+
ka.doPhase(pub, true)
|
74
|
+
#ka.doPhase(pubKey.native_pubKey, true)
|
75
|
+
if block
|
76
|
+
keyType = block.call(:keytype)
|
77
|
+
else
|
78
|
+
keyType = "AES"
|
79
|
+
end
|
80
|
+
keyType = "AES" if is_empty?(keyType)
|
81
|
+
teLogger.debug "Generate secret key type #{keyType}"
|
82
|
+
ka.generateSecret(keyType).encoded
|
83
|
+
end
|
84
|
+
|
85
|
+
def is_public_key_equal?(pubKey)
|
86
|
+
@keypair.public.encoded == pubKey.encoded
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_storage(type, &block)
|
90
|
+
|
91
|
+
case type
|
92
|
+
when :p12, :pkcs12
|
93
|
+
to_pkcs12 do |key|
|
94
|
+
case key
|
95
|
+
when :keypair
|
96
|
+
@keypair
|
97
|
+
else
|
98
|
+
block.call(key) if block
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
when :jks
|
103
|
+
to_pkcs12 do |key|
|
104
|
+
case key
|
105
|
+
when :storeType
|
106
|
+
"JKS"
|
107
|
+
when :keypair
|
108
|
+
@keypair
|
109
|
+
else
|
110
|
+
block.call(key) if key
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
when :pem
|
115
|
+
|
116
|
+
header = "-----BEGIN EC PRIVATE KEY-----\n"
|
117
|
+
footer = "\n-----END EC PRIVATE KEY-----"
|
118
|
+
|
119
|
+
out = StringIO.new
|
120
|
+
out.write header
|
121
|
+
out.write to_b64_mime(@keypair.private.encoded)
|
122
|
+
out.write footer
|
123
|
+
|
124
|
+
out.string
|
125
|
+
|
126
|
+
else
|
127
|
+
raise KeypairEngineException, "Unknown storage type #{type}"
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.from_storage(bin, &block)
|
133
|
+
|
134
|
+
if is_pem?(bin)
|
135
|
+
else
|
136
|
+
from_pkcs12(bin, &block)
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.is_pem?(bin)
|
142
|
+
begin
|
143
|
+
(bin =~ /BEGIN/) != nil
|
144
|
+
rescue ArgumentError => ex
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def equal?(kp)
|
150
|
+
case kp
|
151
|
+
when Ccrypto::ECCKeyBundle
|
152
|
+
@keypair.encoded == kp.private.encoded
|
153
|
+
else
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def method_missing(mtd, *args, &block)
|
159
|
+
teLogger.debug "Sending to native #{mtd}"
|
160
|
+
@keypair.send(mtd, *args, &block)
|
161
|
+
end
|
162
|
+
|
163
|
+
def respond_to_missing?(mtd, incPriv = false)
|
164
|
+
teLogger.debug "Respond to missing #{mtd}"
|
165
|
+
@keypair.respond_to?(mtd)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
class ECCEngine
|
171
|
+
include TR::CondUtils
|
172
|
+
include DataConversion
|
173
|
+
|
174
|
+
include TeLogger::TeLogHelper
|
175
|
+
teLogger_tag :j_ecc
|
176
|
+
|
177
|
+
def self.supported_curves
|
178
|
+
if @curves.nil?
|
179
|
+
@curves = org.bouncycastle.asn1.x9.ECNamedCurveTable.getNames.sort.to_a.map { |c| Ccrypto::ECCConfig.new(c) }
|
180
|
+
end
|
181
|
+
@curves
|
182
|
+
end
|
183
|
+
|
184
|
+
def initialize(*args,&block)
|
185
|
+
@config = args.first
|
186
|
+
raise KeypairEngineException, "1st parameter must be a #{Ccrypto::KeypairConfig.class} object" if not @config.is_a?(Ccrypto::KeypairConfig)
|
187
|
+
end
|
188
|
+
|
189
|
+
def generate_keypair(&block)
|
190
|
+
|
191
|
+
algoName = "ECDSA"
|
192
|
+
prov = Ccrypto::Java::JCEProvider::BCProv
|
193
|
+
randomEngine = java.security.SecureRandom.new
|
194
|
+
if block
|
195
|
+
# it is the responsibility of caller program to add the
|
196
|
+
# provider into the provider list.
|
197
|
+
# Here provider string shall be used
|
198
|
+
uprov = block.call(:jce_provider)
|
199
|
+
prov = uprov if not is_empty?(uprov)
|
200
|
+
|
201
|
+
uAlgo = block.call(:jce_algo_name)
|
202
|
+
algoName = uAlgo if not is_empty?(uAlgo)
|
203
|
+
|
204
|
+
uRandEng = block.call(:random_engine)
|
205
|
+
randomEngine = uRandEng if not uRandEng.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
kpg = java.security.KeyPairGenerator.getInstance(algoName, prov)
|
209
|
+
#kpg.java_send :initialize, [java.security.spec.AlgorithmParameterSpec, java.security.SecureRandom], java.security.spec.ECGenParameterSpec.new(curve), java.security.SecureRandom.new
|
210
|
+
kpg.java_send :initialize, [java.security.spec.AlgorithmParameterSpec, randomEngine.class], java.security.spec.ECGenParameterSpec.new(@config.curve), randomEngine
|
211
|
+
kp = kpg.generate_key_pair
|
212
|
+
|
213
|
+
kb = ECCKeyBundle.new(kp)
|
214
|
+
kb
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
def regenerate_keypair(pubKey, privKey, &block)
|
219
|
+
kp = java.security.KeyPair.new(pubKey, privKey)
|
220
|
+
ECCKeyBundle.new(kp)
|
221
|
+
end
|
222
|
+
|
223
|
+
def sign(val, &block)
|
224
|
+
raise KeypairEngineException, "Keypair is required" if @config.keypair.nil?
|
225
|
+
raise KeypairEngineException, "ECC keypair is required. Given #{@config.keypair}" if not @config.keypair.is_a?(ECCKeyBundle)
|
226
|
+
kp = @config.keypair
|
227
|
+
|
228
|
+
sign = java.security.Signature.getInstance("SHA256WithECDSA")
|
229
|
+
sign.initSign(kp.private_key)
|
230
|
+
teLogger.debug "Signing data : #{val}"
|
231
|
+
case val
|
232
|
+
when java.io.InputStream
|
233
|
+
buf = Java::byte[102400].new
|
234
|
+
while((read = val.read(buf, 0, buf.length)) != nil)
|
235
|
+
sign.update(buf,0,read)
|
236
|
+
end
|
237
|
+
else
|
238
|
+
sign.update(to_java_bytes(val))
|
239
|
+
end
|
240
|
+
|
241
|
+
sign.sign
|
242
|
+
end
|
243
|
+
|
244
|
+
def self.verify(pubKey, val, sign)
|
245
|
+
ver = java.security.Signature.getInstance("SHA256WithECDSA")
|
246
|
+
ver.initVerify(pubKey)
|
247
|
+
teLogger.debug "Verifing data : #{val}"
|
248
|
+
case val
|
249
|
+
when java.io.InputStream
|
250
|
+
buf = Java::byte[102400].new
|
251
|
+
while((read = val.read(buf, 0 ,buf.length)) != nil)
|
252
|
+
ver.update(buf,0, read)
|
253
|
+
end
|
254
|
+
else
|
255
|
+
ver.update(to_java_bytes(val))
|
256
|
+
end
|
257
|
+
|
258
|
+
ver.verify(to_java_bytes(sign))
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
require_relative '../data_conversion'
|
3
|
+
|
4
|
+
module Ccrypto
|
5
|
+
module Java
|
6
|
+
class HKDFEngine
|
7
|
+
include DataConversion
|
8
|
+
include TR::CondUtils
|
9
|
+
|
10
|
+
def initialize(*args, &block)
|
11
|
+
@config = args.first
|
12
|
+
|
13
|
+
raise KDFEngineException, "KDF config is expected. Given #{@config}" if not @config.is_a?(Ccrypto::KDFConfig)
|
14
|
+
raise KDFEngineException, "Output bit length (outBitLength) value is not given or not a positive value (#{@config.outBitLength})" if is_empty?(@config.outBitLength) or @config.outBitLength <= 0
|
15
|
+
|
16
|
+
|
17
|
+
@config.salt = SecureRandom.random_bytes(16) if is_empty?(@config.salt)
|
18
|
+
end
|
19
|
+
|
20
|
+
def derive(input, output = :binary)
|
21
|
+
begin
|
22
|
+
|
23
|
+
case @config.digest
|
24
|
+
when :sha1
|
25
|
+
dig = org.bouncycastle.crypto.digests.SHA1Digest.new
|
26
|
+
when :sha224
|
27
|
+
dig = org.bouncycastle.crypto.digests.SHA224Digest.new
|
28
|
+
when :sha256
|
29
|
+
dig = org.bouncycastle.crypto.digests.SHA256Digest.new
|
30
|
+
when :sha384
|
31
|
+
dig = org.bouncycastle.crypto.digests.SHA384Digest.new
|
32
|
+
when :sha512
|
33
|
+
dig = org.bouncycastle.crypto.digests.SHA512Digest.new
|
34
|
+
when :sha3_224
|
35
|
+
dig = org.bouncycastle.crypto.digests.SHA3Digest.new(224)
|
36
|
+
when :sha3_256
|
37
|
+
dig = org.bouncycastle.crypto.digests.SHA3Digest.new(256)
|
38
|
+
when :sha3_384
|
39
|
+
dig = org.bouncycastle.crypto.digests.SHA3Digest.new(384)
|
40
|
+
when :sha3_512
|
41
|
+
dig = org.bouncycastle.crypto.digests.SHA3Digest.new(512)
|
42
|
+
else
|
43
|
+
raise KDFEngineException, "Digest #{@config.digest} not supported"
|
44
|
+
end
|
45
|
+
|
46
|
+
@config.info = "" if @config.info.nil?
|
47
|
+
|
48
|
+
hkdf = org.bouncycastle.crypto.generators.HKDFBytesGenerator.new(dig)
|
49
|
+
hkdfParam = org.bouncycastle.crypto.params.HKDFParameters.new(to_java_bytes(input), to_java_bytes(@config.salt) ,to_java_bytes(@config.info))
|
50
|
+
hkdf.init(hkdfParam)
|
51
|
+
|
52
|
+
out = ::Java::byte[@config.outBitLength/8].new
|
53
|
+
hkdf.generateBytes(out, 0, out.length)
|
54
|
+
|
55
|
+
case output
|
56
|
+
when :b64
|
57
|
+
to_b64(out)
|
58
|
+
when :hex
|
59
|
+
to_hex(out)
|
60
|
+
else
|
61
|
+
out
|
62
|
+
end
|
63
|
+
|
64
|
+
rescue Exception => ex
|
65
|
+
raise KDFEngineException, ex
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
|
2
|
+
require_relative '../data_conversion'
|
3
|
+
|
4
|
+
module Ccrypto
|
5
|
+
module Java
|
6
|
+
class HMACEngine
|
7
|
+
include TR::CondUtils
|
8
|
+
include DataConversion
|
9
|
+
|
10
|
+
include TeLogger::TeLogHelper
|
11
|
+
teLogger_tag :j_hmac
|
12
|
+
|
13
|
+
def initialize(*args, &block)
|
14
|
+
@config = args.first
|
15
|
+
|
16
|
+
raise HMACEngineException, "HMAC config is expected" if not @config.is_a?(Ccrypto::HMACConfig)
|
17
|
+
|
18
|
+
raise HMACEngineException, "Signing key is required" if is_empty?(@config.key)
|
19
|
+
raise HMACEngineException, "Secret key as signing key is required. Given #{@config.key.class}" if not @config.key.is_a?(Ccrypto::SecretKey)
|
20
|
+
|
21
|
+
teLogger.debug "Config : #{@config.inspect}"
|
22
|
+
begin
|
23
|
+
macAlgo = to_jce_spec(@config)
|
24
|
+
teLogger.debug "Mac algo : #{macAlgo}"
|
25
|
+
@hmac = javax.crypto.Mac.getInstance(to_jce_spec(@config))
|
26
|
+
@hmac.init(@config.key.to_jce_secret_key)
|
27
|
+
rescue Exception => ex
|
28
|
+
raise HMACEngineException, ex
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def hmac_update(val)
|
34
|
+
@hmac.update(to_java_bytes(val)) if not_empty?(val)
|
35
|
+
end
|
36
|
+
|
37
|
+
def hmac_final
|
38
|
+
@hmac.doFinal
|
39
|
+
end
|
40
|
+
|
41
|
+
def hmac_digest(val, output = :binary)
|
42
|
+
hmac_update(val)
|
43
|
+
res = hmac_final
|
44
|
+
|
45
|
+
case output
|
46
|
+
when :hex
|
47
|
+
to_hex(res)
|
48
|
+
when :b64
|
49
|
+
to_b64(res)
|
50
|
+
else
|
51
|
+
res
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
def to_jce_spec(config)
|
58
|
+
res = []
|
59
|
+
res << "HMAC"
|
60
|
+
|
61
|
+
salgo = config.digest.to_s
|
62
|
+
if salgo =~ /_/
|
63
|
+
res << salgo.gsub("_","-").upcase
|
64
|
+
else
|
65
|
+
res << salgo.upcase
|
66
|
+
end
|
67
|
+
|
68
|
+
res.join
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
require_relative '../data_conversion'
|
3
|
+
|
4
|
+
module Ccrypto
|
5
|
+
module Java
|
6
|
+
|
7
|
+
class PBKDF2Engine
|
8
|
+
include TR::CondUtils
|
9
|
+
include DataConversion
|
10
|
+
|
11
|
+
def initialize(*args, &block)
|
12
|
+
@config = args.first
|
13
|
+
|
14
|
+
raise KDFEngineException, "KDF config is expected. Given #{@config}" if not @config.is_a?(Ccrypto::PBKDF2Config)
|
15
|
+
raise KDFEngineException, "Output bit length (outBitLength) value is not given or not a positive value (#{@config.outBitLength})" if is_empty?(@config.outBitLength) or @config.outBitLength <= 0
|
16
|
+
|
17
|
+
raise KDFEngineException, "Digest algo is not supported. Given #{@config.digest}, supported: #{supported_digest.join(", ")}" if not @config.digest.nil? and not is_digest_supported?(@config.digest)
|
18
|
+
|
19
|
+
@config.digest = default_digest if is_empty?(@config.digest)
|
20
|
+
|
21
|
+
@config.salt = SecureRandom.random_bytes(16) if is_empty?(@config.salt)
|
22
|
+
end
|
23
|
+
|
24
|
+
def derive(input, output = :binary)
|
25
|
+
|
26
|
+
begin
|
27
|
+
|
28
|
+
case input
|
29
|
+
when String
|
30
|
+
if input.ascii_only?
|
31
|
+
pass = input.to_java.toCharArray
|
32
|
+
else
|
33
|
+
pass = to_hex(to_java_bytes(input)).to_java.toCharArray
|
34
|
+
end
|
35
|
+
when ::Java::byte[]
|
36
|
+
pass = to_hex(to_java_bytes(input)).to_java.toCharArray
|
37
|
+
else
|
38
|
+
raise KDFEngineException, "Input type '#{input.class}' cannot convert to char array"
|
39
|
+
end
|
40
|
+
|
41
|
+
skf = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHMAC#{@config.digest.upcase}",JCEProvider::DEFProv)
|
42
|
+
keySpec = javax.crypto.spec.PBEKeySpec.new(pass.to_java, to_java_bytes(@config.salt), @config.iter, @config.outBitLength)
|
43
|
+
|
44
|
+
sk = skf.generateSecret(keySpec)
|
45
|
+
out = sk.encoded
|
46
|
+
|
47
|
+
case output
|
48
|
+
when :b64
|
49
|
+
to_b64(out)
|
50
|
+
when :hex
|
51
|
+
to_hex(out)
|
52
|
+
else
|
53
|
+
out
|
54
|
+
end
|
55
|
+
|
56
|
+
rescue Exception => ex
|
57
|
+
raise KDFEngineException, ex
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_digest
|
63
|
+
:sha256
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def logger
|
68
|
+
if @logger.nil?
|
69
|
+
@logger = TeLogger::Tlogger.new
|
70
|
+
@logger.tag = :j_pbkdf2
|
71
|
+
end
|
72
|
+
@logger
|
73
|
+
end
|
74
|
+
|
75
|
+
def is_digest_supported?(dig)
|
76
|
+
supported_digest.include?(dig)
|
77
|
+
end
|
78
|
+
|
79
|
+
def supported_digest
|
80
|
+
[:sha1, :sha256, :sha224, :sha384, :sha512]
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|