ccrypto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 708c818b1776a95812b03efc9cc90bffb7e75ef5fe3325c455dfd3b5ae8b52fe
4
+ data.tar.gz: f62763565bb50a8216d461e02dfb9737cb20867b8606c06e7f1200a161960d9a
5
+ SHA512:
6
+ metadata.gz: b35007b3c2e4316328d14fa4a2929f2c8b6be032468711586efb63aeaf4271d5b9714f8d8723b4f0e9871bc46b4bab4e9afcf2dc9a43a7679bd33377cf6fabcb
7
+ data.tar.gz: '08e2704618f620d371b4d1eebfbe4715b0516a88b1ab09a336d631bedfbccc7dbb96d19ea872918976c6909bdda81c1dbb0f700bf29474083a6ffa961ee42af2'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in ccrypto.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ccrypto (0.1.0)
5
+ teLogger
6
+ toolrack
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ base58 (0.2.3)
12
+ diff-lcs (1.5.0)
13
+ rake (13.0.6)
14
+ rspec (3.11.0)
15
+ rspec-core (~> 3.11.0)
16
+ rspec-expectations (~> 3.11.0)
17
+ rspec-mocks (~> 3.11.0)
18
+ rspec-core (3.11.0)
19
+ rspec-support (~> 3.11.0)
20
+ rspec-expectations (3.11.0)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.11.0)
23
+ rspec-mocks (3.11.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.11.0)
26
+ rspec-support (3.11.0)
27
+ teLogger (0.1.0)
28
+ tlogger (0.26.3)
29
+ toolrack (0.18.3)
30
+ base58
31
+ tlogger
32
+
33
+ PLATFORMS
34
+ x86_64-linux
35
+
36
+ DEPENDENCIES
37
+ ccrypto!
38
+ rake (~> 13.0)
39
+ rspec (~> 3.0)
40
+
41
+ BUNDLED WITH
42
+ 2.2.28
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Ccrypto
2
+
3
+ Ccrypto - Common Crypto is the attempt to normalize cryptography API between Ruby and Java, and possibly other runtime supported by Ruby.
4
+
5
+ It is rooted in Ruby because of its expressiveness.
6
+
7
+ This gem is mainly provide high level common elements for the implemented runtime to select a proper implementation.
8
+
9
+ This including all the classes under the lib/ccrypto/configs/ directory. Those are suppose to be parameter pass to the runtime implementation to pick the required implementation under that runtime.
10
+
11
+ Note this layer is suppose to be barebone native cryptographic algorithm API which should be just thin wrapper around the runtime cryptographic library
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'ccrypto'
19
+
20
+ # select runtime
21
+ # if Ruby runtime backed by OpenSSL
22
+ # https://github.com/cameronian/ccrypto-ruby
23
+ gem 'ccrypto-ruby'
24
+
25
+ # or on Java runtime backed by JCE + bouncycastle
26
+ # https://github.com/cameronian/ccrypto-java
27
+ gem 'ccrypto-java'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ $ bundle install
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install ccrypto
37
+ $ gem install ccrypto-ruby # for Ruby runtime
38
+ $ gem install ccrypto-java # for Java runtime
39
+
40
+
41
+ ## Usage
42
+
43
+ Detail usage refers to spec files in ccrypto-ruby and ccrypto-java.
44
+
45
+ ## Development hint
46
+
47
+ To add a different provider, runtime implementation requires to implement a provider class that has the following methods:
48
+
49
+ * All static method
50
+ * provider\_name() - returns string indicating the provider
51
+ * algo\_instance(\*args,&block) - return specific implementation class for the given arguments
52
+ * asn1\_engine(\*args, &block) - return ASN1 engine from the runtime for given arguments
53
+ * util\_instance(\*args, &block) - return utilities from the runtime. For example memory buffer, compression engine, data conversion etc.
54
+
55
+
56
+ In the main entry for the runtime implementation, register this provider by calling:
57
+ ```ruby
58
+ Ccrypto::Provider.instance.register(<provider class>)
59
+ ```
60
+
61
+ That's it.
62
+
63
+ Refers to [Ccrypto ruby runtime](https://github.com/cameronian/ccrypto-ruby) or [Ccrypto Java runtime](https://github.com/cameronian/ccrypto-java) for more info.
64
+
65
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ require 'devops_assist'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "ccrypto"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/ccrypto.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ccrypto/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ccrypto"
7
+ spec.version = Ccrypto::VERSION
8
+ spec.authors = ["Ian"]
9
+ spec.email = ["cameronian0@protonmail.com"]
10
+
11
+ spec.summary = ""
12
+ spec.description = ""
13
+ spec.homepage = "https://github.com/cameronian/ccrypto"
14
+ spec.required_ruby_version = ">= 2.4.0"
15
+
16
+ #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
17
+
18
+ #spec.metadata["homepage_uri"] = spec.homepage
19
+ #spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
20
+ #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_dependency 'teLogger'
34
+ spec.add_dependency 'toolrack'
35
+
36
+ spec.add_dependency 'activesupport'
37
+
38
+ spec.add_development_dependency 'devops_assist'
39
+
40
+ # Uncomment to register a new dependency of your gem
41
+ # spec.add_dependency "example-gem", "~> 1.0"
42
+
43
+ # For more information and examples about making a new gem, checkout our
44
+ # guide at: https://bundler.io/guides/creating_gem.html
45
+ end
@@ -0,0 +1,11 @@
1
+
2
+
3
+ module Ccrypto
4
+ class AlgoFactory
5
+
6
+ def self.engine(*args, &block)
7
+ Provider.instance.provider.algo_instance(*args, &block)
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+
2
+
3
+ module Ccrypto
4
+ class ASN1
5
+
6
+ def self.engine(*args, &block)
7
+ Provider.instance.provider.asn1_engine(*args, &block)
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+
2
+
3
+ module Ccrypto
4
+ class ASN1Object
5
+ attr_reader :asn1_type
6
+
7
+ def initialize(type, asn1)
8
+ @asn1_type = type
9
+ @asn1 = asn1
10
+ end
11
+
12
+ def native_asn1
13
+ @asn1
14
+ end
15
+
16
+ def is_type?(type)
17
+ @asn1_type.to_s.downcase.to_sym == type.to_s.downcase.to_sym
18
+ end
19
+
20
+ def method_missing(mtd, *args, &block)
21
+ @asn1.send(mtd, *args, &block)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+
2
+
3
+ module Ccrypto
4
+ module AlgoConfig
5
+
6
+ module ClassMethods
7
+
8
+ end
9
+ def self.include(klass)
10
+ klass.extend(ClassMethods)
11
+ end
12
+
13
+ attr_accessor :provider_config
14
+ def provider_info(val)
15
+ @provider_config = val
16
+ self
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,150 @@
1
+
2
+
3
+ module Ccrypto
4
+
5
+ module CipherGCMMode
6
+ attr_accessor :auth_data, :auth_tag
7
+ end
8
+
9
+ class CipherConfig
10
+ include AlgoConfig
11
+ include TR::CondUtils
12
+
13
+ attr_accessor :algo, :key
14
+ attr_accessor :keysize, :mode, :padding
15
+ attr_accessor :iv, :ivLength
16
+ attr_accessor :cipherOps
17
+
18
+ def initialize(algo, opts = { }, &block)
19
+ @algo = algo
20
+
21
+ @logger = Tlogger.new
22
+ @logger.tag = :cipher_conf
23
+
24
+ if not_empty?(opts) and opts.is_a?(Hash)
25
+ @mode = opts[:mode]
26
+
27
+ if is_mode?(:gcm)
28
+ self.extend CipherGCMMode
29
+ @logger.debug "Extending GCM mode"
30
+
31
+ @auth_data = opts[:auth_data]
32
+ @auth_tag = opts[:auth_tag]
33
+
34
+ #p "auth data : #{@auth_data}"
35
+ end
36
+
37
+ @iv = opts[:iv]
38
+ @ivLength = opts[:ivLength] if is_empty?(@iv)
39
+
40
+ @key = opts[:key]
41
+ @keysize = opts[:keysize] if is_empty?(@key)
42
+
43
+ @padding = opts[:padding]
44
+
45
+ @cipherOps = opts[:cipherOps]
46
+ end
47
+
48
+ if block
49
+ @mode = block.call(:mode)
50
+
51
+ if is_mode?(:gcm)
52
+ self.extend CipherGCMMode
53
+ @logger.debug "Extending GCM mode"
54
+
55
+ @auth_data = block.call(:auth_data)
56
+ @auth_tag = block.call(:auth_tag)
57
+ end
58
+
59
+ @iv = block.call(:iv)
60
+ @ivLength = block.call(:ivLength) || 16 if @iv.nil?
61
+
62
+ @key = block.call(:key)
63
+ @keysize = block.call(:keysize) if @key.nil?
64
+
65
+ @padding = block.call(:padding)
66
+
67
+ @cipherOps = block.call(:cipherOps)
68
+ end
69
+
70
+ end
71
+
72
+ def has_iv?
73
+ not_empty?(@iv)
74
+ end
75
+
76
+ def has_key?
77
+ not_empty?(@key)
78
+ end
79
+
80
+ def is_algo?(algo)
81
+ if @algo.nil? or is_empty?(@algo)
82
+ false
83
+ else
84
+ (@algo.to_s.downcase =~ /#{algo}/) != nil
85
+ end
86
+ end
87
+
88
+ def is_mode?(mode)
89
+ if @mode.nil? or is_empty?(@mode)
90
+ false
91
+ else
92
+ (@mode.to_s.downcase =~ /#{mode.to_s}/) != nil
93
+ end
94
+ end
95
+
96
+ def encrypt_cipher_mode
97
+ @cipherOps = :encrypt
98
+ end
99
+ def is_encrypt_cipher_mode?
100
+ case @cipherOps
101
+ when :encrypt, :enc
102
+ true
103
+ else
104
+ false
105
+ end
106
+ end
107
+
108
+ def decrypt_cipher_mode
109
+ @cipherOps = :decrypt
110
+ end
111
+ def is_decrypt_cipher_mode?
112
+ case @cipherOps
113
+ when :decrypt, :dec
114
+ true
115
+ else
116
+ false
117
+ end
118
+ end
119
+
120
+ def to_s
121
+ "#{@algo}-#{@keysize}-#{@mode}-#{@padding}"
122
+ end
123
+
124
+ def logger
125
+ if @logger.nil?
126
+ @logger = Tlogger.new
127
+ @logger.tag = :cipher_conf
128
+ end
129
+ @logger
130
+ end
131
+ end
132
+
133
+ class DirectCipherConfig < CipherConfig
134
+ # str can be String or Hash
135
+ # If String it will be directly used by underlying
136
+ # engine with minimum parsing which means might not have other
137
+ # info
138
+ def initialize(str)
139
+ raise CipherConfigException, "Hash is expected" if not str.is_a?(Hash)
140
+ super(str[:algo], str)
141
+ end
142
+
143
+ end
144
+
145
+ class CipherEngineConfig < CipherConfig
146
+ # engine that is discovered by cipher engine
147
+ # Means can directly use the object
148
+ end
149
+
150
+ end
@@ -0,0 +1,16 @@
1
+
2
+ require_relative 'algo_config'
3
+
4
+ module Ccrypto
5
+ class CompressionConfig
6
+ include AlgoConfig
7
+
8
+ attr_accessor :level, :strategy
9
+
10
+ def initialize
11
+ @level = :default
12
+ @strategy = :default
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,98 @@
1
+
2
+
3
+ module Ccrypto
4
+ class DigestConfig
5
+ include AlgoConfig
6
+ include TR::CondUtils
7
+
8
+ attr_accessor :algo, :outBitLength
9
+ attr_accessor :hardInBitLength
10
+ attr_accessor :nativeDigestEngine
11
+ def initialize(algo, outBitLen, opts = { })
12
+ @algo = algo
13
+ @outBitLength = outBitLen
14
+ if not_empty?(opts)
15
+ @provider_config = opts[:provider_config]
16
+ @hardInBitLength = opts[:hard_in_bit_length]
17
+ end
18
+ end
19
+
20
+ def has_hard_in_bit_length?
21
+ (not @hardInBitLength.nil?) or @hardInBitLength.to_i > 0
22
+ end
23
+ end
24
+
25
+ SHA1 = DigestConfig.new(:sha1, 160)
26
+ SHA224 = DigestConfig.new(:sha224, 224)
27
+ SHA256 = DigestConfig.new(:sha256, 256)
28
+ SHA384 = DigestConfig.new(:sha384, 384)
29
+ SHA512 = DigestConfig.new(:sha512, 512)
30
+ SHA512_224 = DigestConfig.new(:sha512_224, 224)
31
+ SHA512_256 = DigestConfig.new(:sha512_256, 256)
32
+
33
+ SHA3_224 = DigestConfig.new(:sha3_224, 224)
34
+ SHA3_256 = DigestConfig.new(:sha3_256, 256)
35
+ SHA3_384 = DigestConfig.new(:sha3_384, 384)
36
+ SHA3_512 = DigestConfig.new(:sha3_512, 512)
37
+
38
+ BLAKE2b160 = DigestConfig.new(:blake2b160, 160)
39
+ BLAKE2b256 = DigestConfig.new(:blake2b256, 256)
40
+ BLAKE2b384 = DigestConfig.new(:blake2b384, 384)
41
+ BLAKE2b512 = DigestConfig.new(:blake2b512, 512)
42
+
43
+ BLAKE2s128 = DigestConfig.new(:blake2s128, 128)
44
+ BLAKE2s160 = DigestConfig.new(:blake2s160, 160)
45
+ BLAKE2s224 = DigestConfig.new(:blake2s224, 224)
46
+ BLAKE2s256 = DigestConfig.new(:blake2s256, 256)
47
+
48
+ DSTU7564_256 = DigestConfig.new(:dstu7564_256, 256)
49
+ KUPYNA_256 = DSTU7564_256
50
+ DSTU7564_384 = DigestConfig.new(:dstu7564_384, 384)
51
+ KUPYNA_384 = DSTU7564_384
52
+ DSTU7564_512 = DigestConfig.new(:dstu7564_512, 512)
53
+ KUPYNA_512 = DSTU7564_512
54
+
55
+ GOSH3411 = DigestConfig.new(:gosh3411, 256)
56
+ GOSH3411_2012_256 = DigestConfig.new(:gosh3411_2012_256, 256)
57
+ GOSH3411_2012_512 = DigestConfig.new(:gosh3411_2012_512, 512)
58
+
59
+ HARAKA256 = DigestConfig.new(:haraka256, 256, { hard_in_bit_length: 256 })
60
+ HARAKA512 = DigestConfig.new(:haraka512, 256, { hard_in_bit_length: 512 })
61
+
62
+ KECCAK224 = DigestConfig.new(:keccak224, 224)
63
+ KECCAK256 = DigestConfig.new(:keccak256, 256)
64
+ KECCAK288 = DigestConfig.new(:keccak288, 288)
65
+ KECCAK384 = DigestConfig.new(:keccak384, 384)
66
+ KECCAK512 = DigestConfig.new(:keccak512, 512)
67
+
68
+ RIPEMD128 = DigestConfig.new(:ripemd128, 128)
69
+ RIPEMD160 = DigestConfig.new(:ripemd160, 160)
70
+ RIPEMD256 = DigestConfig.new(:ripemd256, 256)
71
+ RIPEMD320 = DigestConfig.new(:ripemd320, 320)
72
+
73
+ SHAKE128_256 = DigestConfig.new(:shake128_256, 256)
74
+ SHAKE256_512 = DigestConfig.new(:shake256_512, 512)
75
+ SHAKE128 = DigestConfig.new(:shake128, 128)
76
+ SHAKE256 = DigestConfig.new(:shake256, 256)
77
+
78
+ SKEIN1024_1024 = DigestConfig.new(:skein1024_1024, 1024)
79
+ SKEIN1024_384 = DigestConfig.new(:skein1024_384, 384)
80
+ SKEIN1024_512 = DigestConfig.new(:skein1024_512, 512)
81
+
82
+ SKEIN256_128 = DigestConfig.new(:skein256_128, 128)
83
+ SKEIN256_160 = DigestConfig.new(:skein256_160, 160)
84
+ SKEIN256_224 = DigestConfig.new(:skein256_224, 224)
85
+ SKEIN256_256 = DigestConfig.new(:skein256_256, 256)
86
+
87
+ SKEIN512_128 = DigestConfig.new(:skein512_128, 128)
88
+ SKEIN512_160 = DigestConfig.new(:skein512_160, 160)
89
+ SKEIN512_224 = DigestConfig.new(:skein512_224, 224)
90
+ SKEIN512_256 = DigestConfig.new(:skein512_256, 256)
91
+ SKEIN512_384 = DigestConfig.new(:skein512_384, 384)
92
+ SKEIN512_512 = DigestConfig.new(:skein512_512, 512)
93
+
94
+ SM3 = DigestConfig.new(:sm3, 256)
95
+
96
+ WHIRLPOOL = DigestConfig.new(:whirlpool, 512)
97
+
98
+ end
@@ -0,0 +1,13 @@
1
+
2
+
3
+ module Ccrypto
4
+ class HMACConfig
5
+ include AlgoConfig
6
+
7
+ attr_accessor :key, :digest
8
+
9
+ def initialize
10
+ @digest = :sha256
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,67 @@
1
+
2
+
3
+ module Ccrypto
4
+ class KDFConfig
5
+ include AlgoConfig
6
+ attr_accessor :algo, :outBitLength
7
+ end
8
+
9
+ class ScryptConfig < KDFConfig
10
+ attr_accessor :cost, :blockSize, :parallel, :salt
11
+
12
+ # https://stackoverflow.com/questions/11126315/what-are-optimal-scrypt-work-factors
13
+ # Specific good explanation:
14
+ # https://stackoverflow.com/a/30308723/3625825
15
+ # Memory requirement : 128 bytes x Cost Factor x block size
16
+ # at cost factor 2^14 = 16384 == 128 bytes x 16384 (cost) x 8 (block size) === 16 MB
17
+ # if at cost factor 2 ^ 14 = 16384 == 128 x 16384 (cost) x 512 (block size) === 1 GB
18
+ # Tuning the cost & block size can change how much memory needed to process this
19
+ # for EACH TIME PROCESSING...
20
+ #
21
+ # Legitimate user only need to do once
22
+ # Attacker using brute force may not be feasiable anymore if the value is high
23
+ #
24
+ # BC source code indicated
25
+ # Cost parameter bound >= 1 and < 65536 (2^16) (value must be power of 2, i.e 2^1 = 2, 2^2 = 4 etc)
26
+ # the actual is the max value should be 2^(128*blocksize/8)
27
+ # block size = 1 == 2^(128*1/8) == 2^(128/8) == 2^16
28
+ # block size = 2 == 2^(128*2/8) == 2^32 == 4 GB
29
+ # If want higher value, change block size 2^16 is min in this case because block must be 1 and above
30
+ # block size>= 1
31
+ # parallelization must be > 1 and < Integer.MAX_VALUE / (128 * parallelization * 8)
32
+ # tested on Java 8 Integer.MAX_VALUE = 2GB (2^31)
33
+ # if parallelization value == 1, the supported parallel is 2048,000
34
+ # Hmm that's why I think nobody use more then 1?
35
+ #
36
+ # this config shall be 16 MB per process
37
+ #costParam = opts[:costParam] || 2 ** 14 # 2 ^ 16
38
+ #blockSize = opts[:blockSize] || 8
39
+ # this one also 16 MB per process
40
+ # but apparently there are saying higher r is better
41
+ # https://stackoverflow.com/a/33297994/3625825
42
+
43
+ def initialize
44
+ @cost = 16384 # 2**14
45
+ @blockSize = 8
46
+ @parallel = 1
47
+ @salt = SecureRandom.random_bytes(16)
48
+ end
49
+ end
50
+
51
+ class HKDFConfig < KDFConfig
52
+ attr_accessor :salt, :info, :digest
53
+ def initialize
54
+ @salt = SecureRandom.random_bytes(16)
55
+ @digest = :sha256
56
+ end
57
+ end
58
+
59
+ class PBKDF2Config < KDFConfig
60
+ attr_accessor :salt, :digest, :iter
61
+ def initialize
62
+ @salt = SecureRandom.random_bytes(16)
63
+ @digest = :sha256
64
+ @iter = rand(200000..400000)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+
2
+
3
+ module Ccrypto
4
+ class KeyConfig
5
+ include AlgoConfig
6
+
7
+ attr_accessor :algo, :keysize
8
+
9
+ def to_s
10
+ "#{@algo}/#{@keysize}"
11
+ end
12
+
13
+ end
14
+ end