secure_string 0.9.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.
- data/LICENSE.txt +24 -0
- data/README.rdoc +63 -0
- data/Rakefile +37 -0
- data/lib/secure_string.rb +59 -0
- data/lib/secure_string/base64_methods.rb +28 -0
- data/lib/secure_string/cipher_methods.rb +76 -0
- data/lib/secure_string/digest_methods.rb +48 -0
- data/lib/secure_string/rsa_methods.rb +65 -0
- data/spec/spec_helper.rb +4 -0
- metadata +74 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2010, Jeffrey C. Reinecke
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
* Neither the name of the copyright holders nor the
|
12
|
+
names of its contributors may be used to endorse or promote products
|
13
|
+
derived from this software without specific prior written permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL JEFFREY REINECKE BE LIABLE FOR ANY
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= Overview
|
2
|
+
|
3
|
+
SecureString is a special string subclass that provides two pieces of
|
4
|
+
functionality that can be used individually:
|
5
|
+
|
6
|
+
* Byte string support: Although a string can already contain bytes, this makes
|
7
|
+
it easier to view and work with strings holding binary data, including
|
8
|
+
conversion to/from raw hex or Base64 encoded values.
|
9
|
+
* Secure string support: Easy methods for RSA encryption, AES encoding, and
|
10
|
+
SHA/MD5 digest hashing, of the data in the strings.
|
11
|
+
|
12
|
+
= Contact
|
13
|
+
|
14
|
+
If you have any questions, comments, concerns, patches, or bugs, you can contact
|
15
|
+
me via the github repository at:
|
16
|
+
|
17
|
+
http://github.com/paploo/secure_string
|
18
|
+
|
19
|
+
or directly via e-mail at:
|
20
|
+
|
21
|
+
mailto:jeff@paploo.net
|
22
|
+
|
23
|
+
= Version History
|
24
|
+
|
25
|
+
[0.9.0 - 2010-Nov-03] Initial release.
|
26
|
+
* Feature complete, but lacks spec tests and examples.
|
27
|
+
|
28
|
+
= TODO List
|
29
|
+
|
30
|
+
* Add complete spec tests.
|
31
|
+
* Add examples.
|
32
|
+
|
33
|
+
= License
|
34
|
+
|
35
|
+
The files contained in this repository are released under the commercially and
|
36
|
+
GPL compatible "New BSD License", given below:
|
37
|
+
|
38
|
+
== License Text
|
39
|
+
|
40
|
+
Copyright (c) 2010, Jeffrey C. Reinecke
|
41
|
+
All rights reserved.
|
42
|
+
|
43
|
+
Redistribution and use in source and binary forms, with or without
|
44
|
+
modification, are permitted provided that the following conditions are met:
|
45
|
+
* Redistributions of source code must retain the above copyright
|
46
|
+
notice, this list of conditions and the following disclaimer.
|
47
|
+
* Redistributions in binary form must reproduce the above copyright
|
48
|
+
notice, this list of conditions and the following disclaimer in the
|
49
|
+
documentation and/or other materials provided with the distribution.
|
50
|
+
* Neither the name of the copyright holders nor the
|
51
|
+
names of its contributors may be used to endorse or promote products
|
52
|
+
derived from this software without specific prior written permission.
|
53
|
+
|
54
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
55
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
56
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
57
|
+
DISCLAIMED. IN NO EVENT SHALL JEFFREY REINECKE BE LIABLE FOR ANY
|
58
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
59
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
60
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
61
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
62
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
63
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require "rake/rdoctask"
|
3
|
+
|
4
|
+
# ===== RDOC BUILDING =====
|
5
|
+
# This isn't necessary if installing from a gem.
|
6
|
+
|
7
|
+
Rake::RDocTask.new do |rdoc|
|
8
|
+
rdoc.rdoc_dir = "rdoc"
|
9
|
+
rdoc.rdoc_files.add "lib/**/*.rb", "README.rdoc"
|
10
|
+
end
|
11
|
+
|
12
|
+
# ===== SPEC TESTING =====
|
13
|
+
|
14
|
+
begin
|
15
|
+
require "spec/rake/spectask"
|
16
|
+
|
17
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
18
|
+
spec.spec_opts = ['-c' '-f specdoc']
|
19
|
+
spec.spec_files = ['spec']
|
20
|
+
end
|
21
|
+
|
22
|
+
Spec::Rake::SpecTask.new(:spec_with_backtrace) do |spec|
|
23
|
+
spec.spec_opts = ['-c' '-f specdoc', '-b']
|
24
|
+
spec.spec_files = ['spec']
|
25
|
+
end
|
26
|
+
rescue LoadError
|
27
|
+
task :spec do
|
28
|
+
puts "You must have rspec installed to run this task."
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# ===== GEM BUILDING =====
|
33
|
+
|
34
|
+
desc "Build the gem file for this package"
|
35
|
+
task :build_gem do
|
36
|
+
STDOUT.puts `gem build secure_string.gemspec`
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
require_relative 'secure_string/digest_methods'
|
4
|
+
require_relative 'secure_string/base64_methods'
|
5
|
+
require_relative 'secure_string/cipher_methods'
|
6
|
+
require_relative 'secure_string/rsa_methods'
|
7
|
+
|
8
|
+
# SecureString is a String subclass whose emphasis is on byte data rather than
|
9
|
+
# human readable strings. class gives a number of conveniences, such
|
10
|
+
# as easier viewing of the byte data as hex, digest methods, and encryption
|
11
|
+
# and decryption methods.
|
12
|
+
class SecureString < String
|
13
|
+
include Base64Methods
|
14
|
+
include DigestMethods
|
15
|
+
include RSAMethods
|
16
|
+
include CipherMethods
|
17
|
+
|
18
|
+
# Creates the string from one many kinds of values:
|
19
|
+
# [:data] (default) The passed string value is directly used.
|
20
|
+
# [:hex] Initialize using a hexidecimal string.
|
21
|
+
# [:int] Initialize using the numeric value of the hexidecimal string.
|
22
|
+
# [:base64] Initialize using the given base64 encoded data.
|
23
|
+
def initialize(mode = :data, value)
|
24
|
+
case mode
|
25
|
+
when :hex
|
26
|
+
hex_string = value.to_s
|
27
|
+
data = [hex_string].pack('H' + hex_string.length.to_s)
|
28
|
+
when :data
|
29
|
+
data = value.to_s
|
30
|
+
when :int
|
31
|
+
self.send(__method__, :hex, value.to_i.to_s(16))
|
32
|
+
when :base64
|
33
|
+
data = Base64.decode64(value.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
self.replace(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Override the default String inspect to return the hexidecimal
|
40
|
+
# representation of the data contained in this string.
|
41
|
+
def inspect
|
42
|
+
return "<#{to_hex}>"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the hexidecimal string representation of the data.
|
46
|
+
def to_hex
|
47
|
+
return (self.empty? ? '' : self.unpack('H' + (self.length*2).to_s)[0])
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the data converted from hexidecimal into an integer.
|
51
|
+
# This is usually as a BigInt.
|
52
|
+
#
|
53
|
+
# WARNING: If the data string is empty, then this returns -1, as there is no
|
54
|
+
# integer representation of the absence of data.
|
55
|
+
def to_i
|
56
|
+
return (self.empty? ? -1 : to_hex.hex)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
class SecureString < String
|
4
|
+
module Base64Methods
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
|
12
|
+
# Encodes to Base64. By default, the output is made URL safe, which means all
|
13
|
+
# newlines are stripped out. If you want standard formatted Base64 with
|
14
|
+
# newlines, then call this method with url_safe as false.
|
15
|
+
def to_base64(url_safe = true)
|
16
|
+
encoded_data = (url_safe ? Base64.urlsafe_encode64(self) : Base64.encode64(self))
|
17
|
+
return self.class.new( encoded_data )
|
18
|
+
end
|
19
|
+
|
20
|
+
# Decode self as a Base64 data string and return the result.
|
21
|
+
def from_base64
|
22
|
+
return self.class.new( Base64.decode64(self) )
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class SecureString < String
|
4
|
+
module CipherMethods
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.send(:extend, ClassMethods)
|
8
|
+
mod.send(:include, InstanceMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# A convenience method for generating random cipher keys and initialization
|
14
|
+
# vectors.
|
15
|
+
def cipher_keygen(cipher_name)
|
16
|
+
cipher = OpenSSL::Cipher::Cipher.new(cipher_name)
|
17
|
+
cipher.encrypt
|
18
|
+
return [cipher.random_key, cipher.random_iv].map {|s| self.new(s)}
|
19
|
+
end
|
20
|
+
|
21
|
+
# A convenience method for generating a random key and init vector for AES keys.
|
22
|
+
# Defaults to a key length of 256.
|
23
|
+
def aes_keygen(key_len=256)
|
24
|
+
return cipher_keygen("aes-#{key_len.to_i}-cbc")
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
|
31
|
+
# Given an OpenSSL cipher name, a key, and initialization vector,
|
32
|
+
# encrypt the data.
|
33
|
+
#
|
34
|
+
# Use OpenSSL::Cipher.ciphers to get a list of available cipher names.
|
35
|
+
#
|
36
|
+
# To generate a new key and iv, do the following:
|
37
|
+
# cipher = OpenSSL::Cipher::Cipher.new(cipher_name)
|
38
|
+
# cipher.encrypt
|
39
|
+
# key = cipher.random_key
|
40
|
+
# iv = cipher.random_iv
|
41
|
+
def to_cipher(cipher_name, key, iv)
|
42
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
43
|
+
cipher.encrypt # MUST set the mode BEFORE setting the key and iv!
|
44
|
+
cipher.key = key
|
45
|
+
cipher.iv = iv
|
46
|
+
msg = cipher.update(self)
|
47
|
+
msg << cipher.final
|
48
|
+
return self.class.new(msg)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Given an OpenSSL cipher name, a key, and an init vector,
|
52
|
+
# decrypt the data.
|
53
|
+
def from_cipher(cipher_name, key, iv)
|
54
|
+
cipher = OpenSSL::Cipher.new(cipher_name)
|
55
|
+
cipher.decrypt # MUST set the mode BEFORE setting the key and iv!
|
56
|
+
cipher.key = key
|
57
|
+
cipher.iv = iv
|
58
|
+
msg = cipher.update(self)
|
59
|
+
msg << cipher.final
|
60
|
+
return self.class.new(msg)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Given an AES key and initialization vector, AES encode the data.
|
64
|
+
def to_aes(key, iv, key_len=256)
|
65
|
+
return self.class.new( to_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
|
66
|
+
end
|
67
|
+
|
68
|
+
# Given an AES key and init vector, AES decode the data.
|
69
|
+
def from_aes(key, iv, key_len=256)
|
70
|
+
return self.class.new( from_cipher("aes-#{key_len.to_i}-cbc", key, iv) )
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class SecureString < String
|
4
|
+
module DigestMethods
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
|
12
|
+
# Returns the digest of the byte string as a SecureString, using the passed OpenSSL object.
|
13
|
+
def to_digest(digest_obj)
|
14
|
+
return self.class.new( digest_obj.digest(self) )
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the MD5 of the byte string as a SecureString.
|
18
|
+
def to_md5
|
19
|
+
return to_digest( OpenSSL::Digest::MD5.new )
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the SHA2 of the byte string as a SecureString.
|
23
|
+
#
|
24
|
+
# By default, this uses the 256 bit SHA2, but the optional arugment allows
|
25
|
+
# specification of which bit length to use.
|
26
|
+
def to_sha2(length=256)
|
27
|
+
if [224,256,384,512].include?(length)
|
28
|
+
digest_klass = OpenSSL::Digest.const_get("SHA#{length}", false)
|
29
|
+
return to_digest( digest_klass )
|
30
|
+
else
|
31
|
+
raise ArgumentError, "Invalid SHA2 length: #{length}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the SHA2 256 of the data string. See +to_sha2+.
|
36
|
+
def to_sha256
|
37
|
+
return to_sha2(256)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the SHA2 512 of the data string. See +to_sha2+.
|
41
|
+
def to_sha512
|
42
|
+
return to_sha2(512)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class SecureString < String
|
4
|
+
module RSAMethods
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.send(:extend, ClassMethods)
|
8
|
+
mod.send(:include, InstanceMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
# A convenience method for generating random public/private RSA key pairs.
|
14
|
+
# Defaults to a key length of 1024.
|
15
|
+
#
|
16
|
+
# Returns the private key first, then the public key. Returns them in PEM file
|
17
|
+
# format by default, as this is most useful for portability. DER format can
|
18
|
+
# be explicitly specified with the second argument.
|
19
|
+
def rsa_keygen(key_len=1024, format = :pem)
|
20
|
+
private_key_obj = OpenSSL::PKey::RSA.new(key_len.to_i)
|
21
|
+
public_key_obj = private_key_obj.public_key
|
22
|
+
formatting_method = (format == :der ? :to_der : :to_pem)
|
23
|
+
return [private_key_obj, public_key_obj].map {|k| self.new( k.send(formatting_method) )}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
|
30
|
+
# Given an RSA public key, it RSA encrypts the data string.
|
31
|
+
#
|
32
|
+
# Note that the key must be 11 bytes longer than the data string or it doesn't
|
33
|
+
# work.
|
34
|
+
def to_rsa(public_key)
|
35
|
+
key = OpenSSL::PKey::RSA.new(public_key)
|
36
|
+
return self.class.new( key.public_encrypt(self) )
|
37
|
+
end
|
38
|
+
|
39
|
+
# Given an RSA private key, it decrypts the data string back into the original text.
|
40
|
+
def from_rsa(private_key)
|
41
|
+
key = OpenSSL::PKey::RSA.new(private_key)
|
42
|
+
return self.class.new( key.private_decrypt(self) )
|
43
|
+
end
|
44
|
+
|
45
|
+
# Signs the given message using hte given private key.
|
46
|
+
#
|
47
|
+
# By default, signs using SHA256, but another digest object can be given.
|
48
|
+
def sign(private_key, digest_obj=OpenSSL::Digest::SHA256.new)
|
49
|
+
key = OpenSSL::PKey::RSA.new(private_key)
|
50
|
+
return self.class.new( key.sign(digest_obj, self) )
|
51
|
+
end
|
52
|
+
|
53
|
+
# Verifies the given signature matches the messages digest, using the
|
54
|
+
# signer's public key.
|
55
|
+
#
|
56
|
+
# By default, verifies using SHA256, but another digest object can be given.
|
57
|
+
def verify?(public_key, signature, digest_obj=OpenSSL::Digest::SHA256.new)
|
58
|
+
key = OpenSSL::PKey::RSA.new(public_key)
|
59
|
+
return key.verify(digest_obj, signature.to_s, self)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: secure_string
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 9
|
8
|
+
- 0
|
9
|
+
version: 0.9.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Jeff Reinecke
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-03 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: " A String subclass to simplify handling of:\n 1. Binary data, including HEX encoding and Bin64 encoding.\n 2. Encryption such as RSA, AES, and digest methods such as SHA and MD5.\n"
|
22
|
+
email: jeff@paploo.net
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README.rdoc
|
29
|
+
files:
|
30
|
+
- README.rdoc
|
31
|
+
- LICENSE.txt
|
32
|
+
- Rakefile
|
33
|
+
- lib/secure_string/base64_methods.rb
|
34
|
+
- lib/secure_string/cipher_methods.rb
|
35
|
+
- lib/secure_string/digest_methods.rb
|
36
|
+
- lib/secure_string/rsa_methods.rb
|
37
|
+
- lib/secure_string.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://www.github.com/paploo/secure_string
|
41
|
+
licenses:
|
42
|
+
- BSD
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 1
|
55
|
+
- 9
|
56
|
+
- 2
|
57
|
+
version: 1.9.2
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.7
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: A String subclass for simple handling of binary data and encryption.
|
73
|
+
test_files: []
|
74
|
+
|