crypto-lite 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +3 -3
- data/README.md +1023 -511
- data/Rakefile +34 -34
- data/lib/crypto/lite.rb +2 -2
- data/lib/crypto-lite/config.rb +32 -32
- data/lib/crypto-lite/helper.rb +25 -25
- data/lib/crypto-lite/metal.rb +135 -128
- data/lib/crypto-lite/sign_rsa.rb +29 -29
- data/lib/crypto-lite/version.rb +23 -23
- data/lib/crypto-lite.rb +145 -148
- data/lib/crypto.rb +2 -2
- data/test/helper.rb +11 -11
- data/test/test_base58.rb +36 -36
- data/test/test_bitcoin_addr.rb +58 -58
- data/test/test_hash.rb +47 -47
- data/test/test_hash_sha.rb +90 -87
- data/test/test_version.rb +19 -19
- metadata +7 -7
data/Rakefile
CHANGED
@@ -1,34 +1,34 @@
|
|
1
|
-
require 'hoe'
|
2
|
-
require './lib/crypto-lite/version.rb'
|
3
|
-
|
4
|
-
|
5
|
-
Hoe.spec 'crypto-lite' do
|
6
|
-
|
7
|
-
self.version = CryptoLite::VERSION
|
8
|
-
|
9
|
-
self.summary = "crypto-lite - cryptographic secure hash functions and public key signature algorithms made easy"
|
10
|
-
self.description = summary
|
11
|
-
|
12
|
-
self.urls = { home: 'https://github.com/
|
13
|
-
|
14
|
-
self.author = 'Gerald Bauer'
|
15
|
-
self.email = 'wwwmake@googlegroups.com'
|
16
|
-
|
17
|
-
# switch extension to .markdown for gihub formatting
|
18
|
-
self.readme_file = 'README.md'
|
19
|
-
self.history_file = 'CHANGELOG.md'
|
20
|
-
|
21
|
-
self.extra_deps = [
|
22
|
-
['digest-
|
23
|
-
['base32-alphabets'],
|
24
|
-
['base58-alphabets'],
|
25
|
-
['elliptic'],
|
26
|
-
]
|
27
|
-
|
28
|
-
self.licenses = ['Public Domain']
|
29
|
-
|
30
|
-
self.spec_extras = {
|
31
|
-
required_ruby_version: '>= 2.3'
|
32
|
-
}
|
33
|
-
|
34
|
-
end
|
1
|
+
require 'hoe'
|
2
|
+
require './lib/crypto-lite/version.rb'
|
3
|
+
|
4
|
+
|
5
|
+
Hoe.spec 'crypto-lite' do
|
6
|
+
|
7
|
+
self.version = CryptoLite::VERSION
|
8
|
+
|
9
|
+
self.summary = "crypto-lite - cryptographic secure hash functions and public key signature algorithms made easy"
|
10
|
+
self.description = summary
|
11
|
+
|
12
|
+
self.urls = { home: 'https://github.com/rubycocos/blockchain' }
|
13
|
+
|
14
|
+
self.author = 'Gerald Bauer'
|
15
|
+
self.email = 'wwwmake@googlegroups.com'
|
16
|
+
|
17
|
+
# switch extension to .markdown for gihub formatting
|
18
|
+
self.readme_file = 'README.md'
|
19
|
+
self.history_file = 'CHANGELOG.md'
|
20
|
+
|
21
|
+
self.extra_deps = [
|
22
|
+
['digest-lite'],
|
23
|
+
['base32-alphabets'],
|
24
|
+
['base58-alphabets'],
|
25
|
+
['elliptic'],
|
26
|
+
]
|
27
|
+
|
28
|
+
self.licenses = ['Public Domain']
|
29
|
+
|
30
|
+
self.spec_extras = {
|
31
|
+
required_ruby_version: '>= 2.3'
|
32
|
+
}
|
33
|
+
|
34
|
+
end
|
data/lib/crypto/lite.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require_relative '../crypto-lite' ## lets you use require 'crypto/lite' too
|
2
|
-
|
1
|
+
require_relative '../crypto-lite' ## lets you use require 'crypto/lite' too
|
2
|
+
|
data/lib/crypto-lite/config.rb
CHANGED
@@ -1,32 +1,32 @@
|
|
1
|
-
module Crypto
|
2
|
-
|
3
|
-
class Configuration
|
4
|
-
|
5
|
-
def initialize
|
6
|
-
@debug = false
|
7
|
-
end
|
8
|
-
|
9
|
-
def debug?() @debug || false; end
|
10
|
-
def debug=(value) @debug = value; end
|
11
|
-
end # class Configuration
|
12
|
-
|
13
|
-
## lets you use
|
14
|
-
## Crypto.configure do |config|
|
15
|
-
## config.debug = true
|
16
|
-
## end
|
17
|
-
|
18
|
-
def self.configuration
|
19
|
-
@configuration ||= Configuration.new
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.configure
|
23
|
-
yield( configuration )
|
24
|
-
end
|
25
|
-
|
26
|
-
## add convenience helper for format
|
27
|
-
def self.debug?() configuration.debug?; end
|
28
|
-
def self.debug=(value) self.configuration.debug = value; end
|
29
|
-
end # module Crypto
|
30
|
-
|
31
|
-
|
32
|
-
|
1
|
+
module Crypto
|
2
|
+
|
3
|
+
class Configuration
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@debug = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def debug?() @debug || false; end
|
10
|
+
def debug=(value) @debug = value; end
|
11
|
+
end # class Configuration
|
12
|
+
|
13
|
+
## lets you use
|
14
|
+
## Crypto.configure do |config|
|
15
|
+
## config.debug = true
|
16
|
+
## end
|
17
|
+
|
18
|
+
def self.configuration
|
19
|
+
@configuration ||= Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure
|
23
|
+
yield( configuration )
|
24
|
+
end
|
25
|
+
|
26
|
+
## add convenience helper for format
|
27
|
+
def self.debug?() configuration.debug?; end
|
28
|
+
def self.debug=(value) self.configuration.debug = value; end
|
29
|
+
end # module Crypto
|
30
|
+
|
31
|
+
|
32
|
+
|
data/lib/crypto-lite/helper.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
|
-
|
2
|
-
module CryptoHelper
|
3
|
-
### note: use include CryptoHelper
|
4
|
-
## to get "top-level" / global helpers
|
5
|
-
|
6
|
-
## add convenience "top-level" helpers
|
7
|
-
def sha256( *args, **kwargs ) Crypto.sha256( *args, **kwargs ); end
|
8
|
-
def sha3_256( *args, **kwargs ) Crypto.sha3_256( *args, **kwargs ); end
|
9
|
-
|
10
|
-
def keccak256( *args, **kwargs ) Crypto.keccak256( *args, **kwargs ); end
|
11
|
-
|
12
|
-
def rmd160( *args, **kwargs ) Crypto.rmd160( *args, **kwargs ); end
|
13
|
-
## def ripemd160( input ) Crypto.rmd160( input ); end
|
14
|
-
alias_method :ripemd160, :rmd160
|
15
|
-
|
16
|
-
def hash160( *args, **kwargs ) Crypto.hash160( *args, **kwargs ); end
|
17
|
-
|
18
|
-
def hash256( *args, **kwargs ) Crypto.hash256( *args, **kwargs ); end
|
19
|
-
|
20
|
-
|
21
|
-
def base58( *args, **kwargs ) Crypto.base58( *args, **kwargs ); end
|
22
|
-
def base58check( *args, **kwargs ) Crypto.base58check( *args, **kwargs ); end
|
23
|
-
end
|
24
|
-
|
25
|
-
|
1
|
+
|
2
|
+
module CryptoHelper
|
3
|
+
### note: use include CryptoHelper
|
4
|
+
## to get "top-level" / global helpers
|
5
|
+
|
6
|
+
## add convenience "top-level" helpers
|
7
|
+
def sha256( *args, **kwargs ) Crypto.sha256( *args, **kwargs ); end
|
8
|
+
def sha3_256( *args, **kwargs ) Crypto.sha3_256( *args, **kwargs ); end
|
9
|
+
|
10
|
+
def keccak256( *args, **kwargs ) Crypto.keccak256( *args, **kwargs ); end
|
11
|
+
|
12
|
+
def rmd160( *args, **kwargs ) Crypto.rmd160( *args, **kwargs ); end
|
13
|
+
## def ripemd160( input ) Crypto.rmd160( input ); end
|
14
|
+
alias_method :ripemd160, :rmd160
|
15
|
+
|
16
|
+
def hash160( *args, **kwargs ) Crypto.hash160( *args, **kwargs ); end
|
17
|
+
|
18
|
+
def hash256( *args, **kwargs ) Crypto.hash256( *args, **kwargs ); end
|
19
|
+
|
20
|
+
|
21
|
+
def base58( *args, **kwargs ) Crypto.base58( *args, **kwargs ); end
|
22
|
+
def base58check( *args, **kwargs ) Crypto.base58check( *args, **kwargs ); end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
data/lib/crypto-lite/metal.rb
CHANGED
@@ -1,128 +1,135 @@
|
|
1
|
-
module Crypto
|
2
|
-
module Metal
|
3
|
-
|
4
|
-
def self.debug?() Crypto.debug?; end
|
5
|
-
|
6
|
-
########################
|
7
|
-
### to the "metal" crypto primitives
|
8
|
-
## work with binary strings (aka byte arrays) / data
|
9
|
-
|
10
|
-
##
|
11
|
-
## todo/check: use/keep bin-suffix in name - why? why not?
|
12
|
-
|
13
|
-
|
14
|
-
def self.base58bin( input )
|
15
|
-
## todo/check: input must be a (binary) string - why? why not?
|
16
|
-
Base58::Bitcoin.encode_bin( input )
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.base58bin_check( input )
|
20
|
-
## todo/check: input must be a (binary) string - why? why not?
|
21
|
-
hash256 = hash256bin( input )
|
22
|
-
base58bin( input + hash256[0,4] )
|
23
|
-
end
|
24
|
-
|
25
|
-
|
26
|
-
########################
|
27
|
-
# (secure) hash functions
|
28
|
-
|
29
|
-
def self.keccak256bin( input )
|
30
|
-
message = message( input ) ## "normalize" / convert to (binary) string
|
31
|
-
Digest::
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.rmd160bin( input )
|
35
|
-
message = message( input ) ## "normalize" / convert to (binary) string
|
36
|
-
Digest::RMD160.digest( message )
|
37
|
-
end
|
38
|
-
|
39
|
-
## add alias RIPEMD160 - why? why not?
|
40
|
-
class << self
|
41
|
-
alias_method :ripemd160bin, :rmd160bin
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
def self.sha256bin( input, engine=nil ) ## todo/check: add alias sha256b or such to - why? why not?
|
46
|
-
message = message( input ) ## "normalize" / convert to (binary) string
|
47
|
-
|
48
|
-
if engine && ['openssl'].include?( engine.to_s.downcase )
|
49
|
-
puts " engine: #{engine}" if debug?
|
50
|
-
digest = OpenSSL::Digest::SHA256.new
|
51
|
-
## or use OpenSSL::Digest.new( 'SHA256' )
|
52
|
-
digest.update( message )
|
53
|
-
digest.digest
|
54
|
-
else ## use "built-in" hash function from digest module
|
55
|
-
Digest::SHA256.digest( message )
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
##
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
def self.
|
84
|
-
message = message( input ) ## "normalize" / convert to (binary) string
|
85
|
-
|
86
|
-
|
87
|
-
end
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
end
|
1
|
+
module Crypto
|
2
|
+
module Metal
|
3
|
+
|
4
|
+
def self.debug?() Crypto.debug?; end
|
5
|
+
|
6
|
+
########################
|
7
|
+
### to the "metal" crypto primitives
|
8
|
+
## work with binary strings (aka byte arrays) / data
|
9
|
+
|
10
|
+
##
|
11
|
+
## todo/check: use/keep bin-suffix in name - why? why not?
|
12
|
+
|
13
|
+
|
14
|
+
def self.base58bin( input )
|
15
|
+
## todo/check: input must be a (binary) string - why? why not?
|
16
|
+
Base58::Bitcoin.encode_bin( input )
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.base58bin_check( input )
|
20
|
+
## todo/check: input must be a (binary) string - why? why not?
|
21
|
+
hash256 = hash256bin( input )
|
22
|
+
base58bin( input + hash256[0,4] )
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
########################
|
27
|
+
# (secure) hash functions
|
28
|
+
|
29
|
+
def self.keccak256bin( input )
|
30
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
31
|
+
Digest::KeccakLite.digest( message, 256 )
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.rmd160bin( input )
|
35
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
36
|
+
Digest::RMD160.digest( message )
|
37
|
+
end
|
38
|
+
|
39
|
+
## add alias RIPEMD160 - why? why not?
|
40
|
+
class << self
|
41
|
+
alias_method :ripemd160bin, :rmd160bin
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def self.sha256bin( input, engine=nil ) ## todo/check: add alias sha256b or such to - why? why not?
|
46
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
47
|
+
|
48
|
+
if engine && ['openssl'].include?( engine.to_s.downcase )
|
49
|
+
puts " engine: #{engine}" if debug?
|
50
|
+
digest = OpenSSL::Digest::SHA256.new
|
51
|
+
## or use OpenSSL::Digest.new( 'SHA256' )
|
52
|
+
digest.update( message )
|
53
|
+
digest.digest
|
54
|
+
else ## use "built-in" hash function from digest module
|
55
|
+
Digest::SHA256.digest( message )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def self.sha3_256bin( input, engine=nil )
|
61
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
62
|
+
|
63
|
+
if engine && ['openssl'].include?( engine.to_s.downcase )
|
64
|
+
puts " engine: #{engine}" if debug?
|
65
|
+
digest = OpenSSL::Digest.new( 'SHA3-256' )
|
66
|
+
digest.update( message )
|
67
|
+
digest.digest
|
68
|
+
else ## use "built-in" hash function from digest module
|
69
|
+
Digest::SHA3Lite.digest( message, 256 )
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
####
|
75
|
+
## helper
|
76
|
+
# def hash160( pubkey )
|
77
|
+
# binary = [pubkey].pack( "H*" ) # Convert to binary first before hashing
|
78
|
+
# sha256 = Digest::SHA256.digest( binary )
|
79
|
+
# ripemd160 = Digest::RMD160.digest( sha256 )
|
80
|
+
# ripemd160.unpack( "H*" )[0] # Convert back to hex
|
81
|
+
# end
|
82
|
+
|
83
|
+
def self.hash160bin( input )
|
84
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
85
|
+
|
86
|
+
rmd160bin(sha256bin( message ))
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def self.hash256bin( input )
|
91
|
+
message = message( input ) ## "normalize" / convert to (binary) string
|
92
|
+
|
93
|
+
sha256bin(sha256bin( message ))
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
##############################
|
98
|
+
## helpers
|
99
|
+
def self.message( input ) ## convert input to (binary) string
|
100
|
+
if debug?
|
101
|
+
input_type = if input.is_a?( String )
|
102
|
+
"#{input.class.name}/#{input.encoding}"
|
103
|
+
else
|
104
|
+
input.class.name
|
105
|
+
end
|
106
|
+
puts " input: #{input} (#{input_type})"
|
107
|
+
end
|
108
|
+
|
109
|
+
message = if input.is_a?( Integer ) ## assume byte if single (unsigned) integer
|
110
|
+
raise ArgumentError, "expected unsigned byte (0-255) - got #{input} (0x#{input.to_s(16)}) - can't pack negative number; sorry" if input < 0
|
111
|
+
## note: pack - H (String) => hex string (high nibble first)
|
112
|
+
## todo/check: is there a better way to convert integer number to (binary) string!!!
|
113
|
+
[input.to_s(16)].pack('H*')
|
114
|
+
else ## assume (binary) string
|
115
|
+
input
|
116
|
+
end
|
117
|
+
|
118
|
+
if debug?
|
119
|
+
bytes = message.bytes
|
120
|
+
bin = bytes.map {|byte| byte.to_s(2).rjust(8, "0")}.join( ' ' )
|
121
|
+
hex = bytes.map {|byte| byte.to_s(16).rjust(2, "0")}.join( ' ' )
|
122
|
+
puts " #{pluralize( bytes.size, 'byte')}: #{bytes.inspect}"
|
123
|
+
puts " binary: #{bin}"
|
124
|
+
puts " hex: #{hex}"
|
125
|
+
end
|
126
|
+
|
127
|
+
message
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.pluralize( count, noun )
|
131
|
+
count == 1 ? "#{count} #{noun}" : "#{count} #{noun}s"
|
132
|
+
end
|
133
|
+
|
134
|
+
end # module Metal
|
135
|
+
end # module Crypto
|
data/lib/crypto-lite/sign_rsa.rb
CHANGED
@@ -1,29 +1,29 @@
|
|
1
|
-
module Crypto
|
2
|
-
|
3
|
-
|
4
|
-
module RSA
|
5
|
-
def self.generate_keys ## todo/check: add a generate alias - why? why not?
|
6
|
-
key_pair = OpenSSL::PKey::RSA.new( 2048 )
|
7
|
-
private_key = key_pair.export
|
8
|
-
public_key = key_pair.public_key.export
|
9
|
-
|
10
|
-
[private_key, public_key]
|
11
|
-
end
|
12
|
-
|
13
|
-
|
14
|
-
def self.sign( plaintext, private_key )
|
15
|
-
private_key = OpenSSL::PKey::RSA.new( private_key ) ## note: convert/wrap into to obj from exported text format
|
16
|
-
Base64.encode64( private_key.private_encrypt( plaintext ))
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.decrypt( ciphertext, public_key )
|
20
|
-
public_key = OpenSSL::PKey::RSA.new( public_key ) ## note: convert/wrap into to obj from exported text format
|
21
|
-
public_key.public_decrypt( Base64.decode64( ciphertext ))
|
22
|
-
end
|
23
|
-
|
24
|
-
|
25
|
-
def self.valid_signature?( plaintext, ciphertext, public_key )
|
26
|
-
plaintext == decrypt( ciphertext, public_key )
|
27
|
-
end
|
28
|
-
end # module RSA
|
29
|
-
end # module Crypto
|
1
|
+
module Crypto
|
2
|
+
|
3
|
+
|
4
|
+
module RSA
|
5
|
+
def self.generate_keys ## todo/check: add a generate alias - why? why not?
|
6
|
+
key_pair = OpenSSL::PKey::RSA.new( 2048 )
|
7
|
+
private_key = key_pair.export
|
8
|
+
public_key = key_pair.public_key.export
|
9
|
+
|
10
|
+
[private_key, public_key]
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def self.sign( plaintext, private_key )
|
15
|
+
private_key = OpenSSL::PKey::RSA.new( private_key ) ## note: convert/wrap into to obj from exported text format
|
16
|
+
Base64.encode64( private_key.private_encrypt( plaintext ))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.decrypt( ciphertext, public_key )
|
20
|
+
public_key = OpenSSL::PKey::RSA.new( public_key ) ## note: convert/wrap into to obj from exported text format
|
21
|
+
public_key.public_decrypt( Base64.decode64( ciphertext ))
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def self.valid_signature?( plaintext, ciphertext, public_key )
|
26
|
+
plaintext == decrypt( ciphertext, public_key )
|
27
|
+
end
|
28
|
+
end # module RSA
|
29
|
+
end # module Crypto
|
data/lib/crypto-lite/version.rb
CHANGED
@@ -1,23 +1,23 @@
|
|
1
|
-
|
2
|
-
module CryptoLite
|
3
|
-
|
4
|
-
MAJOR = 0
|
5
|
-
MINOR = 3
|
6
|
-
PATCH =
|
7
|
-
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
8
|
-
|
9
|
-
def self.version
|
10
|
-
VERSION
|
11
|
-
end
|
12
|
-
|
13
|
-
def self.banner
|
14
|
-
"crypto-lite/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.root
|
18
|
-
File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
|
19
|
-
end
|
20
|
-
|
21
|
-
end # module CryptoLite
|
22
|
-
|
23
|
-
|
1
|
+
|
2
|
+
module CryptoLite
|
3
|
+
|
4
|
+
MAJOR = 0
|
5
|
+
MINOR = 3
|
6
|
+
PATCH = 1
|
7
|
+
VERSION = [MAJOR,MINOR,PATCH].join('.')
|
8
|
+
|
9
|
+
def self.version
|
10
|
+
VERSION
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.banner
|
14
|
+
"crypto-lite/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.root
|
18
|
+
File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
|
19
|
+
end
|
20
|
+
|
21
|
+
end # module CryptoLite
|
22
|
+
|
23
|
+
|