lenc 1.1.3 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +40 -10
- data/bin/encr +5 -0
- data/lib/lenc.rb +1 -0
- data/lib/lenc/aes.rb +127 -52
- data/lib/lenc/encr.rb +64 -0
- data/lib/lenc/lencrypt.rb +13 -21
- data/lib/lenc/repo.rb +340 -160
- data/lib/lenc/tools.rb +22 -0
- data/test/test.rb +100 -32
- metadata +24 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81e06c0955051deca4de3a4687debbed0d38ab2e
|
4
|
+
data.tar.gz: 38b7cf28b8c71b99711d440a27dc7535fc82ed02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be50fb30cf452036677c901f05c5eaa81db9fcc6b071a2029ec87f11b4c89d9f9ff39606e97ec428720e304a2a02188253abadeea8db67cc9363ddfa22377011
|
7
|
+
data.tar.gz: c5e73819b059507bffa9aac25dbb7c22f21a256d278898e1139285627609a80c98b8a7bda407663e8d54c4a8ba2e63a1c0433c0ee8bcb777a966bc17b1ac6024
|
data/README.md
CHANGED
@@ -5,6 +5,9 @@ Lenc
|
|
5
5
|
LEnc is a Ruby gem that maintains encrypted repositories of files, enabling secure, encrypted
|
6
6
|
backups to free cloud services such as Dropbox, Google Drive, and Microsoft SkyDrive.
|
7
7
|
|
8
|
+
It can also encrypt or decrypt directory trees 'in place', so that the original files are
|
9
|
+
overwritten by their encrypted versions.
|
10
|
+
|
8
11
|
Written by Jeff Sember, March 2013.
|
9
12
|
|
10
13
|
[Source code documentation can be found here.](http://rubydoc.info/gems/lenc/frames)
|
@@ -23,8 +26,8 @@ is named "__lenc_repo__.txt").
|
|
23
26
|
versions of all the files found in the <source> directory. This directory
|
24
27
|
is usually mapped to a cloud service (e.g., Dropbox), or perhaps to a thumb drive.
|
25
28
|
|
26
|
-
|
27
|
-
|
29
|
+
NOTE: THE <encrypted> DIRECTORY IS MANAGED BY THE PROGRAM!
|
30
|
+
ANY FILES WRITTEN TO THIS DIRECTORY BY THE USER MAY BE DELETED.
|
28
31
|
|
29
32
|
* A \<recover\> directory. The program can recover a set of encrypted files here.
|
30
33
|
For safety, the this directory must not lie within an existing repository.
|
@@ -38,16 +41,16 @@ The program can be asked to perform one of the following tasks:
|
|
38
41
|
__Setting up a repository.__ Select a directory you wish to be the \<source\>
|
39
42
|
directory, and make it the current directory. Type:
|
40
43
|
|
41
|
-
|
44
|
+
lencrypt -i ENCDIR
|
42
45
|
|
43
|
-
with KEY a set of characters (
|
46
|
+
with KEY a set of characters (up to 56 letters) to be used as the encryption key,
|
44
47
|
and ENCDIR the name of the \<encrypted\> directory (it must not already exist, and
|
45
48
|
it cannot lie within the current directory's tree).
|
46
49
|
|
47
50
|
|
48
51
|
__Updating a repository.__ From within a \<source\> directory tree, type:
|
49
52
|
|
50
|
-
|
53
|
+
lencrypt
|
51
54
|
|
52
55
|
You will be prompted for the encryption key, and then the program will examine
|
53
56
|
which files within the \<source\> directory have been changed (since the repository
|
@@ -56,17 +59,44 @@ was created or last updated), and re-encrypt these into the \<encrypted\> direct
|
|
56
59
|
|
57
60
|
__Recovering encrypted files.__ Type:
|
58
61
|
|
59
|
-
|
62
|
+
lencrypt -r ENCDIR RECDIR
|
60
63
|
|
61
|
-
|
64
|
+
where ENCDIR contains an encrypted repository's files.
|
62
65
|
The recovered files will be stored in RECDIR.
|
63
66
|
|
64
|
-
|
65
67
|
Additional options can be found by typing:
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
+
lencrypt -h
|
69
70
|
|
71
|
+
Encrypting files 'in place'
|
72
|
+
----------------
|
73
|
+
In addition to maintaining encrypted repositories, the gem can also encrypt (and decrypt)
|
74
|
+
files 'in place', in effect replacing the files with their encrypted counterparts. To encrypt
|
75
|
+
a particular directory's contents (and all of its subdirectories), from that directory, type:
|
76
|
+
|
77
|
+
encr -i
|
78
|
+
|
79
|
+
This marks the directory as the root of an 'in place' repository (by writing a small configuration file).
|
80
|
+
You will be prompted for an encryption password.
|
81
|
+
|
82
|
+
Once such a repository has been defined, the files can be encrypted. From the repository directory (or
|
83
|
+
any of its subdirectories), type:
|
84
|
+
|
85
|
+
encr
|
86
|
+
|
87
|
+
After you enter a password, the program will encrypt all the files (or at least those not marked for
|
88
|
+
skipping within a .lencignore file). If you create any new unencrypted files, you can
|
89
|
+
repeat this command to encrypt them.
|
90
|
+
|
91
|
+
To decrypt the repository's contents, type:
|
92
|
+
|
93
|
+
encr -d
|
94
|
+
|
95
|
+
It is very important to remember the encryption password, since it is NOT stored anywhere by the
|
96
|
+
program. By design, you must enter the correct password twice before any files are encrypted: once when the
|
97
|
+
repository is initialized, and again when the actual encryption is to take place.
|
98
|
+
|
99
|
+
|
70
100
|
Ignore files
|
71
101
|
----------------
|
72
102
|
If desired, you can avoid storing selected files in the encryption repository.
|
data/bin/encr
ADDED
data/lib/lenc.rb
CHANGED
data/lib/lenc/aes.rb
CHANGED
@@ -142,7 +142,8 @@ module RepoInternal
|
|
142
142
|
|
143
143
|
=end
|
144
144
|
class MyAES
|
145
|
-
|
145
|
+
attr_accessor :prefix_mode
|
146
|
+
|
146
147
|
private
|
147
148
|
|
148
149
|
# decryptState values
|
@@ -153,6 +154,7 @@ module RepoInternal
|
|
153
154
|
# to help generate unique nonces (in conjunction with system clock)
|
154
155
|
@@nonceHelper = 0
|
155
156
|
|
157
|
+
|
156
158
|
# Construct a MyAES object to encrypt/decrypt a sequence of bytes.
|
157
159
|
# @param encrypting true for encryption, false for decryption
|
158
160
|
# @param key a bytearray of 4..56 bytes
|
@@ -179,7 +181,7 @@ module RepoInternal
|
|
179
181
|
key = bytes_to_str(key)
|
180
182
|
|
181
183
|
if key.size < 4 || key.size > 56
|
182
|
-
raise ArgumentError,
|
184
|
+
raise ArgumentError, "Key length ##{key.size} not 4..56 bytes"
|
183
185
|
end
|
184
186
|
|
185
187
|
# expand the key to be at least 32 bytes
|
@@ -304,28 +306,37 @@ module RepoInternal
|
|
304
306
|
raise LEnc::DecryptionError, "header doesn't verify"
|
305
307
|
end
|
306
308
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
309
|
+
# If we're in prefix mode, we're only interested in whether
|
310
|
+
# the chunk start matches the verification string above; we
|
311
|
+
# don't actually produce any decoded data (partly because we
|
312
|
+
# do not really know how many padding bytes there are in the
|
313
|
+
# decryption stream)
|
313
314
|
|
315
|
+
if !prefix_mode
|
316
|
+
|
317
|
+
nPadBytes = newData[CHUNK_VERIFY_SIZE].ord
|
318
|
+
actualEnd = csize - nPadBytes
|
319
|
+
if nPadBytes > 16 or actualEnd < CHUNK_HEADER_SIZE
|
320
|
+
raise LEnc::DecryptionError, "nPadBytes/actualEnd mismatch"
|
321
|
+
end
|
314
322
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
323
|
+
# Verify that the padding bytes have correct values
|
324
|
+
(actualEnd...csize).each do |i|
|
325
|
+
if newData[i] != PAD_CHAR
|
326
|
+
raise LEnc::DecryptionError,"padding char bad value"
|
327
|
+
end
|
319
328
|
end
|
329
|
+
|
330
|
+
newData = newData[CHUNK_HEADER_SIZE ... actualEnd]
|
331
|
+
|
332
|
+
@inputBuffer.slice!(0,csize)
|
333
|
+
@outputBuffer << newData
|
334
|
+
|
320
335
|
end
|
321
|
-
|
322
|
-
newData = newData[CHUNK_HEADER_SIZE ... actualEnd]
|
323
|
-
|
324
|
-
@decryptState = DS_WAITCHUNK
|
325
|
-
|
326
|
-
@inputBuffer.slice!(0,csize)
|
327
|
-
@outputBuffer << newData
|
328
336
|
|
337
|
+
@decryptState = DS_WAITCHUNK
|
338
|
+
|
339
|
+
|
329
340
|
end
|
330
341
|
|
331
342
|
incrNonce()
|
@@ -427,26 +438,43 @@ module RepoInternal
|
|
427
438
|
# Returns true iff the start of the string seems to decrypt correctly
|
428
439
|
# for the given password
|
429
440
|
def self.is_string_encrypted(key, test_str)
|
430
|
-
db = warndb
|
441
|
+
db = warndb 0
|
431
442
|
|
432
|
-
!db || hex_dump(test_str, "
|
443
|
+
!db || hex_dump(test_str, "is_string_encrypted?")
|
433
444
|
|
434
445
|
simple_str(test_str)
|
435
446
|
|
436
447
|
lnth = test_str.size
|
437
448
|
lnth -= NONCE_SIZE_SMALL
|
438
|
-
if lnth < AES_BLOCK_SIZE
|
439
|
-
!db || pr("
|
449
|
+
if lnth < AES_BLOCK_SIZE || lnth % AES_BLOCK_SIZE != 0
|
450
|
+
!db || pr(" bad # bytes\n")
|
440
451
|
return false
|
441
452
|
end
|
442
453
|
|
454
|
+
hdr_size = AES_BLOCK_SIZE
|
455
|
+
|
456
|
+
# This method is failing, I suspect because with the mode of AES we're using (CRC?) we can't
|
457
|
+
# decrypt only a single block, and must instead decrypt a complete chunk.
|
458
|
+
|
459
|
+
# No, now I think it's interpreting a bad 'padding' value (due to only decrypting partially)
|
460
|
+
# as indication of bad decryption
|
461
|
+
|
462
|
+
if false
|
463
|
+
warn("using full chunk size")
|
464
|
+
hdr_size = [lnth,CHUNK_SIZE_ENCR].min
|
465
|
+
end
|
466
|
+
|
443
467
|
begin
|
444
|
-
de = MyAES.new(false, key)
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
468
|
+
de = MyAES.new(false, key)
|
469
|
+
|
470
|
+
# Put this decryptor into prefix mode, so that we are only interested
|
471
|
+
# in whether the header verifies correctly
|
472
|
+
de.prefix_mode = true
|
473
|
+
|
474
|
+
de.finish(test_str[0...hdr_size + NONCE_SIZE_SMALL])
|
475
|
+
de.flush()
|
476
|
+
rescue LEnc::DecryptionError => e
|
477
|
+
!db || pr(" (caught DecryptionError #{e})\n")
|
450
478
|
return false
|
451
479
|
end
|
452
480
|
|
@@ -460,20 +488,34 @@ module RepoInternal
|
|
460
488
|
# for the given password, and the file is of the expected length.
|
461
489
|
def self.is_file_encrypted(key, path)
|
462
490
|
|
491
|
+
db = warndb 0
|
492
|
+
|
493
|
+
!db || pr("is_file_encrypted '#{path}'?\n")
|
463
494
|
# key = str_to_bytes(key)
|
464
495
|
|
465
496
|
if not File.file?(path)
|
497
|
+
!db || pr(" not a file\n")
|
466
498
|
return false
|
467
499
|
end
|
468
500
|
|
469
501
|
lnth = File.size(path)
|
470
502
|
minSize = NONCE_SIZE_SMALL + AES_BLOCK_SIZE
|
471
|
-
|
503
|
+
!db || pr(" file size=#{lnth}, minSize=#{minSize}\n")
|
504
|
+
if lnth < minSize or ((lnth - minSize) % AES_BLOCK_SIZE) != 0
|
505
|
+
!db || pr(" length not appropriate\n")
|
472
506
|
return false
|
473
507
|
end
|
474
508
|
|
509
|
+
if false
|
510
|
+
warn("using full size of file")
|
511
|
+
minSize = lnth
|
512
|
+
end
|
513
|
+
|
475
514
|
f = File.open(path,"rb")
|
476
|
-
|
515
|
+
s = f.read(minSize)
|
516
|
+
ret = is_string_encrypted(key, s)
|
517
|
+
!db || pr(" is_string_encrypted returning #{ret}\n")
|
518
|
+
ret
|
477
519
|
end
|
478
520
|
|
479
521
|
|
@@ -485,35 +527,68 @@ end # module RepoInternal
|
|
485
527
|
if main? __FILE__
|
486
528
|
|
487
529
|
s = ''
|
488
|
-
|
489
|
-
|
530
|
+
17.times {|x| s << (65+x).chr}
|
531
|
+
s *= 8
|
490
532
|
|
491
533
|
nonce = "abc" * 20
|
492
534
|
nonce = nonce[0...16]
|
493
535
|
key = "onefishtwofishredfishbluefish" * 3
|
494
536
|
key = key[0...32]
|
495
537
|
|
496
|
-
hex_dump(key,"key")
|
497
|
-
hex_dump(nonce,"nonce")
|
498
538
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
hex_dump(s,"calling aes.encrypt with")
|
511
|
-
hex_dump(enc,"aes.encrypt returned")
|
539
|
+
if false # this seems to work, disable for now
|
540
|
+
hex_dump(key,"key")
|
541
|
+
hex_dump(nonce,"nonce")
|
542
|
+
|
543
|
+
aes = OpenSSL::Cipher.new("AES-256-CBC")
|
544
|
+
|
545
|
+
aes.padding = 0
|
546
|
+
aes.encrypt
|
547
|
+
aes.key = key
|
548
|
+
|
549
|
+
aes.iv = nonce
|
512
550
|
|
513
|
-
|
551
|
+
enc = aes.update(s)
|
552
|
+
enc << aes.final
|
553
|
+
|
554
|
+
hex_dump(s,"calling aes.encrypt with")
|
555
|
+
hex_dump(enc,"aes.encrypt returned")
|
556
|
+
|
557
|
+
s = enc
|
558
|
+
|
559
|
+
require 'base64'
|
560
|
+
s = Base64.urlsafe_encode64(s)
|
561
|
+
hex_dump(s,"base64")
|
562
|
+
|
563
|
+
# Verify that we don't need every block to verify encryption
|
564
|
+
aes = OpenSSL::Cipher.new("AES-256-CBC")
|
565
|
+
aes.padding = 0
|
566
|
+
aes.decrypt
|
567
|
+
aes.key = key
|
568
|
+
aes.iv = nonce
|
569
|
+
|
570
|
+
dec = aes.update(enc[0...16])
|
571
|
+
dec << aes.final
|
572
|
+
|
573
|
+
hex_dump(dec,"decrypted")
|
574
|
+
end
|
575
|
+
|
576
|
+
include RepoInternal
|
577
|
+
|
578
|
+
aes = MyAES.new(true, key, nonce)
|
579
|
+
|
580
|
+
aes.finish(s)
|
581
|
+
enc = aes.flush()
|
582
|
+
|
583
|
+
hex_dump(enc,"encrypted")
|
584
|
+
|
585
|
+
aes = MyAES.new(false, key)
|
586
|
+
aes.finish(enc)
|
587
|
+
dec = aes.flush()
|
588
|
+
hex_dump(dec,"decrypted")
|
589
|
+
|
590
|
+
isenc = MyAES.is_string_encrypted(key, enc)
|
591
|
+
pr("is encrypted= #{isenc}\n")
|
514
592
|
|
515
|
-
require 'base64'
|
516
|
-
s = Base64.urlsafe_encode64(s)
|
517
|
-
hex_dump(s,"base64")
|
518
593
|
|
519
594
|
end
|
data/lib/lenc/encr.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'repo'
|
2
|
+
|
3
|
+
class EncrApp
|
4
|
+
include LEnc
|
5
|
+
|
6
|
+
def run(argv = ARGV)
|
7
|
+
|
8
|
+
req 'trollop'
|
9
|
+
p = Trollop::Parser.new do
|
10
|
+
opt :init, "create new singular repository"
|
11
|
+
opt :orignames, "(with --init) leave filenames unencrypted"
|
12
|
+
opt :encrypt, "encrypt files (default operation)"
|
13
|
+
opt :decrypt, "decrypt files"
|
14
|
+
opt :key, "encryption key", :type => :string
|
15
|
+
opt :verbose,"verbose operation"
|
16
|
+
opt :where, "specify source directory (default = current directory)", :type => :strings
|
17
|
+
opt :quiet, "quiet operation"
|
18
|
+
end
|
19
|
+
|
20
|
+
options = Trollop::with_standard_exception_handling p do
|
21
|
+
p.parse argv
|
22
|
+
end
|
23
|
+
|
24
|
+
v = 0
|
25
|
+
v = -1 if options[:quiet]
|
26
|
+
v = 1 if options[:verbose]
|
27
|
+
|
28
|
+
nOpt = 0
|
29
|
+
nOpt += 1 if options[:init]
|
30
|
+
nOpt += 1 if options[:encrypt]
|
31
|
+
nOpt += 1 if options[:decrypt]
|
32
|
+
|
33
|
+
p.die("Only one operation can be performed at a time.",nil) if nOpt > 1
|
34
|
+
|
35
|
+
r = Repo.new(:dryrun => options[:dryrun], :verbosity => v)
|
36
|
+
|
37
|
+
key = options[:key]
|
38
|
+
|
39
|
+
begin
|
40
|
+
|
41
|
+
if options[:init]
|
42
|
+
r.create(options[:where], key, nil, options[:orignames])
|
43
|
+
elsif options[:decrypt]
|
44
|
+
r.open(options[:where],key)
|
45
|
+
r.perform_decrypt
|
46
|
+
else
|
47
|
+
r.open(options[:where],key)
|
48
|
+
r.perform_encrypt
|
49
|
+
end
|
50
|
+
|
51
|
+
r.close()
|
52
|
+
rescue Exception =>e
|
53
|
+
puts("\nProblem encountered: #{e.message}")
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if __FILE__ == $0
|
60
|
+
args = ARGV
|
61
|
+
|
62
|
+
EncrApp.new().run(args)
|
63
|
+
end
|
64
|
+
|
data/lib/lenc/lencrypt.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
1
|
require_relative 'repo'
|
2
2
|
|
3
|
-
# The application script (i.e., the 'main program')
|
4
|
-
#
|
5
3
|
class LEncApp
|
6
4
|
include LEnc
|
7
5
|
|
8
6
|
def run(argv = ARGV)
|
9
|
-
|
7
|
+
|
10
8
|
req 'trollop'
|
11
9
|
p = Trollop::Parser.new do
|
12
|
-
opt :init, "create new encryption repository:
|
10
|
+
opt :init, "create new encryption repository: ENCDIR ", :type => :string
|
11
|
+
opt :key, "encryption key", :type => :string
|
13
12
|
opt :orignames, "(with --init) leave filenames unencrypted"
|
14
|
-
opt :
|
15
|
-
|
16
|
-
opt :update, "update encrypted repository (default operation): KEY", :default => ""
|
17
|
-
#opt :updatepwd, "specify key for update: KEY", :type => :string
|
18
|
-
opt :recover, "recover files from an encrypted repository: KEY ENCDIR RECDIR", :type => :strings
|
13
|
+
opt :update, "update encrypted repository (default operation)"
|
14
|
+
opt :recover, "recover files from an encrypted repository: ENCDIR RECDIR", :type => :strings
|
19
15
|
opt :where, "specify source directory (default = current directory)", :type => :string
|
20
16
|
opt :verbose,"verbose operation"
|
21
17
|
opt :quiet, "quiet operation"
|
@@ -34,12 +30,9 @@ class LEncApp
|
|
34
30
|
v = -1 if options[:quiet]
|
35
31
|
v = 1 if options[:verbose]
|
36
32
|
|
37
|
-
update_pwd = options[:update]
|
38
|
-
update_pwd = nil if update_pwd.size == 0
|
39
|
-
|
40
33
|
nOpt = 0
|
41
34
|
nOpt += 1 if options[:init]
|
42
|
-
nOpt += 1 if
|
35
|
+
nOpt += 1 if options[:update]
|
43
36
|
nOpt += 1 if options[:recover]
|
44
37
|
|
45
38
|
#pr("trollop opts = %s\n",d2(options))
|
@@ -52,20 +45,20 @@ class LEncApp
|
|
52
45
|
begin
|
53
46
|
|
54
47
|
if (a = options[:init])
|
55
|
-
|
56
|
-
|
57
|
-
r.create(options[:where], pwd, encDir, options[:orignames], options[:storekey])
|
48
|
+
encDir = a
|
49
|
+
r.create(options[:where], options[:key], encDir, options[:orignames])
|
58
50
|
elsif (a = options[:recover])
|
59
|
-
p.
|
60
|
-
r.perform_recovery(
|
51
|
+
p.die("Expecting: ENCDIR RECDIR",nil) if a.size != 2
|
52
|
+
r.perform_recovery(options[:key],a[0],a[1])
|
61
53
|
else
|
62
|
-
r.open(options[:where],
|
63
|
-
r.
|
54
|
+
r.open(options[:where],options[:key])
|
55
|
+
r.perform_encrypt()
|
64
56
|
end
|
65
57
|
|
66
58
|
r.close()
|
67
59
|
rescue Exception =>e
|
68
60
|
puts("\nProblem encountered: #{e.message}")
|
61
|
+
puts e.backtrace
|
69
62
|
end
|
70
63
|
|
71
64
|
end
|
@@ -74,7 +67,6 @@ end
|
|
74
67
|
if __FILE__ == $0
|
75
68
|
args = ARGV
|
76
69
|
|
77
|
-
|
78
70
|
LEncApp.new().run(args)
|
79
71
|
end
|
80
72
|
|