lenc 1.1.3 → 1.2.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 +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
|
|