pluginaweek-encrypted_strings 0.3.2
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/CHANGELOG.rdoc +59 -0
- data/LICENSE +20 -0
- data/README.rdoc +88 -0
- data/Rakefile +96 -0
- data/init.rb +1 -0
- data/lib/encrypted_strings.rb +7 -0
- data/lib/encrypted_strings/asymmetric_cipher.rb +185 -0
- data/lib/encrypted_strings/cipher.rb +17 -0
- data/lib/encrypted_strings/extensions/string.rb +205 -0
- data/lib/encrypted_strings/sha_cipher.rb +67 -0
- data/lib/encrypted_strings/symmetric_cipher.rb +101 -0
- data/test/asymmetric_cipher_test.rb +183 -0
- data/test/cipher_test.rb +15 -0
- data/test/keys/encrypted_private +12 -0
- data/test/keys/private +9 -0
- data/test/keys/public +4 -0
- data/test/sha_cipher_test.rb +82 -0
- data/test/string_test.rb +222 -0
- data/test/symmetric_cipher_test.rb +99 -0
- data/test/test_helper.rb +4 -0
- metadata +79 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
== master
|
2
|
+
|
3
|
+
== 0.3.2 / 2009-01-11
|
4
|
+
|
5
|
+
* Use Array#pack/String#unpack instead of Base64 to be compatible with Ruby 1.9+
|
6
|
+
|
7
|
+
== 0.3.1 / 2008-12-4
|
8
|
+
|
9
|
+
* Fix symmetric ciphers not working on Ruby 1.8.6 and below
|
10
|
+
|
11
|
+
== 0.3.0 / 2008-12-14
|
12
|
+
|
13
|
+
* Remove the PluginAWeek namespace
|
14
|
+
|
15
|
+
== 0.2.1 / 2008-12-04
|
16
|
+
|
17
|
+
* Fix class-level defaults not working when inherited
|
18
|
+
|
19
|
+
== 0.2.0 / 2008-12-01
|
20
|
+
|
21
|
+
* Remove AsymmetricEncryptor.default_algorithm, instead relying on SymmetricEncryptor.default_algorithm
|
22
|
+
* Rename NoKeyError to NoPasswordError
|
23
|
+
* Rename Encryptors to Ciphers
|
24
|
+
* Remove deprecated SymmetricEncryptor#key option
|
25
|
+
* Require that symmetric encryption be PKCS #5 compliant
|
26
|
+
|
27
|
+
== 0.1.1 / 2008-12-01
|
28
|
+
|
29
|
+
* Fix non-compliant PKCS #5 algorithm being used for symmetric encryption. Use :pkcs5_compliant => true for better security.
|
30
|
+
* Rename SymmetricEncryptor#key to #password
|
31
|
+
* Fix deprecation messages for Cipher#encrypt/decrypt
|
32
|
+
|
33
|
+
== 0.1.0 / 2008-07-06
|
34
|
+
|
35
|
+
* Remove dependency on active_support
|
36
|
+
|
37
|
+
== 0.0.5 / 2008-07-05
|
38
|
+
|
39
|
+
* Add automatic stringification of salts for SHA encryption
|
40
|
+
* Fix not resetting the encryptor after calling decrypt!
|
41
|
+
|
42
|
+
== 0.0.4 / 2008-05-05
|
43
|
+
|
44
|
+
* Updated documentation
|
45
|
+
|
46
|
+
== 0.0.3 / 2007-09-18
|
47
|
+
|
48
|
+
* Remove gem dependency on activesupport
|
49
|
+
|
50
|
+
== 0.0.2 / 2007-08-23
|
51
|
+
|
52
|
+
* Fix not allowing the decryption mode to be overriden if the string already has an encryptor
|
53
|
+
* Convert dos newlines to unix newlines
|
54
|
+
|
55
|
+
== 0.0.1 / 2007-08-05
|
56
|
+
|
57
|
+
* Official public release
|
58
|
+
* Add api documentation
|
59
|
+
* Refactor unit test names
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2005 Rick Olson, 2006-2009 Aaron Pfeifer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= encrypted_strings
|
2
|
+
|
3
|
+
+encrypted_strings+ provides dead-simple string encryption/decryption syntax.
|
4
|
+
|
5
|
+
== Resources
|
6
|
+
|
7
|
+
API
|
8
|
+
|
9
|
+
* http://api.pluginaweek.org/encrypted_strings
|
10
|
+
|
11
|
+
Bugs
|
12
|
+
|
13
|
+
* http://pluginaweek.lighthouseapp.com/projects/13270-encrypted_strings
|
14
|
+
|
15
|
+
Development
|
16
|
+
|
17
|
+
* http://github.com/pluginaweek/encrypted_strings
|
18
|
+
|
19
|
+
Source
|
20
|
+
|
21
|
+
* git://github.com/pluginaweek/encrypted_strings.git
|
22
|
+
|
23
|
+
== Description
|
24
|
+
|
25
|
+
Encrypting and decrypting data is not exactly the most straightforward and DRY
|
26
|
+
way. encrypted_strings improves the syntax and reduces the complexity, adding
|
27
|
+
straightforward support for encrypting values using SHA-1, Symmetric, and
|
28
|
+
Asymmetric ciphers.
|
29
|
+
|
30
|
+
== Usage
|
31
|
+
|
32
|
+
=== SHA Encryption
|
33
|
+
|
34
|
+
>> password = 'shhhh'
|
35
|
+
=> "shhhh"
|
36
|
+
>> encrypted_password = password.encrypt
|
37
|
+
=> "66c85d26dadde7e1db27e15a0776c921e27143bd"
|
38
|
+
>> encrypted_password.class
|
39
|
+
=> String
|
40
|
+
>> encrypted_password.cipher
|
41
|
+
=> #<EncryptedStrings::ShaCipher:0x2b9238889460 @salt="salt">
|
42
|
+
>> encrypted_password == 'shhhh'
|
43
|
+
=> true
|
44
|
+
>> encrypted_password.decrypt
|
45
|
+
NotImplementedError: Decryption is not supported using a(n) EncryptedStrings::ShaCipher
|
46
|
+
from ./script/../config/../config/../vendor/plugins/encrypted_strings/lib/encrypted_strings/cipher.rb:13:in `decrypt'
|
47
|
+
from ./script/../config/../config/../vendor/plugins/encrypted_strings/lib/encrypted_strings/extensions/string.rb:52:in `decrypt'
|
48
|
+
from (irb):40
|
49
|
+
|
50
|
+
When encrypt is called, it creates a +cipher+ instance which is used for
|
51
|
+
future encryption and decryption of the string. The default cipher uses
|
52
|
+
SHA-1 encryption. For ciphers that do not support decryption, equality with
|
53
|
+
other strings is tested by encrypting the other string and checking whether the
|
54
|
+
resulting encrypted value is the same.
|
55
|
+
|
56
|
+
=== Symmetric Encryption
|
57
|
+
|
58
|
+
>> password = 'shhhh'
|
59
|
+
=> "shhhh"
|
60
|
+
>> crypted_password = password.encrypt(:symmetric, :password => 'secret_key')
|
61
|
+
=> "qSg8vOo6QfU=\n"
|
62
|
+
>> crypted_password.class
|
63
|
+
=> String
|
64
|
+
>> crypted_password == 'shhhh'
|
65
|
+
=> true
|
66
|
+
>> password = crypted_password.decrypt
|
67
|
+
=> "shhhh"
|
68
|
+
|
69
|
+
=== Asymmetric encryption
|
70
|
+
|
71
|
+
>> password = 'shhhh'
|
72
|
+
=> "shhhh"
|
73
|
+
>> crypted_password = password.encrypt(:asymmetric, :public_key_file => './public.key', :private_key_file => './private.key')
|
74
|
+
=> "NEwVzcikYUKfS8HTc9L9eg/dMxBCLZ/nFr7J1aQYjkl3I2MPUD0lmjr/saC6\nTJEPwOl60Ki24H8TUwnGtZy14A==\n"
|
75
|
+
>> crypted_password.class
|
76
|
+
=> String
|
77
|
+
>> crypted_password == 'shhhh'
|
78
|
+
=> true
|
79
|
+
>> password = crypted_password.decrypt
|
80
|
+
=> "shhhh"
|
81
|
+
|
82
|
+
== Dependencies
|
83
|
+
|
84
|
+
None.
|
85
|
+
|
86
|
+
== References
|
87
|
+
|
88
|
+
* Rick Olson - sentry[http://github.com/technoweenie/sentry]
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
spec = Gem::Specification.new do |s|
|
7
|
+
s.name = 'encrypted_strings'
|
8
|
+
s.version = '0.3.2'
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.summary = 'Dead-simple string encryption/decryption syntax'
|
11
|
+
s.description = s.summary
|
12
|
+
|
13
|
+
s.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc)
|
14
|
+
s.require_path = 'lib'
|
15
|
+
s.has_rdoc = true
|
16
|
+
s.test_files = Dir['test/**/*_test.rb']
|
17
|
+
|
18
|
+
s.author = 'Aaron Pfeifer'
|
19
|
+
s.email = 'aaron@pluginaweek.org'
|
20
|
+
s.homepage = 'http://www.pluginaweek.org'
|
21
|
+
s.rubyforge_project = 'pluginaweek'
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Default: run all tests.'
|
25
|
+
task :default => :test
|
26
|
+
|
27
|
+
desc "Test the #{spec.name} plugin."
|
28
|
+
Rake::TestTask.new(:test) do |t|
|
29
|
+
t.libs << 'lib'
|
30
|
+
t.test_files = spec.test_files
|
31
|
+
t.verbose = true
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
require 'rcov/rcovtask'
|
36
|
+
namespace :test do
|
37
|
+
desc "Test the #{spec.name} plugin with Rcov."
|
38
|
+
Rcov::RcovTask.new(:rcov) do |t|
|
39
|
+
t.libs << 'lib'
|
40
|
+
t.test_files = spec.test_files
|
41
|
+
t.rcov_opts << '--exclude="^(?!lib/)"'
|
42
|
+
t.verbose = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
rescue LoadError
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Generate documentation for the #{spec.name} plugin."
|
49
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = spec.name
|
52
|
+
rdoc.template = '../rdoc_template.rb'
|
53
|
+
rdoc.options << '--line-numbers'
|
54
|
+
rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Generate a gemspec file.'
|
58
|
+
task :gemspec do
|
59
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
60
|
+
f.write spec.to_ruby
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Rake::GemPackageTask.new(spec) do |p|
|
65
|
+
p.gem_spec = spec
|
66
|
+
p.need_tar = true
|
67
|
+
p.need_zip = true
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'Publish the beta gem.'
|
71
|
+
task :pgem => [:package] do
|
72
|
+
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
|
73
|
+
end
|
74
|
+
|
75
|
+
desc 'Publish the API documentation.'
|
76
|
+
task :pdoc => [:rdoc] do
|
77
|
+
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'Publish the API docs and gem'
|
81
|
+
task :publish => [:pgem, :pdoc, :release]
|
82
|
+
|
83
|
+
desc 'Publish the release files to RubyForge.'
|
84
|
+
task :release => [:gem, :package] do
|
85
|
+
require 'rubyforge'
|
86
|
+
|
87
|
+
ruby_forge = RubyForge.new.configure
|
88
|
+
ruby_forge.login
|
89
|
+
|
90
|
+
%w(gem tgz zip).each do |ext|
|
91
|
+
file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
|
92
|
+
puts "Releasing #{File.basename(file)}..."
|
93
|
+
|
94
|
+
ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
|
95
|
+
end
|
96
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'encrypted_strings'
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module EncryptedStrings
|
2
|
+
# Indicates no public key was found
|
3
|
+
class NoPublicKeyError < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Indicates no private key was found
|
7
|
+
class NoPrivateKeyError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Encryption in which the keys used to encrypt/decrypt come in pairs. Also
|
11
|
+
# known as public key encryption. Anything that's encrypted using the
|
12
|
+
# public key can only be decrypted with the same algorithm and a matching
|
13
|
+
# private key. Any message that is encrypted with the private key can only
|
14
|
+
# be decrypted with the matching public key.
|
15
|
+
#
|
16
|
+
# Source: http://support.microsoft.com/kb/246071
|
17
|
+
#
|
18
|
+
# == Encrypting
|
19
|
+
#
|
20
|
+
# To encrypt a string using an asymmetric cipher, the location of the
|
21
|
+
# public key file must be specified. You can define the default for this
|
22
|
+
# value like so:
|
23
|
+
#
|
24
|
+
# EncryptedStrings::AsymmetricCipher.default_public_key_file = './public.key'
|
25
|
+
#
|
26
|
+
# If these configuration options are not passed in to #encrypt, then the
|
27
|
+
# default values will be used. You can override the default values like so:
|
28
|
+
#
|
29
|
+
# password = 'shhhh'
|
30
|
+
# password.encrypt(:asymmetric, :public_key_file => './encrypted_public.key') # => "INy95irZ8AlHmvc6ZAF/ARsTpbqPIB/4bEAKKOebjsayB7NYWtIzpswvzxqf\nNJ5yyuvxfMODrcg7RimEMFkFlg==\n"
|
31
|
+
#
|
32
|
+
# An exception will be raised if either the public key file could not be
|
33
|
+
# found or the key could not decrypt the public key file.
|
34
|
+
#
|
35
|
+
# == Decrypting
|
36
|
+
#
|
37
|
+
# To decrypt a string using an asymmetric cipher, the location of the
|
38
|
+
# private key file must be specified. If this file is itself encrypted, you
|
39
|
+
# must also specify the algorithm and password used to seed the symmetric
|
40
|
+
# algorithm that will decrypt the plublic key file. You can define defaults
|
41
|
+
# for these values like so:
|
42
|
+
#
|
43
|
+
# EncryptedStrings::AsymmetricCipher.default_private_key_file = './private.key'
|
44
|
+
# EncryptedStrings::SymmetricCipher.default_algorithm = 'DES-EDE3-CBC'
|
45
|
+
# EncryptedStrings::SymmetricCipher.default_password = 'secret'
|
46
|
+
#
|
47
|
+
# If these configuration options are not passed in to #decrypt, then the
|
48
|
+
# default values will be used. You can override the default values like so:
|
49
|
+
#
|
50
|
+
# password = "INy95irZ8AlHmvc6ZAF/ARsTpbqPIB/4bEAKKOebjsayB7NYWtIzpswvzxqf\nNJ5yyuvxfMODrcg7RimEMFkFlg==\n"
|
51
|
+
# password.decrypt(:asymmetric, :public_key_file => './encrypted_public.key', :password => 'secret') # => "shhhh"
|
52
|
+
#
|
53
|
+
# An exception will be raised if either the private key file could not be
|
54
|
+
# found or the password could not decrypt the private key file.
|
55
|
+
class AsymmetricCipher < Cipher
|
56
|
+
class << self
|
57
|
+
# The default private key to use during encryption. Default is nil.
|
58
|
+
attr_accessor :default_private_key_file
|
59
|
+
|
60
|
+
# The default public key to use during encryption. Default is nil.
|
61
|
+
attr_accessor :default_public_key_file
|
62
|
+
end
|
63
|
+
|
64
|
+
# Private key used for decrypting data
|
65
|
+
attr_reader :private_key_file
|
66
|
+
|
67
|
+
# Public key used for encrypting data
|
68
|
+
attr_reader :public_key_file
|
69
|
+
|
70
|
+
# The algorithm to use if the key files are encrypted themselves
|
71
|
+
attr_accessor :algorithm
|
72
|
+
|
73
|
+
# The password used during symmetric decryption of the key files
|
74
|
+
attr_accessor :password
|
75
|
+
|
76
|
+
# Creates a new cipher that uses an asymmetric encryption strategy.
|
77
|
+
#
|
78
|
+
# Configuration options:
|
79
|
+
# * <tt>:private_key_file</tt> - Encrypted private key file
|
80
|
+
# * <tt>:public_key_file</tt> - Public key file
|
81
|
+
# * <tt>:password</tt> - The password to use in the symmetric cipher
|
82
|
+
# * <tt>:algorithm</tt> - Algorithm to use symmetrically encrypted strings
|
83
|
+
def initialize(options = {})
|
84
|
+
invalid_options = options.keys - [:private_key_file, :public_key_file, :algorithm, :password]
|
85
|
+
raise ArgumentError, "Unknown key(s): #{invalid_options.join(", ")}" unless invalid_options.empty?
|
86
|
+
|
87
|
+
options = {
|
88
|
+
:private_key_file => AsymmetricCipher.default_private_key_file,
|
89
|
+
:public_key_file => AsymmetricCipher.default_public_key_file
|
90
|
+
}.merge(options)
|
91
|
+
|
92
|
+
@public_key = @private_key = nil
|
93
|
+
|
94
|
+
self.private_key_file = options[:private_key_file]
|
95
|
+
self.public_key_file = options[:public_key_file]
|
96
|
+
raise ArgumentError, 'At least one key file must be specified (:private_key_file or :public_key_file)' unless private_key_file || public_key_file
|
97
|
+
|
98
|
+
self.algorithm = options[:algorithm]
|
99
|
+
self.password = options[:password]
|
100
|
+
|
101
|
+
super()
|
102
|
+
end
|
103
|
+
|
104
|
+
# Encrypts the given data. If no public key file has been specified, then
|
105
|
+
# a NoPublicKeyError will be raised.
|
106
|
+
def encrypt(data)
|
107
|
+
raise NoPublicKeyError, "Public key file: #{public_key_file}" unless public?
|
108
|
+
|
109
|
+
encrypted_data = public_rsa.public_encrypt(data)
|
110
|
+
[encrypted_data].pack('m')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Decrypts the given data. If no private key file has been specified, then
|
114
|
+
# a NoPrivateKeyError will be raised.
|
115
|
+
def decrypt(data)
|
116
|
+
raise NoPrivateKeyError, "Private key file: #{private_key_file}" unless private?
|
117
|
+
|
118
|
+
decrypted_data = data.unpack('m')[0]
|
119
|
+
private_rsa.private_decrypt(decrypted_data)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Sets the location of the private key and loads it
|
123
|
+
def private_key_file=(file)
|
124
|
+
@private_key_file = file and load_private_key
|
125
|
+
end
|
126
|
+
|
127
|
+
# Sets the location of the public key and loads it
|
128
|
+
def public_key_file=(file)
|
129
|
+
@public_key_file = file and load_public_key
|
130
|
+
end
|
131
|
+
|
132
|
+
# Does this cipher have a public key available?
|
133
|
+
def public?
|
134
|
+
return true if @public_key
|
135
|
+
|
136
|
+
load_public_key
|
137
|
+
!@public_key.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
# Does this cipher have a private key available?
|
141
|
+
def private?
|
142
|
+
return true if @private_key
|
143
|
+
|
144
|
+
load_private_key
|
145
|
+
!@private_key.nil?
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
# Loads the private key from the configured file
|
150
|
+
def load_private_key
|
151
|
+
@private_rsa = nil
|
152
|
+
|
153
|
+
if private_key_file && File.file?(private_key_file)
|
154
|
+
@private_key = File.read(private_key_file)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Loads the public key from the configured file
|
159
|
+
def load_public_key
|
160
|
+
@public_rsa = nil
|
161
|
+
|
162
|
+
if public_key_file && File.file?(public_key_file)
|
163
|
+
@public_key = File.read(public_key_file)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Retrieves the private RSA from the private key
|
168
|
+
def private_rsa
|
169
|
+
if password
|
170
|
+
options = {:password => password}
|
171
|
+
options[:algorithm] = algorithm if algorithm
|
172
|
+
|
173
|
+
private_key = @private_key.decrypt(:symmetric, options)
|
174
|
+
OpenSSL::PKey::RSA.new(private_key)
|
175
|
+
else
|
176
|
+
@private_rsa ||= OpenSSL::PKey::RSA.new(@private_key)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Retrieves the public RSA
|
181
|
+
def public_rsa
|
182
|
+
@public_rsa ||= OpenSSL::PKey::RSA.new(@public_key)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|