edb 0.5 → 0.6
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/.gitignore +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +6 -2
- data/Rakefile +1 -1
- data/bin/edb +16 -4
- data/edb.gemspec +1 -0
- data/lib/edb/cryptography.rb +26 -6
- data/lib/edb/cryptography/aes_256_cbc.rb +28 -26
- data/lib/edb/dbms/mysql.rb +1 -1
- data/lib/edb/dbms/postgresql.rb +2 -2
- data/lib/edb/dumper.rb +2 -2
- data/lib/edb/version.rb +1 -1
- data/specs/cryptography/aes_256_cbc_spec.rb +57 -0
- data/specs/dbms/mysql_spec.rb +22 -0
- data/specs/dbms/postgresql_spec.rb +23 -0
- data/specs/dbms_spec.rb +5 -5
- data/specs/spec_helper.rb +3 -0
- data/specs/storage_spec.rb +4 -4
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf3bbfcc12b3ea9bf8b8d2da7e5acce50e69295f
|
4
|
+
data.tar.gz: cdd15ff978c970706a69c1dc0b7f3ca1fd20a9a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 427fafb18b6e04e331d689b1efc4a0fa7d3cd4b29afa9f380d5695d3ee6ae54066e27c95ce6d1aef430a90d187d8fb65a23dd1b62f182c4e824ed9fe93d78f1f
|
7
|
+
data.tar.gz: 14fbd57f7b9989d8ff27e8035aa3ebd79335b03bbcfcf46f6a82b324bf27b561c9873eddd60a1ac07d4983f4331f856cace405bf47c3aea773618d87a78d75a9
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
edb (0.
|
4
|
+
edb (0.6)
|
5
5
|
aws-sdk (= 1.59.1)
|
6
|
+
fast_secure_compare (~> 1.0)
|
6
7
|
ftp_sync (~> 0.4)
|
7
8
|
hkdf (~> 0.2)
|
8
9
|
|
@@ -15,6 +16,7 @@ GEM
|
|
15
16
|
json (~> 1.4)
|
16
17
|
nokogiri (>= 1.4.4)
|
17
18
|
diff-lcs (1.2.5)
|
19
|
+
fast_secure_compare (1.0.1)
|
18
20
|
ftp_sync (0.4.3)
|
19
21
|
net-ftp-list (>= 2.1.1)
|
20
22
|
hkdf (0.2.0)
|
data/README.md
CHANGED
@@ -11,10 +11,14 @@ The first one deals with the actual backup process of your database. The second
|
|
11
11
|
`$ gem install edb`
|
12
12
|
|
13
13
|
## Run
|
14
|
-
|
14
|
+
Customize `example/edb.yml` (remember also to change the `secret` by running `$ edb -k`) and then:
|
15
15
|
|
16
16
|
`$ edb example/edb.yml`
|
17
17
|
|
18
|
+
To decrypt the encrypted files:
|
19
|
+
|
20
|
+
`$ edb example/edb.yml -d [FILE]`
|
21
|
+
|
18
22
|
Consider also to add *EDB* to your cronjobs.
|
19
23
|
|
20
24
|
## Available modules
|
@@ -24,7 +28,7 @@ Consider also to add *EDB* to your cronjobs.
|
|
24
28
|
|
25
29
|
## FAQ
|
26
30
|
**Q:** What if I want to dump two or three MySQL databases?
|
27
|
-
*A:* Just add a `:MySQL:` block for every database you need to dump.
|
31
|
+
*A:* Just add a `:MySQL:` block for every database you need to dump.
|
28
32
|
|
29
33
|
|
30
34
|
**Q:** What if I want to save a database to S3 and another one into my local filesystem?
|
data/Rakefile
CHANGED
data/bin/edb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
2
|
#--
|
3
3
|
# Copyright(C) 2015 Giovanni Capuano <webmaster@giovannicapuano.net>
|
4
4
|
#
|
@@ -26,9 +26,11 @@ require 'edb'
|
|
26
26
|
require 'yaml'
|
27
27
|
require 'optparse'
|
28
28
|
|
29
|
+
@tasks = {}
|
30
|
+
|
29
31
|
OptionParser.new do |opts|
|
30
32
|
opts.version = EDB::VERSION
|
31
|
-
opts.banner = "Usage: edb [-
|
33
|
+
opts.banner = "Usage: edb [-kd] [CONFIG_FILE]"
|
32
34
|
|
33
35
|
opts.on('-k', '--generate-key', 'Generate a random key of 32 characters') do
|
34
36
|
require 'securerandom'
|
@@ -36,6 +38,10 @@ OptionParser.new do |opts|
|
|
36
38
|
puts SecureRandom.hex(32)
|
37
39
|
exit
|
38
40
|
end
|
41
|
+
|
42
|
+
opts.on('-d', '--decrypt [FILE]', 'Decrypt given file') do |file|
|
43
|
+
@tasks[:decrypt] = { file_to_decrypt: File.expand_path(file) }
|
44
|
+
end
|
39
45
|
end.parse!
|
40
46
|
|
41
47
|
config_file = ARGV[0]
|
@@ -43,5 +49,11 @@ if !config_file || !config_file.end_with?('.yml') || !File.exists?(config_file)
|
|
43
49
|
abort 'Please provide a valid configuration file.'
|
44
50
|
end
|
45
51
|
|
46
|
-
opts = YAML.load_file(config_file)
|
47
|
-
|
52
|
+
EDB.opts = YAML.load_file(config_file)
|
53
|
+
|
54
|
+
if @tasks.has_key?(:decrypt)
|
55
|
+
ciphering_method = EDB.opts[:CRYPTOGRAPHY].first[0]
|
56
|
+
EDB::Cryptography.decrypt ciphering_method, @tasks[:decrypt][:file_to_decrypt]
|
57
|
+
else
|
58
|
+
EDB::Dumper.new.run
|
59
|
+
end
|
data/edb.gemspec
CHANGED
data/lib/edb/cryptography.rb
CHANGED
@@ -28,14 +28,34 @@ module EDB
|
|
28
28
|
class << self
|
29
29
|
include ::EDB::IsModuleSupported
|
30
30
|
|
31
|
-
def encrypt(method,
|
32
|
-
|
33
|
-
|
31
|
+
def encrypt(method, filename)
|
32
|
+
::EDB::Logger.log(:info, "Encrypting #{filename}...")
|
33
|
+
|
34
|
+
ciphered_data = File.open(filename, 'rb') do |file|
|
35
|
+
data = file.read
|
36
|
+
|
37
|
+
this_module = to_module(method)
|
38
|
+
ciphered_data = this_module.encrypt(data)
|
39
|
+
end
|
40
|
+
|
41
|
+
File.open(filename, 'wb') do |file|
|
42
|
+
file.write(ciphered_data)
|
43
|
+
end
|
34
44
|
end
|
35
45
|
|
36
|
-
def decrypt(method,
|
37
|
-
|
38
|
-
|
46
|
+
def decrypt(method, filename)
|
47
|
+
::EDB::Logger.log(:info, "Decrypting #{filename}...")
|
48
|
+
|
49
|
+
data = File.open(filename, 'rb') do |file|
|
50
|
+
ciphered_data = file.read
|
51
|
+
|
52
|
+
this_module = to_module(method)
|
53
|
+
data = this_module.decrypt(ciphered_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
File.open("#{filename}.dec", 'wb') do |file|
|
57
|
+
file.write(data)
|
58
|
+
end
|
39
59
|
end
|
40
60
|
|
41
61
|
private
|
@@ -23,52 +23,54 @@
|
|
23
23
|
#++
|
24
24
|
require 'openssl'
|
25
25
|
require 'hkdf'
|
26
|
+
require 'fast_secure_compare/fast_secure_compare'
|
26
27
|
|
27
28
|
module EDB
|
28
29
|
module Cryptography
|
29
30
|
module AES_256_CBC
|
30
31
|
class << self
|
31
|
-
def encrypt(
|
32
|
-
|
32
|
+
def encrypt(data)
|
33
|
+
raise "Cannot encrypt #{filename}: It's empty" if data.empty?
|
33
34
|
|
34
35
|
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
35
36
|
cipher.encrypt
|
36
|
-
cipher.key = hash_key(::EDB.opts[:CRYPTOGRAPHY][:AES_256_CBC][:secret])
|
37
|
-
cipher.iv = iv = cipher.random_iv
|
38
37
|
|
39
|
-
|
40
|
-
|
38
|
+
cipher.key, authentication_key = hash_keychain(::EDB.opts[:CRYPTOGRAPHY][:AES_256_CBC][:secret])
|
39
|
+
cipher.iv = iv = cipher.random_iv
|
41
40
|
|
42
|
-
|
43
|
-
|
44
|
-
ciphered_content << iv
|
45
|
-
file.write(ciphered_content)
|
46
|
-
end
|
47
|
-
end
|
41
|
+
ciphered_data = cipher.update(data) + cipher.final
|
42
|
+
ciphered_data << iv
|
48
43
|
|
49
|
-
|
50
|
-
|
44
|
+
authentication = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), authentication_key, ciphered_data)
|
45
|
+
ciphered_data << authentication
|
46
|
+
end
|
51
47
|
|
52
|
-
|
53
|
-
raise "Cannot decrypt #{
|
54
|
-
ciphered_content_length = ciphered_content.length
|
48
|
+
def decrypt(ciphered_data)
|
49
|
+
raise "Cannot decrypt #{filename}: It's empty" if ciphered_data.length < 64
|
55
50
|
|
56
51
|
decipher = OpenSSL::Cipher.new('AES-256-CBC')
|
57
52
|
decipher.decrypt
|
58
53
|
|
59
|
-
|
60
|
-
decipher.
|
54
|
+
authentication = slice_str!(ciphered_data, 32)
|
55
|
+
decipher.key, authentication_key = hash_keychain(::EDB.opts[:CRYPTOGRAPHY][:AES_256_CBC][:secret])
|
56
|
+
|
57
|
+
new_authentication = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), authentication_key, ciphered_data)
|
58
|
+
raise 'Authentication failed.' unless FastSecureCompare.compare(authentication, new_authentication)
|
61
59
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
file.write(deciphered_content)
|
66
|
-
end
|
60
|
+
decipher.iv = slice_str!(ciphered_data, 16)
|
61
|
+
|
62
|
+
deciphered_data = decipher.update(ciphered_data) + decipher.final
|
67
63
|
end
|
68
64
|
|
69
65
|
private
|
70
|
-
def
|
71
|
-
|
66
|
+
def slice_str!(str, n)
|
67
|
+
len = str.length
|
68
|
+
str.slice!(len - n, len)
|
69
|
+
end
|
70
|
+
|
71
|
+
def hash_keychain(s)
|
72
|
+
hkdf = HKDF.new(s)
|
73
|
+
[ hkdf.next_bytes(64), hkdf.next_bytes(64) ]
|
72
74
|
end
|
73
75
|
end
|
74
76
|
end
|
data/lib/edb/dbms/mysql.rb
CHANGED
data/lib/edb/dbms/postgresql.rb
CHANGED
@@ -35,11 +35,11 @@ module EDB
|
|
35
35
|
|
36
36
|
::EDB::Logger.log(:info, "Dumping #{db[:database]}...")
|
37
37
|
pg_dump = db[:binpath] && !db[:binpath].empty? ? File.join(db[:binpath], 'pg_dump') : 'pg_dump'
|
38
|
-
system "PGPASSWORD='#{db[:password]}' #{pg_dump} -h #{db[:host]} -p #{db[:port]} -U #{db[:username]} -F c -b -f '#{files[:dump]}' #{db[:database]}"
|
38
|
+
Kernel.system "PGPASSWORD='#{db[:password]}' #{pg_dump} -h #{db[:host]} -p #{db[:port]} -U #{db[:username]} -F c -b -f '#{files[:dump]}' #{db[:database]}"
|
39
39
|
|
40
40
|
::EDB::Logger.log(:info, 'Dumping the cluster...')
|
41
41
|
pg_dumpall = db[:binpath] && !db[:binpath].empty? ? File.join(db[:binpath], 'pg_dumpall') : 'pg_dumpall'
|
42
|
-
system "PGPASSWORD='#{db[:password]}' #{pg_dumpall} -h #{db[:host]} -p #{db[:port]} -U #{db[:username]} -f '#{files[:cluster]}'"
|
42
|
+
Kernel.system "PGPASSWORD='#{db[:password]}' #{pg_dumpall} -h #{db[:host]} -p #{db[:port]} -U #{db[:username]} -f '#{files[:cluster]}'"
|
43
43
|
|
44
44
|
files.values
|
45
45
|
end
|
data/lib/edb/dumper.rb
CHANGED
data/lib/edb/version.rb
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe EDB::Cryptography::AES_256_CBC do
|
4
|
+
describe '#encrypt' do
|
5
|
+
context 'given string is empty' do
|
6
|
+
it 'raises an exception' do
|
7
|
+
expect { EDB::Cryptography::AES_256_CBC.encrypt('') }.to raise_error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'non-empty string is given' do
|
12
|
+
let(:data) { 'kanbaru is mai waifu' }
|
13
|
+
let(:ciphered_data) { EDB::Cryptography::AES_256_CBC.encrypt(data) }
|
14
|
+
|
15
|
+
it 'returns a valid AES-256-CBC ciphered string' do
|
16
|
+
expect(ciphered_data.length).to be 80
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#decrypt' do
|
22
|
+
context 'given data is less than 64 chars long' do
|
23
|
+
it 'raises an exception' do
|
24
|
+
expect { EDB::Cryptography::AES_256_CBC.decrypt('H' * 63) }.to raise_error
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'valid data is given' do
|
29
|
+
let(:real_data) { 'kanbaru is mai waifu' }
|
30
|
+
let(:ciphered_data) { EDB::Cryptography::AES_256_CBC.encrypt(real_data) }
|
31
|
+
let(:data) { EDB::Cryptography::AES_256_CBC.decrypt(ciphered_data) }
|
32
|
+
|
33
|
+
it 'returns a string matching the original given one' do
|
34
|
+
expect(data).to eq real_data
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#slice_str!' do
|
40
|
+
let(:str) { 'senjougahara' }
|
41
|
+
let(:sliced) { EDB::Cryptography::AES_256_CBC.instance_exec(str) { |str| slice_str!(str, 6) } }
|
42
|
+
|
43
|
+
it 'returns and remove the last n characters from given string' do
|
44
|
+
expect(sliced).to eq 'gahara'
|
45
|
+
expect(str).to eq 'senjou'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#hash_keychain' do
|
50
|
+
let(:keychain) { EDB::Cryptography::AES_256_CBC.instance_eval { hash_keychain('hitagi') } }
|
51
|
+
|
52
|
+
it 'returns an array with two 64 bytes long keys' do
|
53
|
+
expect(keychain[0].length).to be 64
|
54
|
+
expect(keychain[1].length).to be 64
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe EDB::DBMS::MySQL do
|
4
|
+
describe '#backup' do
|
5
|
+
let(:dump) { '/Applications/MAMP/Library/bin/mysqldump --user=username --password=password --single-transaction sample_database > ./sample_database.sql' }
|
6
|
+
|
7
|
+
it 'calls mysqldump correctly' do
|
8
|
+
allow(Kernel).to receive(:system).and_wrap_original do |m, *args|
|
9
|
+
expect(args[0]).to eq dump
|
10
|
+
end
|
11
|
+
|
12
|
+
EDB::DBMS::MySQL.backup('.')
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'returns the the files that have been created' do
|
16
|
+
allow(Kernel).to receive(:system).and_wrap_original {}
|
17
|
+
|
18
|
+
results = EDB::DBMS::MySQL.backup('.')
|
19
|
+
expect(results).to eq ['./sample_database.sql']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe EDB::DBMS::PostgreSQL do
|
4
|
+
describe '#backup' do
|
5
|
+
let(:dump) { "PGPASSWORD='password' /Applications/Postgres.app/Contents/Versions/9.3/bin/pg_dump -h localhost -p 5432 -U username -F c -b -f './sample_database.sql' sample_database" }
|
6
|
+
let(:cluster) { "PGPASSWORD='password' /Applications/Postgres.app/Contents/Versions/9.3/bin/pg_dumpall -h localhost -p 5432 -U username -f './cluster.sql'" }
|
7
|
+
|
8
|
+
it 'calls pg_dump correctly' do
|
9
|
+
allow(Kernel).to receive(:system).and_wrap_original do |m, *args|
|
10
|
+
expect(args[0]).to satisfy { |c| [dump, cluster].include?(c) }
|
11
|
+
end
|
12
|
+
|
13
|
+
EDB::DBMS::PostgreSQL.backup('.')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the the files that have been created' do
|
17
|
+
allow(Kernel).to receive(:system).and_wrap_original {}
|
18
|
+
|
19
|
+
results = EDB::DBMS::PostgreSQL.backup('.')
|
20
|
+
expect(results).to eq ['./sample_database.sql', './cluster.sql']
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/specs/dbms_spec.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
require_relative 'spec_helper'
|
2
2
|
|
3
|
-
describe EDB::
|
3
|
+
describe EDB::DBMS do
|
4
4
|
describe '#supports?' do
|
5
|
-
context 'given
|
5
|
+
context 'given DBMS device is supported' do
|
6
6
|
it 'returns true' do
|
7
|
-
expect(EDB::
|
7
|
+
expect(EDB::DBMS.supports?('MySQL')).to be_truthy
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
context 'given
|
11
|
+
context 'given DBMS device is not supported' do
|
12
12
|
it 'returns false' do
|
13
|
-
expect(EDB::
|
13
|
+
expect(EDB::DBMS.supports?('CongoDB')).to be_falsy
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
data/specs/spec_helper.rb
CHANGED
data/specs/storage_spec.rb
CHANGED
@@ -2,15 +2,15 @@ require_relative 'spec_helper'
|
|
2
2
|
|
3
3
|
describe EDB::Cryptography do
|
4
4
|
describe '#supports?' do
|
5
|
-
context 'given
|
5
|
+
context 'given storage device is supported' do
|
6
6
|
it 'returns true' do
|
7
|
-
expect(EDB::
|
7
|
+
expect(EDB::Storage.supports?('S3')).to be_truthy
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
context 'given
|
11
|
+
context 'given storage device is not supported' do
|
12
12
|
it 'returns false' do
|
13
|
-
expect(EDB::
|
13
|
+
expect(EDB::Storage.supports?('CappellaAmbrosiana')).to be_falsy
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: edb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.6'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Giovanni Capuano
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: fast_secure_compare
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
55
69
|
description: EDB aims to be a framework to make and manage backups of your database.
|
56
70
|
email: webmaster@giovannicapuano.net
|
57
71
|
executables:
|
@@ -83,7 +97,10 @@ files:
|
|
83
97
|
- lib/edb/storage/ftp.rb
|
84
98
|
- lib/edb/storage/s3.rb
|
85
99
|
- lib/edb/version.rb
|
100
|
+
- specs/cryptography/aes_256_cbc_spec.rb
|
86
101
|
- specs/cryptography_spec.rb
|
102
|
+
- specs/dbms/mysql_spec.rb
|
103
|
+
- specs/dbms/postgresql_spec.rb
|
87
104
|
- specs/dbms_spec.rb
|
88
105
|
- specs/spec_helper.rb
|
89
106
|
- specs/storage_spec.rb
|