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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a71c838f760ff31a7d853f32b8f9fdcc04621b56
4
+ data.tar.gz: e4ee1f83e5fd2b158ec5397703f02402f1b1a182
5
+ SHA512:
6
+ metadata.gz: 1bc44af9c93ec3992406479f342e56146d8607f4f564ec594bdb67f23aa1f5e2b5afd31f0c2e32f76951d6e48ec615a0c24d86cadd50b703d98c2099c88a7da4
7
+ data.tar.gz: adf0c0379be49bbdb19ace972fa517f56bdf643ef19c51b5deb32376631dc19e098c0b6d301f1d62b5dc054078855efa69ebd8376f9ea806d94beefd2b5abb73
@@ -0,0 +1,2 @@
1
+ 2013-03-21 Jeff Sember jpsember@gmail.com
2
+ * Version 1.0.0 released
@@ -0,0 +1,90 @@
1
+ lenc : Maintains encrypted repositories of files, enabling secure, encrypted
2
+ backups to free cloud services such as Dropbox, Google Drive, and Microsoft SkyDrive.
3
+
4
+ Written and (c) by Jeff Sember, March 2013.
5
+ ================================================================================
6
+
7
+
8
+ The program manipulates three distinct directories:
9
+
10
+ 1) A <source> directory, which holds all the
11
+ files to be encrypted. The only file that the program modifies within this
12
+ directory tree is a hidden configuration file ".lenc" (on Windows, this file
13
+ is named "__lenc_repo__.txt").
14
+
15
+ 2) An <encrypted> directory, where the program stores the encrypted
16
+ versions of all the files found in the <source> directory. This directory
17
+ is usually mapped to a cloud service (e.g., Dropbox), or perhaps to a thumb drive.
18
+ NOTE: THE <encrypted> DIRECTORY IS MANAGED BY THE PROGRAM!
19
+ ANY FILES WRITTEN TO THIS DIRECTORY BY THE USER MAY BE DELETED.
20
+
21
+ 3) A <recover> directory. The program can recover a set of encrypted files here.
22
+ For safety, the this directory must not lie within an existing repository.
23
+
24
+
25
+ Running the program
26
+ ================================================================================
27
+
28
+ The program can be asked to perform one of the following tasks:
29
+
30
+ 1) Setting up a repository. Select a directory you wish to be the <source>
31
+ directory, and make it the current directory. Type:
32
+
33
+ lencrypt -i KEY ENCDIR
34
+
35
+ with KEY a set of characters (8 to 56 letters) to be used as the encryption key,
36
+ and ENCDIR the name of the <encrypted> directory (it must not already exist, and
37
+ it cannot lie within the current directory's tree).
38
+
39
+
40
+ 2) Updating a repository. From within a <source> directory tree, type:
41
+
42
+ lencrypt
43
+
44
+ The program will examine which files within the <source> directory have been
45
+ changed (since the repository was created or last updated), and re-encrypt these
46
+ into the <encrypted> directory.
47
+
48
+ 3) Recovering encrypted files. Type:
49
+
50
+ lencrypt -r KEY ENCDIR RECDIR
51
+
52
+ with KEY the key associated with a repository whose encrypted files are stored in ENCDIR.
53
+ The recovered files will be stored in RECDIR.
54
+
55
+
56
+
57
+ Ignore files
58
+ ================================================================================
59
+ If desired, you can avoid storing selected files in the encryption repository.
60
+ Within the <source> directory (or any of its subdirectories), place a text
61
+ file '.lencignore' with a list of file or directory names (or patterns) to be
62
+ ignored. Example:
63
+
64
+ # This is a comment
65
+ #
66
+ log
67
+ *.mp3
68
+ _SKIP_*
69
+
70
+ This causes the program to ignore any file or directory named 'log', as well as
71
+ any ending with ".mp3" or starting with "_SKIP_".
72
+
73
+ Some files are automatically ignored, e.g. ".DS_Store".
74
+
75
+ The format of ignore files is similar to that of .gitignore files. Details:
76
+
77
+ [] Each line should contain a single pattern representing files or directories to be ignored.
78
+ [] A line will be ignored (treated as a comment) if it is blank, or if it starts with '#'.
79
+ [] The path separator should be '/' (Mac, Unix) or '\' (Windows).
80
+ [] If a pattern starts with '#', you can precede it with '\' to avoid it being ignored.
81
+ [] Precede a pattern with '!' to specifically include files/directories, overriding any previous
82
+ matching pattern in a parent directory.
83
+ [] If a pattern ends with the path separator, it will be removed, and the pattern will
84
+ match only directories, not files.
85
+ [] The wildcard '*' matches for any sequence of zero or more characters.
86
+ [] The wildcard '?' matches any single character.
87
+ [] If the pattern contains any path separators, then the wildcards '*', '?' will not
88
+ match the path separator.
89
+
90
+
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'lenc'
4
+
5
+ LEncApp.new().run(ARGV)
@@ -0,0 +1 @@
1
+ require 'lenc/lencrypt'
@@ -0,0 +1,523 @@
1
+ require_relative 'tools'
2
+
3
+ require 'openssl'
4
+
5
+ module LEnc
6
+ class DecryptionError < Exception
7
+ end
8
+ end
9
+
10
+ module RepoInternal
11
+
12
+ # --------------------------------------------------------------
13
+
14
+ AES_BLOCK_SIZE = 16
15
+ PAD_BYTE = 254
16
+ PAD_CHAR = PAD_BYTE.chr
17
+
18
+ CHUNK_HEADER_SIZE = 8
19
+ CHUNK_VERIFY_SIZE = CHUNK_HEADER_SIZE - 1
20
+ CHUNK_VERIFY_STR = 0.chr * CHUNK_VERIFY_SIZE
21
+
22
+ # Size of input chunks during decryption (they include space for a header);
23
+ # must be a multiple of AES_BLOCK_SIZE
24
+ CHUNK_SIZE_DECR = 1 << 16
25
+
26
+ # Size of input chunks during encryption
27
+ CHUNK_SIZE_ENCR = CHUNK_SIZE_DECR - CHUNK_HEADER_SIZE
28
+
29
+ # The size of nonce the underlying API expects
30
+ NONCE_SIZE_LARGE = 16
31
+
32
+ # The size of nonce we'll be using (we'll pad it out with zeros
33
+ # when a full size one is required)
34
+ NONCE_SIZE_SMALL = 8
35
+
36
+
37
+
38
+ =begin
39
+ Wrapper for OpenSSL AES cipher.
40
+
41
+
42
+ Usage for encryption:
43
+ ----------------------------------------------------
44
+ original = "..." # bytes to encrypt (string)
45
+
46
+ key = "xxxx..." # encryption key (string of size 8..56)
47
+
48
+ en = MyAES.new(true, key) # construct an encryptor
49
+
50
+ en.finish(original) # add data to encrypt
51
+
52
+ encrypted = en.flush() # get encrypted bytes (string)
53
+ ----------------------------------------------------
54
+
55
+
56
+
57
+ Usage for decryption:
58
+ ----------------------------------------------------
59
+ encrypted = "..." # bytes to decrypt
60
+
61
+ key = "xxxx...."
62
+
63
+ de = MyAES.new(false, key) # construct a decryptor
64
+
65
+ de.finish(original) # add data to decrypt
66
+
67
+ decrypted = de.flush() # get decrypted bytes
68
+
69
+ ----------------------------------------------------
70
+
71
+
72
+ Use of nonces:
73
+ --------------
74
+ The above encryption example generates a new (hopefully unique) 'nonce'
75
+ which is an added security feature. It uses the system clock to do this.
76
+ This means the same file will produce different encrypted byte streams on
77
+ repeated encryption attempts, which may be undesirable. A fixed nonce
78
+ (for a particular input file) can be specified as an additional input:
79
+
80
+ nonce = "nnnn.." # only the first 8 bytes are used
81
+ en = MyAES.new(true, key, nonce)
82
+
83
+ The nonce, whether explicitly given or randomly generated, is added to the
84
+ encrypted stream; hence it need not be specified when decrypting.
85
+
86
+ Stream mode:
87
+ ------------
88
+ When processing large files, you may want to do them a chunk at a time.
89
+ Here's an example of encrypting using stream mode (decrypting is similar):
90
+
91
+
92
+ en = MyAES.new(true, key)
93
+
94
+ s = {size of input file}
95
+ n = 0
96
+ while n < s
97
+ c = [5000, s - n].min
98
+ en.add( {bytes n..n+c-1 from the input file} )
99
+ r = en.flush()
100
+ {append bytes r to output file}
101
+ end
102
+
103
+ en.finish()
104
+ r = en.flush()
105
+ {append bytes r to output file}
106
+
107
+ ----------------------------------------------------
108
+
109
+ Format of encrypted data:
110
+
111
+ [8] nonce (only the first 8 bytes of the nonce are actually used)
112
+
113
+ Followed by one or more encrypted chunks of length [k], where k is 65536, unless it's the last
114
+ chunk in the file, in which case it must be a multiple of 16.
115
+
116
+ The first bytes of each decrypted chunk is a header:
117
+ [7] zeros
118
+ [1] number of padding bytes present at end of block
119
+
120
+
121
+ For example, suppose a file of 71980 'source' bytes has been encrypted. The encrypted file will contain:
122
+ [8] nonce
123
+ [65536] first chunk, consisting of
124
+ [7] zeros
125
+ [1] zero, since this chunk needed no padding
126
+ [65528] 65528 encrypted source bytes
127
+ [6464] second chunk, consisting of
128
+ [7] zeros
129
+ [1] 4, indicating 4 padding bytes
130
+ [6456] 6452 encrypted source bytes plus 4 padding bytes
131
+
132
+ Observe that 65528 + 6452 = 71980.
133
+
134
+ The purpose of the [7] zeros in the (decrypted) chunk header are to indicate
135
+ whether decryption was successful (e.g., if the password was correct). The assumption
136
+ is that an incorrect password will generate 7 zeros in these locations with extremely low probability.
137
+
138
+ The byte used as a padding byte is 254.
139
+
140
+ If a file has length zero, then when encrypted, it will have the following structure:
141
+ [8] nonce
142
+ [16] chunk:
143
+ [7] zeros
144
+ [1] 8, indicating 8 padding bytes
145
+ [8] 0 encrypted source bytes plus 8 padding bytes
146
+
147
+ =end
148
+ class MyAES
149
+
150
+ private
151
+
152
+ # decryptState values
153
+ DS_WAITNONCE = 0 # waiting for nonce to appear in input
154
+ DS_WAITCHUNK = 1 # waiting for encrypted chunk to appear
155
+
156
+ # A class variable that increments with each encryptor object constructed,
157
+ # to help generate unique nonces (in conjunction with system clock)
158
+ @@nonceHelper = 0
159
+
160
+ # Construct a MyAES object to encrypt/decrypt a sequence of bytes.
161
+ # @param encrypting true for encryption, false for decryption
162
+ # @param key a bytearray of 4..56 bytes
163
+ # @param nonce a string of up to 16 characters; if nil, one is
164
+ # generated from the system clock
165
+ def initialize(encrypting, key, nonce=nil)
166
+
167
+ @encrypting = encrypting
168
+ @inputBuffer = ''
169
+ @outputBuffer = ''
170
+ @finished = false
171
+
172
+ if nonce && !encrypting
173
+ raise ArgumentError, \
174
+ "nonce should not be supplied during decryption"
175
+ end
176
+
177
+ if @encrypting
178
+ @nonceWritten = false
179
+ else
180
+ @decryptState = DS_WAITNONCE
181
+ end
182
+
183
+ key = bytes_to_str(key)
184
+
185
+ if key.size < 4 || key.size > 56
186
+ raise ArgumentError, 'Key length not 4..56 bytes'
187
+ end
188
+
189
+ # expand the key to be at least 32 bytes
190
+ k = (32.0 / key.size).ceil.to_i
191
+ key = key * k
192
+
193
+ @key = key[0...32]
194
+ @chunkCount = 0
195
+
196
+ # If we are encrypting, set nonce; otherwise, we must wait for some data to be available
197
+ if @encrypting
198
+ setNonce(nonce)
199
+ else
200
+ @chunkExpected = true
201
+ end
202
+ end
203
+
204
+ # Set nonce
205
+ # @param nonce a string; if nil, uses system clock and an internal
206
+ # counter to generate (hopefully) a unique value
207
+ def setNonce(nonce=nil)
208
+ if !nonce
209
+ # use date-time if no counter provided
210
+ ni = int_to_bytes(Time.now.usec)
211
+ ni.concat(int_to_bytes(@@nonceHelper))
212
+ @@nonceHelper += 1
213
+ nonce = bytes_to_str(ni)
214
+ end
215
+
216
+ raise ArgumentError if !(nonce.is_a? String )
217
+ simple_str(nonce)
218
+
219
+
220
+ nonce = str_sized(nonce,NONCE_SIZE_SMALL)
221
+
222
+ @nonce = nonce
223
+ end
224
+
225
+ def incrNonce()
226
+ c = @nonce
227
+ dig = NONCE_SIZE_SMALL - 1
228
+ while true
229
+ raise ArgumentError, "Nonce overflow" if dig < 0
230
+
231
+ q = c[dig].ord
232
+ if q != 0xff
233
+ c[dig] = (q+1).chr
234
+ break
235
+ end
236
+
237
+ c[dig] = 0.chr
238
+ dig -= 1
239
+ end
240
+ end
241
+
242
+
243
+ # Convert nonce from our version to one the OpenSSL expects.
244
+ # This may involve padding or truncating it as necessary to a
245
+ # fixed length.
246
+ def cvtNonce()
247
+ str_sized(@nonce,NONCE_SIZE_LARGE)
248
+ end
249
+
250
+ def buildAES()
251
+ aes = OpenSSL::Cipher.new("AES-256-CBC")
252
+ aes.padding = 0 # Not sure this is required, as we are doing padding ourselves;
253
+ # we should only be asking it to process blocks that need no padding
254
+ aes
255
+ end
256
+
257
+ def processChunk()
258
+
259
+ if @encrypting
260
+
261
+ csize = [CHUNK_SIZE_ENCR, @inputBuffer.size].min
262
+
263
+ padBytes = (-(csize + CHUNK_HEADER_SIZE)) & (AES_BLOCK_SIZE - 1)
264
+
265
+ csize += padBytes
266
+
267
+ if padBytes
268
+ @inputBuffer << PAD_CHAR * padBytes
269
+ end
270
+
271
+ aes = buildAES()
272
+ aes.encrypt
273
+ aes.key = @key
274
+ nonceStr = cvtNonce
275
+ aes.iv = nonceStr
276
+
277
+ if not @nonceWritten
278
+ @nonceWritten = true
279
+ @outputBuffer << nonceStr[0...NONCE_SIZE_SMALL]
280
+ end
281
+
282
+ cdata = "\0" * CHUNK_VERIFY_SIZE
283
+ cdata << padBytes.chr
284
+ cdata << @inputBuffer.slice!(0,csize)
285
+
286
+ @outputBuffer << aes.update(cdata)
287
+ @outputBuffer << aes.final
288
+
289
+ else # Decrypting
290
+
291
+ csize = [CHUNK_SIZE_DECR,@inputBuffer.size].min
292
+
293
+ # verify that the chunk size is a nonzero multiple of AES_BLOCK_SIZE bytes
294
+ if not csize or 0 != (csize & (AES_BLOCK_SIZE - 1))
295
+ raise LEnc::DecryptionError, "chunk size not a multiple of block size"
296
+ end
297
+
298
+ aes = buildAES()
299
+ aes.decrypt
300
+ aes.key = @key
301
+ aes.iv = cvtNonce()
302
+
303
+ cdata = @inputBuffer[0...csize]
304
+ newData = aes.update(cdata)
305
+ newData << aes.final
306
+
307
+ if !newData.start_with? CHUNK_VERIFY_STR
308
+ raise LEnc::DecryptionError, "header doesn't verify"
309
+ end
310
+
311
+ nPadBytes = newData[CHUNK_VERIFY_SIZE].ord
312
+ actualEnd = csize - nPadBytes
313
+ if nPadBytes > 16 or actualEnd < CHUNK_HEADER_SIZE
314
+ raise LEnc::DecryptionError, "nPadBytes/actualEnd mismatch"
315
+ end
316
+
317
+
318
+
319
+ # Verify that the padding bytes have correct values
320
+ (actualEnd...csize).each do |i|
321
+ if newData[i] != PAD_CHAR
322
+ raise LEnc::DecryptionError,"padding char bad value"
323
+ end
324
+ end
325
+
326
+ newData = newData[CHUNK_HEADER_SIZE ... actualEnd]
327
+
328
+ @decryptState = DS_WAITCHUNK
329
+
330
+ @inputBuffer.slice!(0,csize)
331
+ @outputBuffer << newData
332
+
333
+ end
334
+
335
+ incrNonce()
336
+ @chunkCount += 1
337
+ end
338
+
339
+ public
340
+
341
+ # Process additional input bytes, encrypting (or decrypting) its contents
342
+ # @param data string containing input bytes
343
+ def add(data)
344
+
345
+ raise IllegalStateException if @finished
346
+
347
+ simple_str(data)
348
+
349
+ @inputBuffer << data #.concat(data)
350
+
351
+ while true
352
+
353
+ if not @encrypting
354
+
355
+ # Extract nonce if we're waiting for it and it is now available
356
+ if @decryptState == DS_WAITNONCE
357
+ break if @inputBuffer.size < NONCE_SIZE_SMALL
358
+ setNonce(@inputBuffer.slice!(0...NONCE_SIZE_SMALL))
359
+ @decryptState = DS_WAITCHUNK
360
+ next
361
+ end
362
+
363
+ # If we don't have a full chunk, exit
364
+ # (the last chunk may be smaller; we'll test for this when finishing up)
365
+ break if @inputBuffer.size < CHUNK_SIZE_DECR
366
+
367
+ else
368
+ break if @inputBuffer.size < CHUNK_SIZE_ENCR
369
+ end
370
+
371
+ # Process chunk and repeat
372
+ processChunk()
373
+ end
374
+ end
375
+
376
+ # Stop the encryption/decryption process.
377
+ # Processes any bytes that may have been buffered (since encryption occurs in
378
+ # 16 byte blocks at a time).
379
+ #
380
+ # @param data optional final input string to process before finishing
381
+ #
382
+ def finish(data = nil)
383
+
384
+ add(data) if data
385
+
386
+ raise IllegalStateException if @finished
387
+
388
+ @finished = true
389
+
390
+ inpLen = @inputBuffer.size
391
+
392
+ if @encrypting
393
+ # If input buffer is not empty, or we haven't written a first chunk (which contains the nonce),
394
+ # encrypt a chunk
395
+ if inpLen or (not @nonceWritten)
396
+ processChunk()
397
+ end
398
+ else
399
+
400
+ # We must be at WAITCHUNK with an input buffer that is a multiple of _AES_BLOCK_SIZE bytes in length
401
+ if @decryptState != DS_WAITCHUNK or 0 != (inpLen & (AES_BLOCK_SIZE-1))
402
+ raise LEnc::DecryptionError, "decrypt state problem"
403
+ end
404
+
405
+ # We expect a chunk if there's more input, or if we've never processed a chunk.
406
+ if inpLen != 0 or @chunkCount == 0
407
+ processChunk()
408
+ end
409
+ end
410
+ end
411
+
412
+
413
+ # Return any output bytes that have been generated since the last call to flush()
414
+ # @return string containing bytes
415
+ def flush()
416
+ ret = @outputBuffer
417
+ @outputBuffer = ''
418
+ return ret
419
+ end
420
+
421
+ # Strip the header from an encrypted string
422
+ def strip_encryption_header( encr_str)
423
+ return encr_str[CHUNK_HEADER_SIZE..-1]
424
+ end
425
+
426
+ # Determines if a string is the start of an encrypted sequence
427
+ #
428
+ # @param key password to use (string)
429
+ # @param test_str the string to test
430
+ #
431
+ # Returns true iff the start of the string seems to decrypt correctly
432
+ # for the given password
433
+ def is_string_encrypted(key, test_str)
434
+ db = false
435
+
436
+ !db || hexDump(test_str, "areBytesEncrypted?")
437
+
438
+ simple_str(test_str)
439
+
440
+ lnth = test_str.size
441
+ lnth -= NONCE_SIZE_SMALL
442
+ if lnth < AES_BLOCK_SIZE
443
+ !db || pr(" insufficient # bytes\n")
444
+ return false
445
+ end
446
+
447
+ begin
448
+ de = MyAES(False, key)
449
+ de.finish(test_str[:_AES_BLOCK_SIZE + NONCE_SIZE_SMALL])
450
+ decr = de.flush()
451
+ !db || hexDump(decr,"decrypted successfully")
452
+ rescue LEnc::DecryptionError
453
+ !db || pr(" (caught DecryptionError)\n")
454
+ return false
455
+ end
456
+
457
+ true
458
+ end
459
+
460
+ # Determines if a file is an encrypted file
461
+ # @param key password to use (string, or array of bytes)
462
+ # @param path path to file
463
+ # Returns true iff the start of the file seems to decrypt correctly
464
+ # for the given password, and the file is of the expected length.
465
+ def is_file_encrypted(key, path)
466
+
467
+ # key = str_to_bytes(key)
468
+
469
+ if not File.file?(path)
470
+ return false
471
+ end
472
+
473
+ lnth = File.size(path)
474
+ minSize = NONCE_SIZE_SMALL + AES_BLOCK_SIZE
475
+ if lnth < minSize or ((lnth - minSize) % _AES_BLOCK_SIZE) != 0
476
+ return false
477
+ end
478
+
479
+ f = File.open(path,"rb")
480
+ return is_string_encrypted(key, f.read(minSize))
481
+ end
482
+
483
+
484
+ end # end of class MyAES
485
+
486
+ end # module RepoInternal
487
+
488
+
489
+ if main? __FILE__
490
+
491
+ s = ''
492
+ 16.times {|x| s << (65+x).chr}
493
+
494
+
495
+ nonce = "abc" * 20
496
+ nonce = nonce[0...16]
497
+ key = "onefishtwofishredfishbluefish" * 3
498
+ key = key[0...32]
499
+
500
+ hex_dump(key,"key")
501
+ hex_dump(nonce,"nonce")
502
+
503
+ aes = OpenSSL::Cipher.new("AES-256-CBC")
504
+
505
+ aes.padding = 0
506
+ aes.encrypt
507
+ aes.key = key
508
+
509
+ aes.iv = nonce
510
+
511
+ enc = aes.update(s)
512
+ enc << aes.final
513
+
514
+ hex_dump(s,"calling aes.encrypt with")
515
+ hex_dump(enc,"aes.encrypt returned")
516
+
517
+ s = enc
518
+
519
+ require 'base64'
520
+ s = Base64.urlsafe_encode64(s)
521
+ hex_dump(s,"base64")
522
+
523
+ end