filesafe 1.0.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.
Files changed (5) hide show
  1. data/README.txt +57 -0
  2. data/Rakefile +41 -0
  3. data/VERSION.txt +1 -0
  4. data/bin/filesafe +351 -0
  5. metadata +96 -0
@@ -0,0 +1,57 @@
1
+ DESCRIPTION
2
+
3
+ FileSafe
4
+
5
+ Written by Aaron D. Gifford - http://www.aarongifford.com/
6
+
7
+ A simple Ruby script for encrypting/decrypting files using 256-bit AES
8
+ and a master key derived from a password/passphrase via the PBKDF2
9
+ function.
10
+
11
+
12
+ I wrote this script for use on several systems where I needed to
13
+ regularly encrypt/decrypt one or more files using a password or
14
+ passphrase. The method used should be reasonably secure for the uses I
15
+ required. I have NOT adapted the script (yet) for non-POSIX
16
+ environments (Windows) however.
17
+
18
+ This script was written and tested using Ruby 1.9.x. No attempts to
19
+ adapt or test it under earlier Ruby versions have been made.
20
+
21
+
22
+ LICENSE
23
+
24
+ This script is licensed under an MIT-style license. See the license header at the top of the script source code.
25
+
26
+
27
+ REQUIREMENTS
28
+
29
+ This script requires or relies on:
30
+ openssl -- encryption/HMAC/hash algorithms
31
+ securerandom -- cryptographically secure random data
32
+ tempfile -- for temporary file creation
33
+
34
+ It uses the following gems:
35
+ pbkdf2 -- for the password-based key derivitive function PBKDF2
36
+ highline -- for reading a password/passphrase from a terminal
37
+
38
+
39
+ WEB SITE
40
+
41
+ The latest version can be found at the author's web site:
42
+
43
+ * http://www.aarongifford.com/computers/filesafe/index.html
44
+
45
+
46
+ SUGGESTIONS / BUGS
47
+
48
+ Please report bugs by going to the author's web site and clicking on the
49
+ "Contact Me" link in the left-hand menu. The direct URL is:
50
+
51
+ * http://www.aarongifford.com/leaveanote.html
52
+
53
+
54
+
55
+ Thank you!
56
+ -- Aaron D. Gifford
57
+
@@ -0,0 +1,41 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rake/rdoctask'
3
+
4
+ gemspec = Gem::Specification.new do |spec|
5
+ spec.name = 'filesafe'
6
+ spec.version = File.open('VERSION.txt','r').to_a.join.strip
7
+ spec.date = File.mtime('VERSION.txt')
8
+ spec.author = 'Aaron D. Gifford'
9
+ spec.homepage = 'http://www.aarongifford.com/computers/filesafe/'
10
+ spec.summary = 'Encrypt/decrypt files with a random 256-bit AES key secured by a passphrase derived master key using PBKDF2'
11
+ spec.description = 'A utility script for encrypting and decrypting files using a randomly generated 256-bit AES key and initialization vector secured using the PBKDF2 password/passphrase key derivation algorithm to secure the file key and IV.'
12
+ spec.has_rdoc = false ## No documentation yet
13
+ spec.extra_rdoc_files = [ 'README.txt' ]
14
+ spec.files = [
15
+ 'README.txt',
16
+ 'VERSION.txt',
17
+ 'Rakefile',
18
+ 'bin/filesafe'
19
+ ]
20
+ spec.executables = [ 'filesafe' ]
21
+ spec.add_dependency('pbkdf2', '>= 0.1.0')
22
+ spec.add_dependency('highline', '>= 1.6.1')
23
+ end
24
+
25
+ Rake::GemPackageTask.new(gemspec) do |pkg|
26
+ pkg.need_zip = true
27
+ pkg.need_tar = true
28
+ end
29
+
30
+ Rake::RDocTask.new do |rdoc|
31
+ rdoc.name = 'rdoc'
32
+ rdoc.main = 'README.txt'
33
+ rdoc.rdoc_dir = 'doc'
34
+ rdoc.rdoc_files.include('README.txt')
35
+ end
36
+
37
+ task :default => [
38
+ 'pkg/filesafe-' + File.open('VERSION.txt','r').to_a.join.strip + '.gem',
39
+ :rdoc
40
+ ]
41
+
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # FileSafe
4
+ # Version: 1.0.0
5
+ # From: http://www.aarongifford.com/computers/filesafe/index.html
6
+ #
7
+ # A simple file encryption/decryption script that uses 256-bit AES encryption,
8
+ # a secure random number/data source, and the password/passphrase (via PBKDF2)
9
+ # to encrypt/decrypt one or more files.
10
+ #
11
+ # Written by Aaron D. Gifford - http://www.aarongifford.com/
12
+ # Copyright (c) 2010 Aaron D. Gifford
13
+ #
14
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ # of this software and associated documentation files (the "Software"), to deal
16
+ # in the Software without restriction, including without limitation the rights
17
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ # copies of the Software, and to permit persons to whom the Software is
19
+ # furnished to do so, subject to the following conditions:
20
+ #
21
+ # The above copyright notice and this permission notice shall be included in
22
+ # all copies or substantial portions of the Software.
23
+ #
24
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
+ # THE SOFTWARE.
31
+
32
+ require 'openssl' ## Encryption/HMAC/Hash algorithms
33
+ require 'securerandom' ## Cryptographically secure source of random data
34
+ require 'pbkdf2' ## PBKDF2 algorithm for key material generation
35
+ require 'highline' ## For reading a passphrase from a terminal
36
+ require 'tempfile' ## Temporary file creation
37
+
38
+ def usage(msg)
39
+ STDERR.puts <<-EOM
40
+ ERROR: #{msg}
41
+
42
+ Usage: #{$0} -e|-d [-n] [-p 'pass phrase'] [--] file [file [file...]]
43
+ -e Encrypt file(s)
44
+ -d Decrypt file(s)
45
+ -p 'pass phrase' Passphrase to use for operation
46
+ -n Do NOT create a temporary password HMAC/hash file (during decryption)
47
+ -- End of options -- all subsequent arguments are files
48
+
49
+ EOM
50
+ exit
51
+ end
52
+
53
+ opt = {}
54
+ while ARGV.size > 0 && ARGV[0][0,1] == '-'
55
+ case ARGV[0]
56
+ when '-e'
57
+ ARGV.shift
58
+ usage "Please only specify ONE -d or -e option." if opt.key?(:op)
59
+ opt[:op] = :encrypt
60
+ when '-d'
61
+ ARGV.shift
62
+ usage "Please only specify ONE -d or -e option." if opt.key?(:op)
63
+ opt[:op] = :decrypt
64
+ when '-p'
65
+ ARGV.shift
66
+ usage "Please specify a passphrase and one or more files." if ARGV.size < 2
67
+ opt[:phrase] = ARGV.shift
68
+ when '-n'
69
+ ARV.shift
70
+ usage "This option only applies to decryption operations." unless opt.key?(:decrypt)
71
+ opt[:notemp] = true
72
+ when '--'
73
+ break
74
+ else
75
+ usage "Unrecognized option: #{ARGV[0].inspect}"
76
+ end
77
+ end
78
+ usage "You did not specify a -e encrypt or -d decrypt operation." unless opt.key?(:op)
79
+ usage "You must specify one or more files to operate on." if ARGV.size == 0
80
+
81
+
82
+ ## CONFIGURATION ITEMS:
83
+ PASSHASH_SUFFIX = '.pass'
84
+ CIPHER = 'aes-256-cbc'
85
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER)
86
+ BLOCK_LEN = cipher.block_size
87
+ KEY_LEN = cipher.key_len
88
+ IV_LEN = cipher.iv_len
89
+ SALT_LEN = KEY_LEN + IV_LEN
90
+ HMAC_FUNC = 'sha512'
91
+ HMAC_LEN = OpenSSL::HMAC.new('', HMAC_FUNC).digest.bytesize
92
+ HEADER_LEN = KEY_LEN + IV_LEN + SALT_LEN + HMAC_LEN
93
+ ITERATIONS = 4096
94
+ FILE_CHUNK_LEN = 65536
95
+
96
+ def getphrase(check=false)
97
+ begin
98
+ phrase = HighLine.new.ask('Passphrase: '){|q| q.echo = '*' ; q.overwrite = true }
99
+ return phrase unless check
100
+ tmp = HighLine.new.ask('Retype passphrase: '){|q| q.echo = '*' ; q.overwrite = true }
101
+ return phrase if tmp == phrase
102
+ rescue Interrupt
103
+ exit -1
104
+ end while true
105
+ end
106
+
107
+ def encrypt_file(file, passphrase=nil)
108
+ raise "Cannot encrypt non-existent file: #{file.inspect}" unless File.exist?(file)
109
+ raise "Cannot encrypt unreadable file: #{file.inspect}" unless File.readable?(file)
110
+ raise "Cannot encrypt unwritable file: #{file.inspect}" unless File.writable?(file)
111
+ passhash = false
112
+ if File.exist?(file + PASSHASH_SUFFIX)
113
+ raise "Cannot read password hash temporary file: #{(file + PASSHASH_SUFFIX).inspect}" unless File.readable?(file + PASSHASH_SUFFIX)
114
+ raise "Password hash temporary file length is invalid: #{(file + PASSHASH_SUFFIX).inspect}" unless File.size(file + PASSHASH_SUFFIX) == SALT_LEN + HMAC_LEN
115
+ fp = File.open(file + PASSHASH_SUFFIX, File::RDONLY)
116
+ salt = fp.read(SALT_LEN)
117
+ passcheck = fp.read(HMAC_LEN)
118
+ loop do
119
+ passphrase = getphrase if passphrase.nil?
120
+ pbkdf2data = PBKDF2.new do |p|
121
+ p.hash_function = HMAC_FUNC
122
+ p.password = passphrase
123
+ p.salt = salt
124
+ p.iterations = ITERATIONS
125
+ p.key_length = HMAC_LEN
126
+ end.bin_string
127
+ break if passcheck == pbkdf2data
128
+ puts "*** ERROR: Passphrase mismatch. Try again, abort, or delete temporary file: #{file + PASSHASH_SUFFIX}"
129
+ passphrase = nil
130
+ end
131
+ passhash = true
132
+ elsif passphrase.nil?
133
+ puts "*** ALERT: Enter your NEW passphrase twice. DO NOT FORGET IT, or you may lose your data!"
134
+ passphrase = getphrase(true)
135
+ end
136
+
137
+ ## Use secure random data to populate salt, key, and IV:
138
+ salt = SecureRandom.random_bytes(SALT_LEN) ## Acquire some fresh salt
139
+ file_key = SecureRandom.random_bytes(KEY_LEN) ## Get some random key material
140
+ file_iv = SecureRandom.random_bytes(IV_LEN) ## And a random initialization vector
141
+
142
+ ## Encrypt the file key and IV using password-derived keying material:
143
+ keymaterial = PBKDF2.new do |p|
144
+ p.hash_function = HMAC_FUNC
145
+ p.password = passphrase
146
+ p.salt = salt
147
+ p.iterations = ITERATIONS
148
+ p.key_length = KEY_LEN + IV_LEN
149
+ end.bin_string
150
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER)
151
+ cipher.encrypt
152
+ ## No padding required for this operation since the file key + IV is
153
+ ## an exact multiple of the cipher block length:
154
+ cipher.padding = 0
155
+ cipher.key = keymaterial[0,KEY_LEN]
156
+ cipher.iv = keymaterial[KEY_LEN,IV_LEN]
157
+ encrypted_keymaterial = cipher.update(file_key + file_iv) + cipher.final
158
+ encrypted_file_key = encrypted_keymaterial[0,KEY_LEN]
159
+ encrypted_file_iv = encrypted_keymaterial[KEY_LEN,IV_LEN]
160
+
161
+ ## Open the plaintext file for reading (and later overwriting):
162
+ rfp = File.open(file, File::RDWR|File::EXCL)
163
+
164
+ ## Open a temporary ciphertext file for writing:
165
+ wfp = Tempfile.new(File.basename(rfp.path), File.dirname(rfp.path))
166
+
167
+ ## Write the salt and encrypted file key + IV and
168
+ ## temporarily fill the HMAC slot with zero-bytes:
169
+ wfp.write(salt + encrypted_file_key + encrypted_file_iv + (0.chr * HMAC_LEN))
170
+
171
+ ## Start the HMAC:
172
+ hmac = OpenSSL::HMAC.new(passphrase, HMAC_FUNC)
173
+ hmac << salt
174
+ hmac << encrypted_file_key
175
+ hmac << encrypted_file_iv
176
+
177
+ ## Encrypt file with file key + IV:
178
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER)
179
+ cipher.encrypt
180
+ ## Encryption of file contents uses PCKS#5 padding which OpenSSL should
181
+ ## have enabled by default. Nevertheless, we explicitly enable it here:
182
+ cipher.padding = 1
183
+ cipher.key = file_key
184
+ cipher.iv = file_iv
185
+ until rfp.eof?
186
+ data = rfp.read(FILE_CHUNK_LEN)
187
+ if data.bytesize > 0
188
+ data = cipher.update(data)
189
+ hmac << data
190
+ wfp.write(data)
191
+ end
192
+ end
193
+ data = cipher.final
194
+ if data.bytesize > 0
195
+ ## Save the last bit-o-data and update the HMAC:
196
+ wfp.write(data)
197
+ hmac << data
198
+ end
199
+
200
+ ## Write HMAC digest to file:
201
+ wfp.pos = SALT_LEN + KEY_LEN + IV_LEN
202
+ wfp.write(hmac.digest)
203
+
204
+ ## Overwrite the original plaintext file with zero bytes.
205
+ ## This adds a small measure of security against recovering
206
+ ## the original unencrypted contents. It would likely be
207
+ ## better to overwrite the file multiple times with different
208
+ ## bit patterns, including one or more iterations using
209
+ ## high-quality random data.
210
+ rfp.seek(0,File::SEEK_END)
211
+ fsize = rfp.pos
212
+ rfp.pos = 0
213
+ while rfp.pos + FILE_CHUNK_LEN < fsize
214
+ rfp.write(0.chr * FILE_CHUNK_LEN)
215
+ end
216
+ rfp.write(0.chr * (fsize - rfp.pos)) if rfp.pos < fsize
217
+ rfp.close
218
+
219
+ ## Copy file ownership/permissions:
220
+ stat = File.stat(rfp.path)
221
+ wfp.chown(stat.uid, stat.gid)
222
+ wfp.chmod(stat.mode)
223
+
224
+ ## Close the ciphertext temporary file without deleting:
225
+ wfp.close(false)
226
+
227
+ ## Rename temporary file to permanent name:
228
+ File.rename(wfp.path, rfp.path)
229
+
230
+ ## Remove password hash temp. file:
231
+ File.delete(file + PASSHASH_SUFFIX) if passhash
232
+
233
+ puts "ENCRYPTED: #{file}"
234
+ end
235
+
236
+ def decrypt_file(file, passphrase=nil, notemp=nil)
237
+ raise "Cannot decrypt non-existent file: #{file.inspect}" unless File.exist?(file)
238
+ raise "Cannot decrypt unreadable file: #{file.inspect}" unless File.readable?(file)
239
+ raise "Cannot decrypt unwritable file: #{file.inspect}" unless File.writable?(file)
240
+ fsize = File.size(file)
241
+ raise "File is not in valid encrypted format: #{file.inspect}" unless fsize > HEADER_LEN && (fsize - HEADER_LEN) % BLOCK_LEN == 0
242
+ salt = encrypted_file_key = encrypted_file_iv = nil
243
+ loop do
244
+ passphrase = getphrase if passphrase.nil?
245
+ fp = File.open(file, File::RDONLY)
246
+ salt = fp.read(SALT_LEN)
247
+ encrypted_file_key = fp.read(KEY_LEN)
248
+ encrypted_file_iv = fp.read(IV_LEN)
249
+ file_hmac = fp.read(HMAC_LEN)
250
+ test_hmac = OpenSSL::HMAC.new(passphrase, HMAC_FUNC)
251
+ test_hmac << salt
252
+ test_hmac << encrypted_file_key
253
+ test_hmac << encrypted_file_iv
254
+ until fp.eof?
255
+ data = fp.read(FILE_CHUNK_LEN)
256
+ test_hmac << data unless data.bytesize == 0
257
+ end
258
+ fp.close
259
+ break if test_hmac.digest == file_hmac
260
+ puts "*** ERROR: Incorrect passphrase, or file is not encrypted. Try again or abort."
261
+ passphrase = nil
262
+ end
263
+
264
+ ## Extract and decrypt the encrypted file key + IV.
265
+ ## First, regenerate the password-based key material that encrypts the
266
+ ## file key + IV:
267
+ keymaterial = PBKDF2.new do |p|
268
+ p.hash_function = HMAC_FUNC
269
+ p.password = passphrase
270
+ p.salt = salt
271
+ p.iterations = ITERATIONS
272
+ p.key_length = KEY_LEN + IV_LEN
273
+ end.bin_string
274
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER)
275
+ cipher.decrypt
276
+ cipher.padding = 0 ## No padding is required for this operation
277
+ cipher.key = keymaterial[0,KEY_LEN]
278
+ cipher.iv = keymaterial[KEY_LEN,IV_LEN]
279
+ ## Decrypt file key + IV:
280
+ keymaterial = cipher.update(encrypted_file_key + encrypted_file_iv) + cipher.final
281
+ file_key = keymaterial[0,KEY_LEN]
282
+ file_iv = keymaterial[KEY_LEN,IV_LEN]
283
+
284
+ ## Decrypt file:
285
+ cipher = OpenSSL::Cipher::Cipher.new(CIPHER)
286
+ cipher.decrypt
287
+ cipher.padding = 1 ## File contents use PCKS#5 padding,OpenSSL's default method
288
+ cipher.key = file_key
289
+ cipher.iv = file_iv
290
+
291
+ ## Open ciphertext file for reading:
292
+ rfp = File.open(file, File::RDONLY|File::EXCL)
293
+
294
+ ## Open a temporary plaintext file for writing:
295
+ wfp = Tempfile.new(File.basename(rfp.path), File.dirname(rfp.path))
296
+
297
+ ## Begin reading the ciphertext beyond the headers:
298
+ rfp.pos = HEADER_LEN ## Skip headers
299
+ until rfp.eof?
300
+ data = rfp.read(FILE_CHUNK_LEN)
301
+ if data.bytesize > 0
302
+ data = cipher.update(data)
303
+ wfp.write(data)
304
+ end
305
+ end
306
+ data = cipher.final
307
+ wfp.write(data) if data.bytesize > 0
308
+
309
+ ## Close the ciphertext source file:
310
+ rfp.close
311
+
312
+ ## Copy file ownership/permissions:
313
+ stat = File.stat(rfp.path)
314
+ wfp.chown(stat.uid, stat.gid)
315
+ wfp.chmod(stat.mode)
316
+
317
+ ## Close the plaintext temporary file without deleting:
318
+ wfp.close(false)
319
+
320
+ ## Rename temporary file to permanent name:
321
+ File.rename(wfp.path, rfp.path)
322
+
323
+ if notemp.nil?
324
+ ## Write password hash temp. file using PBKDF2 as an iterated hash of sorts of HMAC_LEN bytes:
325
+ salt = SecureRandom.random_bytes(SALT_LEN) ## Grab a new chunk of random data
326
+ pbkdf2data = PBKDF2.new do |p|
327
+ p.hash_function = HMAC_FUNC
328
+ p.password = passphrase
329
+ p.salt = salt
330
+ p.iterations = ITERATIONS
331
+ p.key_length = HMAC_LEN
332
+ end.bin_string
333
+ File.open(file + PASSHASH_SUFFIX, File::WRONLY|File::EXCL|File::CREAT) {|f| f.write(salt + pbkdf2data)}
334
+ end
335
+
336
+ puts "FILE DECRYPTED: #{file}"
337
+ end
338
+
339
+ ARGV.each do |file|
340
+ begin
341
+ if opt[:op] == :decrypt
342
+ decrypt_file(file, opt[:phrase], opt[:notemp])
343
+ else
344
+ encrypt_file(file, opt[:phrase])
345
+ end
346
+ rescue StandardError => e
347
+ STDERR.puts "*** Error while working on file #{file.inspect}: #{e}"
348
+ exit -1
349
+ end
350
+ end
351
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: filesafe
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Aaron D. Gifford
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-30 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: pbkdf2
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 1
31
+ - 0
32
+ version: 0.1.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: highline
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 1
45
+ - 6
46
+ - 1
47
+ version: 1.6.1
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ description: A utility script for encrypting and decrypting files using a randomly generated 256-bit AES key and initialization vector secured using the PBKDF2 password/passphrase key derivation algorithm to secure the file key and IV.
51
+ email:
52
+ executables:
53
+ - filesafe
54
+ extensions: []
55
+
56
+ extra_rdoc_files:
57
+ - README.txt
58
+ files:
59
+ - README.txt
60
+ - VERSION.txt
61
+ - Rakefile
62
+ - bin/filesafe
63
+ has_rdoc: true
64
+ homepage: http://www.aarongifford.com/computers/filesafe/
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.7
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Encrypt/decrypt files with a random 256-bit AES key secured by a passphrase derived master key using PBKDF2
95
+ test_files: []
96
+