lenc 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.
- checksums.yaml +7 -0
- data/CHANGELOG.txt +2 -0
- data/README.txt +90 -0
- data/bin/lencrypt +5 -0
- data/lib/lenc.rb +1 -0
- data/lib/lenc/aes.rb +523 -0
- data/lib/lenc/config_file.rb +108 -0
- data/lib/lenc/lencrypt.rb +70 -0
- data/lib/lenc/repo.rb +1024 -0
- data/lib/lenc/tools.rb +632 -0
- data/lib/lenc/trollop.rb +782 -0
- data/test/test.rb +377 -0
- metadata +61 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'tools'
|
3
|
+
|
4
|
+
module LEnc
|
5
|
+
|
6
|
+
# Manages a configuration file, which
|
7
|
+
# is a set of key/value pairs that can be saved to the file system.
|
8
|
+
#
|
9
|
+
class ConfigFile
|
10
|
+
|
11
|
+
attr_reader :path
|
12
|
+
|
13
|
+
# @param filename where configuration file is to be found (or written to,
|
14
|
+
# if it doesn't yet exist)
|
15
|
+
# @param parentDir directory containing file; if nil, uses user's home directory
|
16
|
+
#
|
17
|
+
def initialize(filename, parentDir=nil)
|
18
|
+
|
19
|
+
parentDir ||= Dir.home
|
20
|
+
|
21
|
+
@path = File.join(parentDir, filename)
|
22
|
+
|
23
|
+
@content = {}
|
24
|
+
@origContentStr = "!!!"
|
25
|
+
if exists()
|
26
|
+
contents = read_text_file(@path).strip
|
27
|
+
|
28
|
+
@content = JSON.parse(contents)
|
29
|
+
@origContentStr = JSON.dump(@content)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Determine if configuration file exists on disk
|
34
|
+
def exists()
|
35
|
+
File.exists?(@path)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get the directory containing the configuration file
|
39
|
+
def get_directory()
|
40
|
+
File.dirname(@path)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Store a key => value pair (any existing value for this key is overwritten)
|
44
|
+
def set(key,val)
|
45
|
+
@content[key] = val
|
46
|
+
end
|
47
|
+
|
48
|
+
# Remove key (and value), if it exists
|
49
|
+
def remove(key)
|
50
|
+
@content.remove(key)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Write configuration file, if it has changed
|
54
|
+
def write
|
55
|
+
newStr = JSON.dump(@content)
|
56
|
+
|
57
|
+
if newStr != @origContentStr
|
58
|
+
@origContentStr = newStr
|
59
|
+
write_text_file(@path,@origContentStr + "\n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get value for a key
|
64
|
+
# @param defVal value to return if key doesn't exist
|
65
|
+
def val(key, defVal = nil)
|
66
|
+
@content[key] || defVal
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
s = 'ConfigFile '
|
71
|
+
s << path()
|
72
|
+
s << dh(@content)
|
73
|
+
# " [\n"
|
74
|
+
# @content.each_pair do |k,v|
|
75
|
+
# s << d(k) << ' ==> ' << d(v) << "\n"
|
76
|
+
# end
|
77
|
+
# s << "]\n"
|
78
|
+
s
|
79
|
+
end
|
80
|
+
|
81
|
+
def inspect
|
82
|
+
to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
end # Class
|
86
|
+
end # Module
|
87
|
+
|
88
|
+
|
89
|
+
if main? __FILE__
|
90
|
+
|
91
|
+
include LEnc
|
92
|
+
|
93
|
+
f = ConfigFile.new("__testrbconfig__.txt")
|
94
|
+
|
95
|
+
pr("constructed:\n%s\n",d(f))
|
96
|
+
|
97
|
+
srand
|
98
|
+
"alpha bravo echo geronimo".split.each do |s|
|
99
|
+
n = s.size
|
100
|
+
if rand() > 0.8
|
101
|
+
n = rand(12)
|
102
|
+
end
|
103
|
+
f.set(s,n)
|
104
|
+
end
|
105
|
+
|
106
|
+
f.write
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'repo'
|
2
|
+
|
3
|
+
# The application script (i.e., the 'main program')
|
4
|
+
#
|
5
|
+
class LEncApp
|
6
|
+
include LEnc
|
7
|
+
|
8
|
+
def run(argv = ARGV)
|
9
|
+
|
10
|
+
req 'trollop'
|
11
|
+
p = Trollop::Parser.new do
|
12
|
+
opt :init, "create new encryption repository: KEY ENCDIR ", :type => :strings
|
13
|
+
opt :orignames, "leave filenames unencrypted"
|
14
|
+
opt :update, "update encrypted repository (default operation)"
|
15
|
+
opt :recover, "recover files from an encrypted repository: KEY ENCDIR RECDIR", :type => :strings
|
16
|
+
opt :where, "specify source directory (default = current directory)", :type => :string
|
17
|
+
opt :verbose,"verbose operation"
|
18
|
+
opt :quiet, "quiet operation"
|
19
|
+
opt :dryrun, "show which files will be modified, but make no changes"
|
20
|
+
end
|
21
|
+
|
22
|
+
options = Trollop::with_standard_exception_handling p do
|
23
|
+
p.parse argv
|
24
|
+
end
|
25
|
+
|
26
|
+
v = 0
|
27
|
+
v = -1 if options[:quiet]
|
28
|
+
v = 1 if options[:verbose]
|
29
|
+
|
30
|
+
nOpt = 0
|
31
|
+
nOpt += 1 if options[:init]
|
32
|
+
nOpt += 1 if options[:update]
|
33
|
+
nOpt += 1 if options[:recover]
|
34
|
+
|
35
|
+
Trollop::die("Only one operation can be performed at a time." ) if nOpt > 1
|
36
|
+
|
37
|
+
r = Repo.new(:dryrun => options[:dryrun],
|
38
|
+
:verbosity => v)
|
39
|
+
|
40
|
+
begin
|
41
|
+
|
42
|
+
if (a = options[:init])
|
43
|
+
Trollop::die("Expecting: KEY ENCDIR") if a.size != 2
|
44
|
+
pwd,encDir = a
|
45
|
+
r.create(options[:where], pwd, encDir, options[:orignames])
|
46
|
+
elsif (a = options[:recover])
|
47
|
+
Trollop::die("Expecting: KEY ENCDIR RECDIR") if a.size != 3
|
48
|
+
r.perform_recovery(a[0],a[1],a[2])
|
49
|
+
else
|
50
|
+
r.open(options[:where])
|
51
|
+
r.perform_update(options[:verifyenc])
|
52
|
+
end
|
53
|
+
|
54
|
+
r.close()
|
55
|
+
rescue Exception =>e
|
56
|
+
puts("\nProblem encountered: #{e.message}")
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if __FILE__ == $0
|
63
|
+
args = ARGV
|
64
|
+
|
65
|
+
# warn("trying special")
|
66
|
+
# args = "-w /Users/jeff/Desktop/_testdirs_ -r feefiefoefumaldkjsdaflkj /Users/jeff/Desktop/_testdirs_/encr /Users/jeff/Desktop/_testdirs_/resc2".split
|
67
|
+
# args = "-h".split
|
68
|
+
LEncApp.new().run(args)
|
69
|
+
end
|
70
|
+
|
data/lib/lenc/repo.rb
ADDED
@@ -0,0 +1,1024 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'pathname'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
require_relative 'tools'
|
6
|
+
req('aes config_file')
|
7
|
+
|
8
|
+
module RepoInternal
|
9
|
+
class IgnoreEntry
|
10
|
+
attr_accessor :dirOnly, :negated, :pathMode, :rexp, :dbPattern
|
11
|
+
|
12
|
+
# Representation of a .lencignore entry
|
13
|
+
def initialize
|
14
|
+
@dirOnly, @negated, @pathMode, @rexp, @dbPattern = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
to_s
|
19
|
+
end
|
20
|
+
def to_s
|
21
|
+
s = "Ign<"
|
22
|
+
s << df(@dirOnly,"dirOnly") << "expr: " << @rexp.to_s << ">"
|
23
|
+
s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
module LEnc
|
30
|
+
|
31
|
+
class RepoNotFoundException < Exception
|
32
|
+
end
|
33
|
+
|
34
|
+
class EncryptionVerificationException < Exception
|
35
|
+
end
|
36
|
+
|
37
|
+
class VersionException < Exception
|
38
|
+
end
|
39
|
+
|
40
|
+
class RecoveryException < Exception
|
41
|
+
end
|
42
|
+
|
43
|
+
class UpdateException < Exception
|
44
|
+
end
|
45
|
+
|
46
|
+
# Represents an encrypted repository
|
47
|
+
#
|
48
|
+
class Repo
|
49
|
+
|
50
|
+
include RepoInternal
|
51
|
+
|
52
|
+
# The filename that represents a repository; it is
|
53
|
+
# stored in the repository's root directory. """
|
54
|
+
if windows?
|
55
|
+
LENC_REPO_FILENAME = "__lenc_repo__.txt"
|
56
|
+
else
|
57
|
+
LENC_REPO_FILENAME = ".lenc"
|
58
|
+
end
|
59
|
+
|
60
|
+
ENCRFILENAMEPREFIX = "_#"
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
RESPECIALPAT = Regexp.new('^[\.\^\$\*\+\?\{\}\\\|\!\:\)\(\[\]]$')
|
66
|
+
|
67
|
+
if windows?
|
68
|
+
IGNOREFILENAME = "__lencignore__.txt"
|
69
|
+
TEMPFILENAME = "__tmp_file__.bin"
|
70
|
+
else
|
71
|
+
IGNOREFILENAME = ".lencignore"
|
72
|
+
TEMPFILENAME = ".lenc__temp__file"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
DEFAULTIGNORE = \
|
77
|
+
"#{LENC_REPO_FILENAME}\n #{TEMPFILENAME}\n" + \
|
78
|
+
".DS_Store\n" + \
|
79
|
+
".recoverdefaults\n"
|
80
|
+
|
81
|
+
STATE_CLOSED = 0
|
82
|
+
STATE_OPEN = 1
|
83
|
+
|
84
|
+
public
|
85
|
+
|
86
|
+
# Construct a repository object. It is created in a 'closed' state, in that it
|
87
|
+
# is not associated with a particular repository in the file system.
|
88
|
+
#
|
89
|
+
# @param options hash table of optional parameters; e.g.
|
90
|
+
# r = LEnc::Repo(:verbosity => 2, strict => True)
|
91
|
+
#
|
92
|
+
# :dryrun if true, no files on the filesystem will be affected by this
|
93
|
+
# object throughout its lifetime. Useful for showing the user what
|
94
|
+
# would happen if dryrun were false.
|
95
|
+
# :verbosity controls the amount of feedback during this object's lifetime.
|
96
|
+
# default 0; if < 0, silent; if > 0, talkative
|
97
|
+
#
|
98
|
+
def initialize(options = {})
|
99
|
+
|
100
|
+
reset_state()
|
101
|
+
|
102
|
+
@dryrun = options.delete :dryrun
|
103
|
+
@verbosity = (options.delete :verbosity) || 0
|
104
|
+
|
105
|
+
# During recovery, we will use the first file we encounter to
|
106
|
+
# verify if the supplied password is correct, and abort if not.
|
107
|
+
@password_verified = false
|
108
|
+
|
109
|
+
if options.size > 0
|
110
|
+
raise ArgumentError, "Unrecognized options: " + d2(options)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
# Create a new encryption repository, and open it.
|
116
|
+
#
|
117
|
+
# @param repo_dir directory of new repository (nil for current directory)
|
118
|
+
# @param key encryption key, a string from 20 to 56 characters in length
|
119
|
+
# @param enc_dir directory to store encrypted files; must not yet exist, and must
|
120
|
+
# not represent a directory lying within the repo_dir tree
|
121
|
+
# @param original_names if true, the filenames are not encrypted, only the file contents
|
122
|
+
#
|
123
|
+
# @raise ArgumentError if appropriate
|
124
|
+
#
|
125
|
+
def create(repo_dir, key, enc_dir, original_names=false)
|
126
|
+
raise IllegalStateException if @state != STATE_CLOSED
|
127
|
+
|
128
|
+
db = warndb 0
|
129
|
+
!db || pr("Repo.create, %s\n",da( [repo_dir,key,enc_dir,original_names]))
|
130
|
+
repo_dir ||= Dir.pwd
|
131
|
+
|
132
|
+
if !File.directory?(repo_dir)
|
133
|
+
raise ArgumentError, "Not a directory: #{repo_dir}"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Verify that there is no repository.
|
137
|
+
# Construct a ConfigFile object to determine if it already exists
|
138
|
+
@confFile = ConfigFile.new(LENC_REPO_FILENAME, repo_dir)
|
139
|
+
|
140
|
+
if @confFile.exists()
|
141
|
+
raise ArgumentError, 'Encryption repository already exists: ' \
|
142
|
+
+ @confFile.path
|
143
|
+
end
|
144
|
+
|
145
|
+
@confFile.set('version', @version)
|
146
|
+
|
147
|
+
@orignames = original_names
|
148
|
+
|
149
|
+
edir = File.absolute_path(enc_dir)
|
150
|
+
@confFile.set('orignames', @orignames)
|
151
|
+
|
152
|
+
if @verbosity >= 0
|
153
|
+
pr("Creating encryption repository %s\n", @confFile.path)
|
154
|
+
end
|
155
|
+
|
156
|
+
pp = verifyDirsDistinct([repo_dir, edir])
|
157
|
+
|
158
|
+
if pp
|
159
|
+
raise ArgumentError, "Directory " + pp[0] + \
|
160
|
+
" is a subdirectory of " + pp[1]
|
161
|
+
end
|
162
|
+
|
163
|
+
if (key.size < 20 || key.size > 56)
|
164
|
+
raise ArgumentError, "Password length " + key.size.to_s \
|
165
|
+
+ " is illegal"
|
166
|
+
end
|
167
|
+
|
168
|
+
@confFile.set('key', key)
|
169
|
+
|
170
|
+
# Create encryption directory
|
171
|
+
if File.exists?(edir)
|
172
|
+
raise ArgumentError, \
|
173
|
+
"Encryption directory or file already exists: '#{edir}'"
|
174
|
+
end
|
175
|
+
|
176
|
+
@confFile.set('encDir', edir)
|
177
|
+
|
178
|
+
if not @dryrun
|
179
|
+
Dir.mkdir(edir)
|
180
|
+
@confFile.write()
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# Open the repository, by associating it with one in the file system.
|
186
|
+
#
|
187
|
+
# @param startDirectory directory lying within repository tree; if nil, uses
|
188
|
+
# current directory
|
189
|
+
#
|
190
|
+
# @raise IllegalStateException if repository is already open
|
191
|
+
# @raise ArgumentError if directory doesn't exist, or does not lie in a repository
|
192
|
+
#
|
193
|
+
def open(startDirectory=nil)
|
194
|
+
db = warndb 0
|
195
|
+
!db || pr("Repo.open startDir=%s\n",d(startDirectory))
|
196
|
+
|
197
|
+
raise IllegalStateException if @state != STATE_CLOSED
|
198
|
+
|
199
|
+
startDirectory ||= Dir.pwd
|
200
|
+
|
201
|
+
if not File.directory?(startDirectory)
|
202
|
+
raise ArgumentError,"Not a directory: '" + startDirectory + "'"
|
203
|
+
end
|
204
|
+
|
205
|
+
cfile = Repo.findRepository(startDirectory)
|
206
|
+
!db || pr(" find repo (%s) => %s\n",d(startDirectory),d(cfile))
|
207
|
+
|
208
|
+
if !cfile
|
209
|
+
raise RepoNotFoundException, "Can't find repository"
|
210
|
+
end
|
211
|
+
|
212
|
+
@confFile = cfile
|
213
|
+
@startDir = startDirectory
|
214
|
+
@repoBaseDir = cfile.get_directory
|
215
|
+
|
216
|
+
cfVersion = @confFile.val('version', 0)
|
217
|
+
if cfVersion > @version
|
218
|
+
raise(VersionError,"Repository was built with a more recent version of the program.")
|
219
|
+
end
|
220
|
+
|
221
|
+
if cfVersion.floor < @version.floor
|
222
|
+
raise(VersionError,"Repository was built with an older version; rebuild it")
|
223
|
+
end
|
224
|
+
|
225
|
+
# Read values from configuration to instance vars
|
226
|
+
@encrDir = @confFile.val('encDir')
|
227
|
+
@encrKey = @confFile.val('key')
|
228
|
+
@orignames = @confFile.val('orignames')
|
229
|
+
|
230
|
+
prepareKeys()
|
231
|
+
|
232
|
+
@state = STATE_OPEN
|
233
|
+
end
|
234
|
+
|
235
|
+
# Close the repository, if it is open
|
236
|
+
def close
|
237
|
+
return if @state == STATE_CLOSED
|
238
|
+
|
239
|
+
raise IllegalStateException if @state != STATE_OPEN
|
240
|
+
|
241
|
+
removeTempFile()
|
242
|
+
reset_state()
|
243
|
+
end
|
244
|
+
|
245
|
+
# Update the repository. Finds files that need to be re-encrypted and does so.
|
246
|
+
# Repository must be open.
|
247
|
+
#
|
248
|
+
# @param verifyEncryption for debug purposes; if true, each file that is encrypted is tested to confirm that
|
249
|
+
# it decrypts correctly.
|
250
|
+
#
|
251
|
+
# @raise IllegalStateException if repository isn't open.
|
252
|
+
#
|
253
|
+
def perform_update(verifyEncryption=false)
|
254
|
+
raise IllegalStateException if @state != STATE_OPEN
|
255
|
+
|
256
|
+
setInputOutputDirs(@startDir,@encrDir)
|
257
|
+
|
258
|
+
@verifyEncryption = verifyEncryption
|
259
|
+
|
260
|
+
puts("Encrypting...") if @verbosity >= 1
|
261
|
+
|
262
|
+
begin
|
263
|
+
encryptDir(@repoBaseDir, @encrDir)
|
264
|
+
puts("...done.") if @verbosity >= 1
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
# Recover files from a repository's encryption folder.
|
270
|
+
#
|
271
|
+
# @param key encryption key
|
272
|
+
# @param eDir encryption directory
|
273
|
+
# @param rDir directory to write decrypted files to; creates it if necessary.
|
274
|
+
# Must not lie within eDir tree.
|
275
|
+
#
|
276
|
+
# @raise ArgumentError if problem with the directory arguments;
|
277
|
+
# @raise DecryptionError if incorrect password provided, and strict mode in effect
|
278
|
+
#
|
279
|
+
def perform_recovery(key, eDir, rDir)
|
280
|
+
raise IllegalStateException if @state != STATE_CLOSED
|
281
|
+
|
282
|
+
ret = nil
|
283
|
+
|
284
|
+
@encrKey = key
|
285
|
+
|
286
|
+
rd = File.absolute_path(rDir)
|
287
|
+
prepareKeys()
|
288
|
+
|
289
|
+
if not File.directory?(eDir)
|
290
|
+
raise ArgumentError, "Not a directory: '" + eDir + "'"
|
291
|
+
end
|
292
|
+
|
293
|
+
# There must not exist a repository in the recovery directory
|
294
|
+
cf = Repo.findRepository(rd)
|
295
|
+
|
296
|
+
if cf
|
297
|
+
raise ArgumentError, "Recovery directory lies within repository: " + cf.getPath()
|
298
|
+
end
|
299
|
+
|
300
|
+
puts("Recovering...") if @verbosity >= 1
|
301
|
+
|
302
|
+
setInputOutputDirs(eDir,rd)
|
303
|
+
|
304
|
+
begin
|
305
|
+
recover(eDir, rd)
|
306
|
+
print("...done.") if @verbosity >= 1
|
307
|
+
end
|
308
|
+
|
309
|
+
ret
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
def setInputOutputDirs(inp,outp)
|
315
|
+
@inputDir = File.absolute_path(inp)
|
316
|
+
@outputDir = File.absolute_path(outp)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Determine if no paths in a list lie in a subdirectory of another
|
320
|
+
def verifyDirsDistinct(dirList)
|
321
|
+
dirList.each_with_index do |di,i|
|
322
|
+
dirList.each_with_index do |dj,j|
|
323
|
+
next if i == j
|
324
|
+
if di.start_with? dj
|
325
|
+
return [di,dj]
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
nil
|
330
|
+
end
|
331
|
+
|
332
|
+
def renameTempFile(newFilename)
|
333
|
+
if !File.exists?(TEMPFILENAME)
|
334
|
+
raise IOError, "Temporary file missing or of wrong type"
|
335
|
+
end
|
336
|
+
|
337
|
+
File.rename(TEMPFILENAME, newFilename)
|
338
|
+
end
|
339
|
+
|
340
|
+
# Starting in a particular directory, attempt to find the nearest
|
341
|
+
# parent repository.
|
342
|
+
#
|
343
|
+
# returns ConfigFile, or nil
|
344
|
+
#
|
345
|
+
def self.findRepository(startDir)
|
346
|
+
bp = File.absolute_path(startDir)
|
347
|
+
while true
|
348
|
+
|
349
|
+
# Construct a ConfigFile object to determine if it already exists
|
350
|
+
cfile = ConfigFile.new(LENC_REPO_FILENAME, bp)
|
351
|
+
return cfile if cfile.exists
|
352
|
+
|
353
|
+
prev_bp = bp
|
354
|
+
bp = File.dirname(bp)
|
355
|
+
return nil if prev_bp == bp
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
# Parse an ignore list into a list of IgnoreEntries.
|
361
|
+
#
|
362
|
+
# @param text a script, each line of which describes a pattern
|
363
|
+
#
|
364
|
+
def self.parseIgnoreList(text, ignPath="(unknown)")
|
365
|
+
|
366
|
+
db = false
|
367
|
+
|
368
|
+
ret = []
|
369
|
+
|
370
|
+
text.split("\n").each do |ln|
|
371
|
+
begin
|
372
|
+
|
373
|
+
ln.strip!
|
374
|
+
!db || pr("...parsing line [#{ln}]...\n")
|
375
|
+
|
376
|
+
# Determine if it's a comment
|
377
|
+
if ln.start_with?("\\#")
|
378
|
+
ln = ln[1..-1]
|
379
|
+
else
|
380
|
+
if ln.start_with?("#")
|
381
|
+
ln = ""
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
ient = IgnoreEntry.new
|
386
|
+
|
387
|
+
# Determine if it's a negated pattern
|
388
|
+
|
389
|
+
if ln.start_with?("\\!")
|
390
|
+
ln = ln[1..-1]
|
391
|
+
else
|
392
|
+
if ln.start_with?("!")
|
393
|
+
ln = ln[1..-1]
|
394
|
+
ient.negated = true
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Determine if it should represent directories only
|
399
|
+
|
400
|
+
if ln.end_with?('/')
|
401
|
+
ient.dirOnly = true
|
402
|
+
ln = ln[0..-2]
|
403
|
+
end
|
404
|
+
|
405
|
+
next if not ln # comment or blank line, skip
|
406
|
+
|
407
|
+
# Now we see if there are any path separators in the expression.
|
408
|
+
# If so, we set pathMode.
|
409
|
+
|
410
|
+
|
411
|
+
ient.pathMode = ln.include? '/'
|
412
|
+
|
413
|
+
# Convert expression to regular expression.
|
414
|
+
#
|
415
|
+
# * => .*
|
416
|
+
# ? => .
|
417
|
+
# [...] => [...]
|
418
|
+
# [!..] => [^...]
|
419
|
+
|
420
|
+
|
421
|
+
pat = ''
|
422
|
+
|
423
|
+
inBrace = false
|
424
|
+
i = -1
|
425
|
+
while true
|
426
|
+
i += 1
|
427
|
+
break if i >= ln.size
|
428
|
+
|
429
|
+
ch = ln[i]
|
430
|
+
ch2 = (i+1 < ln.size) ? ln[i+1] : ''
|
431
|
+
|
432
|
+
if inBrace
|
433
|
+
if ch == ']'
|
434
|
+
pat << ']'
|
435
|
+
inBrace = false
|
436
|
+
next
|
437
|
+
end
|
438
|
+
else
|
439
|
+
if ch == '['
|
440
|
+
inBrace = true
|
441
|
+
pat << '['
|
442
|
+
if ch2 == '!'
|
443
|
+
i += 1
|
444
|
+
pat << '^'
|
445
|
+
end
|
446
|
+
next
|
447
|
+
end
|
448
|
+
|
449
|
+
if ch == '*'
|
450
|
+
if ient.pathMode
|
451
|
+
pat << '[^/]*'
|
452
|
+
else
|
453
|
+
pat << '.*'
|
454
|
+
end
|
455
|
+
next
|
456
|
+
end
|
457
|
+
|
458
|
+
if ch == '?'
|
459
|
+
if ient.pathMode
|
460
|
+
pat << '[^/]?'
|
461
|
+
else
|
462
|
+
pat << '.?'
|
463
|
+
end
|
464
|
+
next
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
if '[]'.include? ch
|
469
|
+
raise Exception, "Problem with ignore pattern"
|
470
|
+
end
|
471
|
+
|
472
|
+
if RESPECIALPAT.match(ch)
|
473
|
+
pat << '\\' << ch
|
474
|
+
next
|
475
|
+
end
|
476
|
+
|
477
|
+
pat << ch
|
478
|
+
|
479
|
+
end
|
480
|
+
|
481
|
+
ient.rexp = Regexp.new('^' + pat + '$')
|
482
|
+
ient.dbPattern = pat
|
483
|
+
|
484
|
+
ret.push(ient)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
return ret
|
488
|
+
end
|
489
|
+
|
490
|
+
def reset_state
|
491
|
+
@state = STATE_CLOSED
|
492
|
+
@encrKey = nil
|
493
|
+
@encrKey2 = nil
|
494
|
+
@confFile = nil
|
495
|
+
@repoBaseDir = nil
|
496
|
+
@encrDir = nil
|
497
|
+
@version = 1.0
|
498
|
+
@orignames = false
|
499
|
+
initIgnoreList()
|
500
|
+
end
|
501
|
+
|
502
|
+
# Construct the initial ignore list.
|
503
|
+
#
|
504
|
+
# We maintain a stack of these lists, and subsequent operations can push and pop
|
505
|
+
# additional ignore lists onto this stack as it recursively descends into subdirectories.
|
506
|
+
#
|
507
|
+
def initIgnoreList
|
508
|
+
@ignoreStack = []
|
509
|
+
pushIgnoreList('', Repo.parseIgnoreList(DEFAULTIGNORE))
|
510
|
+
end
|
511
|
+
|
512
|
+
# Push a parsed ignore list onto the stack.
|
513
|
+
#
|
514
|
+
# @param directory the name of the directory, relative to its parent; '' for the outermost directory,
|
515
|
+
# or if it represents the same directory as its parent;
|
516
|
+
# should not include any path separators (/, \)
|
517
|
+
# @param expr a list of IgnoreEntries
|
518
|
+
def pushIgnoreList(directory, expr)
|
519
|
+
@ignoreStack.push([directory + '/', expr])
|
520
|
+
end
|
521
|
+
|
522
|
+
def popIgnoreList()
|
523
|
+
@ignoreStack.pop()
|
524
|
+
end
|
525
|
+
|
526
|
+
def removeTempFile()
|
527
|
+
if not @dryrun and File.file?(TEMPFILENAME)
|
528
|
+
remove_file_or_dir(TEMPFILENAME)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Determine the secondary key, which is used for the filenames (not their contents)
|
533
|
+
# This is found by encrypting the primary key.
|
534
|
+
def prepareKeys()
|
535
|
+
|
536
|
+
key = @encrKey
|
537
|
+
|
538
|
+
en = MyAES.new(true, key, "")
|
539
|
+
|
540
|
+
en.finish(key)
|
541
|
+
|
542
|
+
key2 = en.flush()
|
543
|
+
|
544
|
+
# Skip the nonce and header portions
|
545
|
+
key2 = en.strip_encryption_header(key2)
|
546
|
+
@encrKey2 = key2
|
547
|
+
end
|
548
|
+
|
549
|
+
# Encrypt a filename.
|
550
|
+
#
|
551
|
+
# Encrypts using the alternate key, and ensures uses the hash code
|
552
|
+
# of the (unencrypted) filename as the nonce. This means the
|
553
|
+
# encrypted version of a particular filename is always the same,
|
554
|
+
# which is desirable.
|
555
|
+
#
|
556
|
+
# The security of the filename encryption is thus not as safe as that
|
557
|
+
# of the original files (since the nonces are not guaranteed to be unique),
|
558
|
+
# and is why we use a different key for them.
|
559
|
+
#
|
560
|
+
# Encrypted filenames are also given a prefix to distinguish them
|
561
|
+
# from files not created by this program (or filenames that have not been encrypted)
|
562
|
+
#
|
563
|
+
# If filenames are not encrypted in this repository, returns filename unchanged.
|
564
|
+
#
|
565
|
+
def encryptFilename(s)
|
566
|
+
|
567
|
+
db = warndb 0
|
568
|
+
!db || pr("\n\nencryptFilename %s\n",d(s))
|
569
|
+
|
570
|
+
return s if @orignames
|
571
|
+
|
572
|
+
nonce = OpenSSL::Digest::SHA1.new(s).digest
|
573
|
+
!db || pr(" SHA1 applied, nonce=%s\n",dt(nonce))
|
574
|
+
!db || hex_dump(nonce,"SHA1 nonce")
|
575
|
+
|
576
|
+
bf = MyAES.new(true, @encrKey2, nonce)
|
577
|
+
bf.finish(s)
|
578
|
+
b = bf.flush()
|
579
|
+
|
580
|
+
!db || hex_dump(b,"AES encrypted")
|
581
|
+
|
582
|
+
s3 = Base64.urlsafe_encode64(b)
|
583
|
+
!db || hex_dump(s3,"Base64 encoded")
|
584
|
+
|
585
|
+
s2 = ENCRFILENAMEPREFIX.dup
|
586
|
+
|
587
|
+
s2 << s3
|
588
|
+
!db || pr("encr fname: %s\n",d(s2))
|
589
|
+
|
590
|
+
s2
|
591
|
+
end
|
592
|
+
|
593
|
+
# Decrypt a filename suffix; raises DecryptionError if unsuccessful
|
594
|
+
def decryptFilenameAux(suffix)
|
595
|
+
db = warndb 0
|
596
|
+
!db || pr("decryptFilenameAux: %s\n",d(suffix))
|
597
|
+
begin
|
598
|
+
b = Base64.urlsafe_decode64(suffix)
|
599
|
+
!db || hex_dump(b,"after base64 decode")
|
600
|
+
|
601
|
+
bf = MyAES.new(false, @encrKey2)
|
602
|
+
bf.finish(b)
|
603
|
+
|
604
|
+
b = bf.flush()
|
605
|
+
!db || hex_dump(b,"after decrypt")
|
606
|
+
|
607
|
+
s = bytes_to_str(b)
|
608
|
+
!db || hex_dump(s,"after cvt to string (#{s})")
|
609
|
+
|
610
|
+
rescue ArgumentError => e
|
611
|
+
pr("caught exception during decrypt: %s\n",dt(e))
|
612
|
+
raise DecryptionError(e)
|
613
|
+
end
|
614
|
+
|
615
|
+
set_password_verified()
|
616
|
+
|
617
|
+
return s
|
618
|
+
end
|
619
|
+
|
620
|
+
def set_password_verified
|
621
|
+
if !@password_verified
|
622
|
+
@password_verified = true
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
# Decrypt a filename; if filenames are not encrypted in this repository,
|
627
|
+
# If encrypted, and failed to decrypt, returns nil
|
628
|
+
# returns unchanged
|
629
|
+
def decryptFilename(s, assumeEncrypted=true)
|
630
|
+
db = warndb 0
|
631
|
+
|
632
|
+
return s if (not assumeEncrypted) and @orignames
|
633
|
+
|
634
|
+
if not s.start_with?(ENCRFILENAMEPREFIX)
|
635
|
+
raise ArgumentError, "Encrypted filename has unexpected prefix"
|
636
|
+
end
|
637
|
+
|
638
|
+
!db || pr("decryptFilename %s\n",d(s) )
|
639
|
+
s = s[ENCRFILENAMEPREFIX.size .. -1]
|
640
|
+
decryptFilenameAux(s)
|
641
|
+
end
|
642
|
+
|
643
|
+
|
644
|
+
# Update a single source file if necessary (not a directory)
|
645
|
+
def encryptFile(sourceFile, encryptFile)
|
646
|
+
|
647
|
+
# If encrypted file is a directory, delete it
|
648
|
+
if File.directory?(encryptFile)
|
649
|
+
pth = rel_path(encryptFile, @outputDir)
|
650
|
+
|
651
|
+
if @verbosity >= 1
|
652
|
+
msg = "Encrypting file " + rel_path(sourceFile, @inputDir) + " is overwriting existing directory: " + pth
|
653
|
+
end
|
654
|
+
|
655
|
+
if not @dryrun
|
656
|
+
remove_file_or_dir(encryptFile)
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
# Determine if existing encrypted version exists
|
661
|
+
# and is up to date
|
662
|
+
mustUpdate = (not File.file?(encryptFile)) \
|
663
|
+
or (File.mtime(encryptFile) < File.mtime(sourceFile))
|
664
|
+
|
665
|
+
if mustUpdate
|
666
|
+
showProgress = (@verbosity >= 0)
|
667
|
+
|
668
|
+
srcDisp = rel_path(sourceFile, @inputDir)
|
669
|
+
if showProgress
|
670
|
+
pr("%s", srcDisp)
|
671
|
+
end
|
672
|
+
|
673
|
+
convertFile(sourceFile, encryptFile, true, showProgress, false)
|
674
|
+
|
675
|
+
if @verifyEncryption
|
676
|
+
# Verify that the original and decoded files are identical
|
677
|
+
convertFile(encryptFile, TEMPFILENAME, false, showProgress, true)
|
678
|
+
if !FileUtils.compare_file(sourceFile, TEMPFILENAME)
|
679
|
+
raise EncryptionVerificationException, \
|
680
|
+
"File '#{srcDisp}' did not encrypt/decrypt correctly"
|
681
|
+
end
|
682
|
+
if @verbosity >= 0
|
683
|
+
pr(" (file #{srcDisp} encrypted correctly)\n")
|
684
|
+
end
|
685
|
+
removeTempFile()
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
# Determine if a file matches one of the expressions in the ignore stack.
|
691
|
+
# Searches the stack from top to bottom (i.e., the outermost elements are examined last)
|
692
|
+
def shouldFileBeIgnored(f)
|
693
|
+
f2 = f
|
694
|
+
@ignoreStack.reverse.each do |dir,ients|
|
695
|
+
ients.each do |ient|
|
696
|
+
fArg = ient.pathMode ? f2 : f
|
697
|
+
|
698
|
+
matches = ient.rexp.match(fArg)
|
699
|
+
|
700
|
+
if matches
|
701
|
+
return !ient.negated
|
702
|
+
end
|
703
|
+
end
|
704
|
+
f2 = dir + f2
|
705
|
+
end
|
706
|
+
|
707
|
+
return false
|
708
|
+
end
|
709
|
+
|
710
|
+
# Get path relative to an absolute path
|
711
|
+
# @param path
|
712
|
+
# @param abs_path
|
713
|
+
# @return path expressed relative to abs_path
|
714
|
+
#
|
715
|
+
def rel_path(path, abs_path)
|
716
|
+
pth = Pathname.new(path)
|
717
|
+
return pth.relative_path_from(Pathname.new(abs_path)).to_s
|
718
|
+
end
|
719
|
+
|
720
|
+
# Examine all files in repository; reencrypt those that have changed
|
721
|
+
# (by comparing their time stamps with the time stamps of the encyrypted versions)
|
722
|
+
def encryptDir(sourceDir, encryptDir)
|
723
|
+
db = warndb 0
|
724
|
+
!db || pr("\n\nencryptDir\n %s =>\n %s\n",d(sourceDir),d(encryptDir))
|
725
|
+
|
726
|
+
# Add contents of .lencignore to stack. If none exists, treat as if empty
|
727
|
+
lst = []
|
728
|
+
ignPath = File.join(sourceDir, IGNOREFILENAME)
|
729
|
+
if File.file?(ignPath)
|
730
|
+
f1 = read_text_file(ignPath)
|
731
|
+
lst = Repo.parseIgnoreList(f1, ignPath)
|
732
|
+
end
|
733
|
+
pushIgnoreList(File.basename(sourceDir), lst)
|
734
|
+
|
735
|
+
# Create set of encrypted filenames that belong to this directory, so
|
736
|
+
# we can delete encrypted versions of files that are no longer in the source
|
737
|
+
# directory.
|
738
|
+
encFilenameSet = Set.new
|
739
|
+
|
740
|
+
# If no encrypted directory exists, create one
|
741
|
+
if not File.directory?(encryptDir)
|
742
|
+
if @verbosity >= 1
|
743
|
+
puts("Creating encrypted directory: " + d(rel_path(encryptDir, @outputDir)))
|
744
|
+
end
|
745
|
+
|
746
|
+
if not @dryrun
|
747
|
+
if File.file?(encryptDir)
|
748
|
+
pth = rel_path(encryptDir, @outputDir)
|
749
|
+
|
750
|
+
if @verbosity >= 1
|
751
|
+
pr("Encrypting directory is overwriting existing file: " + pth)
|
752
|
+
end
|
753
|
+
|
754
|
+
remove_file_or_dir(encryptDir)
|
755
|
+
end
|
756
|
+
Dir.mkdir(encryptDir)
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
# Examine each file in source dir
|
761
|
+
dirc = dir_entries(sourceDir)
|
762
|
+
|
763
|
+
dirc.each do |f2|
|
764
|
+
# Convert string to ASCII-8BIT encoding.
|
765
|
+
f = to_ascii8(f2)
|
766
|
+
|
767
|
+
ignore = shouldFileBeIgnored(f)
|
768
|
+
|
769
|
+
filePath = File.join(sourceDir,f)
|
770
|
+
|
771
|
+
if ignore
|
772
|
+
!db || pr("(ignoring %s)\n", rel_path(filePath, @inputDir)) if @verbosity >= 1
|
773
|
+
next
|
774
|
+
end
|
775
|
+
|
776
|
+
if f.start_with?(ENCRFILENAMEPREFIX)
|
777
|
+
if @verbosity >= 0
|
778
|
+
msg = "Source file/dir found with name that looks encrypted: " + d(f)
|
779
|
+
pr("(%s)\n", f)
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
encrName = encryptFilename(f)
|
784
|
+
!db || pr(" encrypted filename %s => %s\n",d(f),d(encrName))
|
785
|
+
|
786
|
+
if @verifyEncryption
|
787
|
+
decrName = decryptFilename(encrName, false)
|
788
|
+
if !decrName || decrName != f
|
789
|
+
!db || pr("decrName encoding=#{decrName.encoding}\n f encoding=#{f.encoding}\n")
|
790
|
+
!db || hex_dump(decrName,"decrName")
|
791
|
+
!db || hex_dump(f,"f")
|
792
|
+
|
793
|
+
raise EncryptionVerificationException, \
|
794
|
+
"Filename #{f} did not encrypt/decrypt properly"
|
795
|
+
end
|
796
|
+
pr(" (filename #{f} encrypted correctly)\n") if @verbosity >= 0
|
797
|
+
end
|
798
|
+
|
799
|
+
encFilenameSet.add(encrName)
|
800
|
+
encrPath = File.join(encryptDir, encrName)
|
801
|
+
|
802
|
+
if File.directory?(filePath)
|
803
|
+
encryptDir(filePath, encrPath)
|
804
|
+
else
|
805
|
+
encryptFile(filePath, encrPath)
|
806
|
+
end
|
807
|
+
end
|
808
|
+
|
809
|
+
# Truncate global ignore list to original length
|
810
|
+
popIgnoreList()
|
811
|
+
|
812
|
+
# Examine every file in encrypted directory; delete those that don't correspond to source dir
|
813
|
+
|
814
|
+
# (if doing dry run, encrypt dir may not exist)
|
815
|
+
|
816
|
+
if File.directory?(encryptDir)
|
817
|
+
dire = dir_entries(encryptDir)
|
818
|
+
else
|
819
|
+
dire = []
|
820
|
+
end
|
821
|
+
|
822
|
+
dire.each do |f|
|
823
|
+
next if not f.start_with?(ENCRFILENAMEPREFIX)
|
824
|
+
|
825
|
+
if not encFilenameSet.member? f
|
826
|
+
begin
|
827
|
+
orphanOrigName = decryptFilename(f)
|
828
|
+
next if !orphanOrigName
|
829
|
+
orphanPath = os.path.join(encryptDir, f)
|
830
|
+
if @verbosity >= 1
|
831
|
+
printf("Removing encrypted version of missing (or ignored) file " \
|
832
|
+
+ rel_path(os.path.join(sourceDir, orphanOrigName), @inputDir) + ": " + orphanPath)
|
833
|
+
end
|
834
|
+
if !@dryrun
|
835
|
+
remove_file_or_dir(orphanPath)
|
836
|
+
end
|
837
|
+
rescue DecryptionError
|
838
|
+
# ignore...
|
839
|
+
end
|
840
|
+
end
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
|
845
|
+
# Decrypt all files (and, recursively, folders) within a directory to _recover folder
|
846
|
+
def recover(encryptDir, recoverDir)
|
847
|
+
db = warndb 0
|
848
|
+
!db || pr("recover enc_dir %s\n recoverDir %s\n",d(encryptDir),d(recoverDir))
|
849
|
+
|
850
|
+
# If no _recover directory exists, create one
|
851
|
+
if not File.directory?(recoverDir)
|
852
|
+
if File.file?(recoverDir)
|
853
|
+
raise RecoveryError, "Cannot replace existing file '" + recoverDir + "' with directory"
|
854
|
+
end
|
855
|
+
Dir.mkdir(recoverDir) if not @dryrun
|
856
|
+
end
|
857
|
+
|
858
|
+
if not File.directory?(encryptDir)
|
859
|
+
raise ArgumentError, "encrypt dir not found"
|
860
|
+
end
|
861
|
+
|
862
|
+
# Examine each file in encrypt dir
|
863
|
+
dirc = dir_entries(encryptDir)
|
864
|
+
!db || pr("files in encrypt dir=%s\n",d2(dirc))
|
865
|
+
|
866
|
+
dirc.each do |f|
|
867
|
+
|
868
|
+
!db || pr("...file=%s\n",d(f))
|
869
|
+
if shouldFileBeIgnored(f)
|
870
|
+
if @verbosity >= 1
|
871
|
+
pr("(ignoring %s)\n", rel_path(f, @inputDir))
|
872
|
+
end
|
873
|
+
next
|
874
|
+
end
|
875
|
+
|
876
|
+
# Only decrypt the filename if it is actually encrypted
|
877
|
+
origName = f
|
878
|
+
if origName.start_with?(ENCRFILENAMEPREFIX)
|
879
|
+
begin
|
880
|
+
decrName = decryptFilename(origName, true)
|
881
|
+
rescue DecryptionError
|
882
|
+
encPath = File.absolute_path(File.join(encryptDir,f))
|
883
|
+
pth = rel_path(encPath, @inputDir)
|
884
|
+
if !@password_verified
|
885
|
+
raise(DecryptionError,"Wrong password (cannot decrypt filename #{pth})")
|
886
|
+
end
|
887
|
+
|
888
|
+
if @verbosity >= 0
|
889
|
+
puts "Unable to decrypt filename: #{pth}"
|
890
|
+
end
|
891
|
+
next
|
892
|
+
end
|
893
|
+
origName = decrName
|
894
|
+
end
|
895
|
+
origPath = File.join(recoverDir, origName)
|
896
|
+
encrFullPath = File.join(encryptDir, f)
|
897
|
+
|
898
|
+
|
899
|
+
# If decrypted version already exists, and is more recent than the
|
900
|
+
# encrypted one, don't restore it.
|
901
|
+
|
902
|
+
if File.file?(encrFullPath) && File.file?(origPath) \
|
903
|
+
&& File.mtime(origPath) >= File.mtime(encrFullPath)
|
904
|
+
if @verbosity >= 1
|
905
|
+
puts(" " + rel_path(origPath,@outputDir) + " (still valid)")
|
906
|
+
end
|
907
|
+
next
|
908
|
+
end
|
909
|
+
|
910
|
+
if File.directory?(encrFullPath)
|
911
|
+
recover(encrFullPath, origPath)
|
912
|
+
else
|
913
|
+
pth = rel_path(origPath, @outputDir)
|
914
|
+
showProgress = @verbosity >= 0
|
915
|
+
if showProgress
|
916
|
+
pr("%s", pth)
|
917
|
+
end
|
918
|
+
begin
|
919
|
+
convertFile(encrFullPath, TEMPFILENAME, false, showProgress)
|
920
|
+
set_password_verified()
|
921
|
+
if not @dryrun
|
922
|
+
if File.file?(origPath)
|
923
|
+
remove_file_or_dir(origPath)
|
924
|
+
end
|
925
|
+
renameTempFile(origPath)
|
926
|
+
end
|
927
|
+
rescue DecryptionError => e
|
928
|
+
|
929
|
+
if !@password_verified
|
930
|
+
raise(DecryptionError,"Wrong password (cannot decrypt file #{pth})")
|
931
|
+
end
|
932
|
+
|
933
|
+
if @verbosity >= 0
|
934
|
+
msg = "Unable to decrypt file: #{pth} (cause: #{e.message})"
|
935
|
+
pr("\n%s\n", msg)
|
936
|
+
end
|
937
|
+
end
|
938
|
+
end
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
# Encrypt or decrypt a file (not a directory)
|
943
|
+
def convertFile(srcPath, destPath, encrypt, showProgress=false, verifying=false)
|
944
|
+
|
945
|
+
db = warndb 0
|
946
|
+
!db||pr("\n\n\n\nconvertFile\n %s =>\n %s;\n %s\n",d(srcPath),d(destPath),df(encrypt,"encrypt"))
|
947
|
+
|
948
|
+
showDots = false
|
949
|
+
if not @dryrun
|
950
|
+
|
951
|
+
fSize = File.size(srcPath)
|
952
|
+
|
953
|
+
fr = File.open(srcPath, 'rb')
|
954
|
+
fw = File.open(TEMPFILENAME, 'wb')
|
955
|
+
|
956
|
+
cSize = 100000
|
957
|
+
|
958
|
+
# Predict number of chunks required
|
959
|
+
chunksRemaining = [1, (fSize / cSize.to_f + 0.5).to_i].max
|
960
|
+
dotSize = 30
|
961
|
+
!db || pr(" fSize=%d, chunksRem=%d\n",fSize,chunksRemaining)
|
962
|
+
|
963
|
+
showDots = showProgress && chunksRemaining >= dotSize
|
964
|
+
!db || (showDots = false)
|
965
|
+
|
966
|
+
if showDots and verifying
|
967
|
+
pr(" verifying:")
|
968
|
+
end
|
969
|
+
|
970
|
+
!db || pr("\n encrKey=%s\n",dt(@encrKey))
|
971
|
+
|
972
|
+
bf = MyAES.new(encrypt, @encrKey)
|
973
|
+
|
974
|
+
proc = 0
|
975
|
+
|
976
|
+
pr(" [") if showDots
|
977
|
+
|
978
|
+
last = false
|
979
|
+
while not last
|
980
|
+
|
981
|
+
if showDots and (chunksRemaining % dotSize) == 0
|
982
|
+
pr(".")
|
983
|
+
end
|
984
|
+
chunksRemaining -= 1
|
985
|
+
|
986
|
+
chunkSize = fSize - proc
|
987
|
+
last = true
|
988
|
+
|
989
|
+
if chunksRemaining > 0
|
990
|
+
last = false
|
991
|
+
chunkSize = cSize
|
992
|
+
end
|
993
|
+
|
994
|
+
proc += chunkSize
|
995
|
+
chunk = fr.read(chunkSize)
|
996
|
+
|
997
|
+
if (!chunk) or chunk.size != chunkSize
|
998
|
+
raise IOError,"Failed to read bytes from file"
|
999
|
+
end
|
1000
|
+
|
1001
|
+
bf.add(chunk)
|
1002
|
+
if last
|
1003
|
+
bf.finish()
|
1004
|
+
end
|
1005
|
+
fw.write(bf.flush())
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
fr.close()
|
1009
|
+
fw.close()
|
1010
|
+
|
1011
|
+
renameTempFile(destPath)
|
1012
|
+
|
1013
|
+
pr("]") if showDots
|
1014
|
+
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
if showProgress and (showDots or not verifying)
|
1018
|
+
pr("\n")
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
end # class Repo
|
1023
|
+
|
1024
|
+
end # module LEnc
|