batch-kit 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +165 -0
- data/lib/batch-kit.rb +9 -0
- data/lib/batch-kit/arguments.rb +57 -0
- data/lib/batch-kit/config.rb +517 -0
- data/lib/batch-kit/configurable.rb +68 -0
- data/lib/batch-kit/core_ext/enumerable.rb +97 -0
- data/lib/batch-kit/core_ext/file.rb +69 -0
- data/lib/batch-kit/core_ext/file_utils.rb +103 -0
- data/lib/batch-kit/core_ext/hash.rb +17 -0
- data/lib/batch-kit/core_ext/numeric.rb +17 -0
- data/lib/batch-kit/core_ext/string.rb +88 -0
- data/lib/batch-kit/database.rb +133 -0
- data/lib/batch-kit/database/java_util_log_handler.rb +65 -0
- data/lib/batch-kit/database/log4r_outputter.rb +57 -0
- data/lib/batch-kit/database/models.rb +548 -0
- data/lib/batch-kit/database/schema.rb +229 -0
- data/lib/batch-kit/encryption.rb +7 -0
- data/lib/batch-kit/encryption/java_encryption.rb +178 -0
- data/lib/batch-kit/encryption/ruby_encryption.rb +175 -0
- data/lib/batch-kit/events.rb +157 -0
- data/lib/batch-kit/framework/acts_as_job.rb +197 -0
- data/lib/batch-kit/framework/acts_as_sequence.rb +123 -0
- data/lib/batch-kit/framework/definable.rb +169 -0
- data/lib/batch-kit/framework/job.rb +121 -0
- data/lib/batch-kit/framework/job_definition.rb +105 -0
- data/lib/batch-kit/framework/job_run.rb +145 -0
- data/lib/batch-kit/framework/runnable.rb +235 -0
- data/lib/batch-kit/framework/sequence.rb +87 -0
- data/lib/batch-kit/framework/sequence_definition.rb +38 -0
- data/lib/batch-kit/framework/sequence_run.rb +48 -0
- data/lib/batch-kit/framework/task_definition.rb +89 -0
- data/lib/batch-kit/framework/task_run.rb +53 -0
- data/lib/batch-kit/helpers/date_time.rb +54 -0
- data/lib/batch-kit/helpers/email.rb +198 -0
- data/lib/batch-kit/helpers/html.rb +175 -0
- data/lib/batch-kit/helpers/process.rb +101 -0
- data/lib/batch-kit/helpers/zip.rb +30 -0
- data/lib/batch-kit/job.rb +11 -0
- data/lib/batch-kit/lockable.rb +138 -0
- data/lib/batch-kit/loggable.rb +78 -0
- data/lib/batch-kit/logging.rb +169 -0
- data/lib/batch-kit/logging/java_util_logger.rb +87 -0
- data/lib/batch-kit/logging/log4r_logger.rb +71 -0
- data/lib/batch-kit/logging/null_logger.rb +35 -0
- data/lib/batch-kit/logging/stdout_logger.rb +96 -0
- data/lib/batch-kit/resources.rb +191 -0
- data/lib/batch-kit/sequence.rb +7 -0
- metadata +122 -0
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require_relative '../logging'
|
3
|
+
|
4
|
+
|
5
|
+
class BatchKit
|
6
|
+
|
7
|
+
class Database
|
8
|
+
|
9
|
+
# Manages the database tables and connection used to record batch-kit
|
10
|
+
# events.
|
11
|
+
class Schema
|
12
|
+
|
13
|
+
attr_reader :log
|
14
|
+
|
15
|
+
|
16
|
+
# Create a batch-kit schema instance
|
17
|
+
#
|
18
|
+
# @param options [Hash] An options hash.
|
19
|
+
# @option options [Symbol] :log_level The level of database messages
|
20
|
+
# that should be visible (default is :error).
|
21
|
+
def initialize(options = {})
|
22
|
+
@log = BatchKit::LogManager.logger('batch-kit.database.schema')
|
23
|
+
@log.level = options.fetch(:log_level, :error)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Returns the Sequel database connection.
|
28
|
+
def connection
|
29
|
+
@conn
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Connects to the database determined by +args+.
|
34
|
+
#
|
35
|
+
# @see Sequel#connect for details on the different arguments that
|
36
|
+
# are required to connect to your database.
|
37
|
+
def connect(*args)
|
38
|
+
Sequel.default_timezone = :utc
|
39
|
+
@conn = Sequel.connect(*args)
|
40
|
+
@conn.loggers << @log
|
41
|
+
@conn.autosequence = true
|
42
|
+
|
43
|
+
create_tables unless deployed?
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# @return true if the batch-kit database tables have been deployed.
|
48
|
+
def deployed?
|
49
|
+
@conn.table_exists?(:batch_md5)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Drops all database tables used for tracking batch-kit processes.
|
54
|
+
def drop_tables
|
55
|
+
@conn.drop_table(:batch_requestor)
|
56
|
+
@conn.drop_table(:batch_request)
|
57
|
+
@conn.drop_table(:batch_lock)
|
58
|
+
@conn.drop_table(:batch_task_run)
|
59
|
+
@conn.drop_table(:batch_task)
|
60
|
+
@conn.drop_table(:batch_job_run_failure)
|
61
|
+
@conn.drop_table(:batch_job_run_log)
|
62
|
+
@conn.drop_table(:batch_job_run_arg)
|
63
|
+
@conn.drop_table(:batch_job_run)
|
64
|
+
@conn.drop_table(:batch_job)
|
65
|
+
@conn.drop_table(:batch_md5)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# Creates all database tables needed for tracking batch-kit processes.
|
70
|
+
def create_tables
|
71
|
+
# MD5 table, used to hold hashes of objects to detect version changes
|
72
|
+
@conn.create_table?(:batch_md5) do
|
73
|
+
primary_key :md5_id
|
74
|
+
String :object_type, size: 30, null: false
|
75
|
+
String :object_name, size: 255, null: false
|
76
|
+
Fixnum :object_version, null: false
|
77
|
+
String :md5_digest, size: 32, null: false
|
78
|
+
DateTime :md5_created_at, null: false
|
79
|
+
unique [:object_type, :object_name, :object_version]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Job table, holds details of job definitions
|
83
|
+
@conn.create_table?(:batch_job) do
|
84
|
+
primary_key :job_id
|
85
|
+
String :job_name, size: 80, null: false
|
86
|
+
String :job_class, size: 80, null: false
|
87
|
+
String :job_method, size: 80, null: false
|
88
|
+
String :job_desc, size: 255, null: true
|
89
|
+
String :job_host, size: 50, null: false
|
90
|
+
String :job_file, size: 255, null: false
|
91
|
+
Fixnum :job_version, null: false
|
92
|
+
foreign_key :job_file_md5_id, :batch_md5, null: false
|
93
|
+
Fixnum :job_run_count, null: false
|
94
|
+
Fixnum :job_success_count, null: false
|
95
|
+
Fixnum :job_fail_count, null: false
|
96
|
+
Fixnum :job_abort_count, null: false
|
97
|
+
Bignum :job_min_success_duration_ms, null: false
|
98
|
+
Bignum :job_max_success_duration_ms, null: false
|
99
|
+
Bignum :job_mean_success_duration_ms, null: false
|
100
|
+
Bignum :job_m2_success_duration_ms, null: false
|
101
|
+
DateTime :job_created_at, null: false
|
102
|
+
DateTime :job_modified_at, null: false
|
103
|
+
DateTime :job_last_run_at, null: true
|
104
|
+
unique [:job_host, :job_name]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Task table, holds details of task definitions
|
108
|
+
@conn.create_table?(:batch_task) do
|
109
|
+
primary_key :task_id
|
110
|
+
foreign_key :job_id, :batch_job, null: false
|
111
|
+
Fixnum :job_version, null: false
|
112
|
+
String :task_name, size: 80, null: false
|
113
|
+
String :task_class, size: 80, null: false
|
114
|
+
String :task_method, size: 80, null: false
|
115
|
+
String :task_desc, size: 255, null: true
|
116
|
+
TrueClass :task_current_flag, null: false, default: true
|
117
|
+
Fixnum :task_run_count, null: false
|
118
|
+
Fixnum :task_success_count, null: false
|
119
|
+
Fixnum :task_fail_count, null: false
|
120
|
+
Fixnum :task_abort_count, null: false
|
121
|
+
Bignum :task_min_success_duration_ms, null: false
|
122
|
+
Bignum :task_max_success_duration_ms, null: false
|
123
|
+
Bignum :task_mean_success_duration_ms, null: false
|
124
|
+
Bignum :task_m2_success_duration_ms, null: false
|
125
|
+
DateTime :task_created_at, null: false
|
126
|
+
DateTime :task_modified_at, null: false
|
127
|
+
DateTime :task_last_run_at, null: true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Job run table, holds details of a single execution of a job
|
131
|
+
@conn.create_table?(:batch_job_run) do
|
132
|
+
primary_key :job_run
|
133
|
+
foreign_key :job_id, :batch_job, null: false
|
134
|
+
String :job_instance, size: 80, null: true
|
135
|
+
Fixnum :job_version, null: false
|
136
|
+
String :job_run_by, size: 50, null: false
|
137
|
+
String :job_cmd_line, size: 2000, null: true
|
138
|
+
DateTime :job_start_time, null: false
|
139
|
+
DateTime :job_end_time, null: true
|
140
|
+
String :job_status, size: 12, null: false
|
141
|
+
Fixnum :job_pid, null: true
|
142
|
+
Fixnum :job_exit_code, null: true
|
143
|
+
TrueClass :job_purged_flag, null: false, default: false
|
144
|
+
end
|
145
|
+
|
146
|
+
# Job run arguments table, holds details of the arguments used on a job
|
147
|
+
@conn.create_table?(:batch_job_run_arg) do
|
148
|
+
foreign_key :job_run, :batch_job_run
|
149
|
+
String :job_arg_name, size: 50, null: false
|
150
|
+
String :job_arg_value, size: 255, null: true
|
151
|
+
primary_key [:job_run, :job_arg_name]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Job run log table, holds log records for a job
|
155
|
+
@conn.create_table?(:batch_job_run_log) do
|
156
|
+
foreign_key :job_run, :batch_job_run
|
157
|
+
Fixnum :log_line, null: false
|
158
|
+
DateTime :log_time, null: false
|
159
|
+
String :log_name, size: 40, null: false
|
160
|
+
String :log_level, size: 8, null: false
|
161
|
+
String :thread_id, size: 8, null: true
|
162
|
+
String :log_message, size: 1000, null: false
|
163
|
+
primary_key [:job_run, :log_line]
|
164
|
+
end
|
165
|
+
|
166
|
+
# Job failure table, holds exception details for job failures
|
167
|
+
@conn.create_table?(:batch_job_run_failure) do
|
168
|
+
# We don't use an FK here, because we want to be able to retain
|
169
|
+
# failure details longer than we retain job runs
|
170
|
+
Fixnum :job_run, null: false
|
171
|
+
foreign_key :job_id, :batch_job, null: false
|
172
|
+
Fixnum :job_version, null: false
|
173
|
+
DateTime :job_failed_at, null: false
|
174
|
+
String :exception_message, size: 500, null: false
|
175
|
+
String :exception_backtrace, size: 4000, null: false
|
176
|
+
end
|
177
|
+
|
178
|
+
# Task run table, holds details of a single execution of a task
|
179
|
+
@conn.create_table?(:batch_task_run) do
|
180
|
+
primary_key :task_run
|
181
|
+
foreign_key :task_id, :batch_task, null: false
|
182
|
+
foreign_key :job_run, :batch_job_run, null: false
|
183
|
+
String :task_instance, size: 80, null: true
|
184
|
+
DateTime :task_start_time, null: false
|
185
|
+
DateTime :task_end_time, null: true
|
186
|
+
String :task_status, size: 12, null: false
|
187
|
+
Fixnum :task_exit_code, null: true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Lock table, holds details of the current locks
|
191
|
+
@conn.create_table?(:batch_lock) do
|
192
|
+
String :lock_name, type: String, size: 50, unique_key: true
|
193
|
+
foreign_key :job_run, :batch_job_run
|
194
|
+
DateTime :lock_created_at, null: false
|
195
|
+
DateTime :lock_expires_at, null: false
|
196
|
+
end
|
197
|
+
|
198
|
+
# Request table, holds details of job run requests
|
199
|
+
@conn.create_table?(:batch_request) do
|
200
|
+
primary_key :request_id
|
201
|
+
String :request_type, size: 80, null: false
|
202
|
+
String :request_label, size: 80, null: false
|
203
|
+
String :request_source, size: 80, null: false
|
204
|
+
String :job_host, size: 30, null: false
|
205
|
+
String :job_file, size: 255, null: false
|
206
|
+
String :job_args, size: 2000, null: true
|
207
|
+
String :job_working_dir, size: 255, null: true
|
208
|
+
TrueClass :processed_flag, default: false, null: false
|
209
|
+
foreign_key :job_run, :batch_job_run, null: true
|
210
|
+
DateTime :request_created_at, null: false
|
211
|
+
DateTime :request_start_at, null: true
|
212
|
+
DateTime :request_launched_at, null: true
|
213
|
+
end
|
214
|
+
|
215
|
+
# Requestor table, holds details of job run requestors
|
216
|
+
@conn.create_table?(:batch_requestor) do
|
217
|
+
foreign_key :request_id, :batch_request, null: false
|
218
|
+
DateTime :requested_at, null: false
|
219
|
+
String :requested_by, size: 80, null: false
|
220
|
+
String :email_address, size: 100, null: true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
class BatchKit
|
2
|
+
|
3
|
+
# Performs password-based encryption (PBE) and decryption using the AES 128-bit
|
4
|
+
# cipher algorithm. Password-based encryption uses a password or passphrase
|
5
|
+
# as the shared secret key with which to encrypt/decrypt other text.
|
6
|
+
#
|
7
|
+
# As with all key-based encryption schemes, the encrypted values are only as
|
8
|
+
# secure as the key used to encrypt them. If the key is accessible to a
|
9
|
+
# hacker, any values encrypted with the key can be decrypted, so the key
|
10
|
+
# should be stored separately from the encrypted values, and be kept as
|
11
|
+
# secure as possible.
|
12
|
+
module Encryption
|
13
|
+
|
14
|
+
include_package 'java.security'
|
15
|
+
include_package 'javax.crypto'
|
16
|
+
include_package 'javax.crypto.spec'
|
17
|
+
|
18
|
+
# Use PBKDF2 with SHA-1 as the key hashing algorithm
|
19
|
+
# The NIST specifically names SHA1 as an acceptable hashing algorithm
|
20
|
+
# for PKBDF2
|
21
|
+
KEY_ALGORITHM = 'PBKDF2WithHmacSHA1'
|
22
|
+
|
23
|
+
# Iteration count; NIST recommends at least 1000 iterations
|
24
|
+
KEY_ITERATION_COUNT = 5000
|
25
|
+
|
26
|
+
# Length of the generated key in bits
|
27
|
+
KEY_DERIVED_LENGTH = 128
|
28
|
+
|
29
|
+
# Algorithm to use for encryption
|
30
|
+
CIPHER_ALGORITHM = 'AES/CBC/PKCS5Padding'
|
31
|
+
|
32
|
+
# Salt; a random 8 bytes used to generate an encryption key from a key
|
33
|
+
# password / passphrase.
|
34
|
+
# @note The same value *must* be used for the salt when converting a key
|
35
|
+
# password/passphrase into an encryption key for both encryptionn and
|
36
|
+
# decryption.
|
37
|
+
SALT = [70, 211, 28, 57, 192, 6, 78, 163].pack("CCCCCCCC").to_java_bytes
|
38
|
+
|
39
|
+
|
40
|
+
# Generate a random master key that can be used to encrypt/decrypt other
|
41
|
+
# sensitive data such as passwords. The master key must be stored some
|
42
|
+
# place separate from the values it is used to encrypt.
|
43
|
+
#
|
44
|
+
# @return [String] A random string of text that can be used as a master
|
45
|
+
# key for encrypting/decrypting other values.
|
46
|
+
def generate_master_key()
|
47
|
+
java.util.UUID.randomUUID().toString()
|
48
|
+
end
|
49
|
+
module_function :generate_master_key
|
50
|
+
|
51
|
+
|
52
|
+
# Encrypt the supplied +clear_text+, using +key_text+ as the pass-phrase.
|
53
|
+
#
|
54
|
+
# @param key_text [String] The clear-text pass-phrase to use as the key
|
55
|
+
# for encryption.
|
56
|
+
# @param clear_text [String] The cleartext string to be encrypted.
|
57
|
+
# @param salt [Array<Byte>] An 8-byte array of random values to use as
|
58
|
+
# the salt.
|
59
|
+
# @return [String] A base-64 encoded string representing the encrypted
|
60
|
+
# +clear_text+ value.
|
61
|
+
def encrypt(key_text, clear_text, salt = SALT)
|
62
|
+
key = generate_key(key_text, salt)
|
63
|
+
encipher(key, clear_text)
|
64
|
+
end
|
65
|
+
module_function :encrypt
|
66
|
+
|
67
|
+
|
68
|
+
# Encipher the supplied +clear_text+, using +key+ as the encryption key.
|
69
|
+
#
|
70
|
+
# Note: this method is possibly less secure than #encrypt, since it uses
|
71
|
+
# the actual key in the enciphering, rather than an SHA1 hash of the key.
|
72
|
+
#
|
73
|
+
# @param key [SecretKeySpec] The key to use for encryption.
|
74
|
+
# @param clear_text [String] The cleartext string to be encrypted.
|
75
|
+
# @param cipher_algorithm [String] The name of the cipher algorithm to
|
76
|
+
# use when encrypting the clear_text.
|
77
|
+
# @return [String] A base-64 encoded string representing the encrypted
|
78
|
+
# +clear_text+ value.
|
79
|
+
def encipher(key, clear_text, cipher_algorithm = CIPHER_ALGORITHM)
|
80
|
+
cipher = Cipher.getInstance(cipher_algorithm)
|
81
|
+
cipher.init(Cipher::ENCRYPT_MODE, key)
|
82
|
+
params = cipher.getParameters()
|
83
|
+
iv = params.getParameterSpec(IvParameterSpec.java_class).getIV()
|
84
|
+
cipher_bytes = cipher.doFinal(clear_text.to_java_bytes)
|
85
|
+
# Combine IV and cipher bytes, and base-64 encode
|
86
|
+
buffer_bytes = Java::byte[KEY_DERIVED_LENGTH / 8 + cipher_bytes.length].new
|
87
|
+
buffer = java.nio.ByteBuffer.wrap(buffer_bytes)
|
88
|
+
buffer.put(iv)
|
89
|
+
buffer.put(cipher_bytes)
|
90
|
+
base64_encode(buffer_bytes)
|
91
|
+
end
|
92
|
+
module_function :encipher
|
93
|
+
|
94
|
+
|
95
|
+
# Decrypt the supplied +cipher_text+, using +key_text+ as the pass-phrase.
|
96
|
+
#
|
97
|
+
# @param key_text [String] The clear-text pass-phrase to use as the key
|
98
|
+
# for decryption.
|
99
|
+
# @param cipher_text [String] A base-64 encoded cipher text string that
|
100
|
+
# is to be decrypted.
|
101
|
+
# @param salt [Array<Byte>] An 8-byte array of random values to use as
|
102
|
+
# the salt. Like the +key_text+, it is imperative that the same value
|
103
|
+
# is used for the salt when decrypting a previously encrypted value.
|
104
|
+
# @return [String] The clear text that was encrypted.
|
105
|
+
def decrypt(key_text, cipher_text, salt = SALT)
|
106
|
+
key = generate_key(key_text, salt)
|
107
|
+
decipher(key, cipher_text)
|
108
|
+
end
|
109
|
+
module_function :decrypt
|
110
|
+
|
111
|
+
|
112
|
+
# Decipher the supplited +cipher_text+, using +key+ as the decipher key.
|
113
|
+
#
|
114
|
+
# @param key [SecretKeySpec] The key used for encryption.
|
115
|
+
# @param cipher_text [String] A base-64 encoded cipher text string that
|
116
|
+
# is to be decrypted.
|
117
|
+
# @param cipher_algorithm [String] The name of the cipher algorithm used
|
118
|
+
# to encrypt the clear_text.
|
119
|
+
# @return [String] The clear text that was encrypted.
|
120
|
+
def decipher(key, cipher_text, cipher_algorithm = CIPHER_ALGORITHM)
|
121
|
+
cipher = Cipher.getInstance(cipher_algorithm)
|
122
|
+
buffer_bytes = base64_decode(cipher_text)
|
123
|
+
# Unpack IV and cipher bytes
|
124
|
+
iv_bytes = Java::byte[KEY_DERIVED_LENGTH / 8].new
|
125
|
+
cipher_bytes = Java::byte[buffer_bytes.length - KEY_DERIVED_LENGTH / 8].new
|
126
|
+
buffer = java.nio.ByteBuffer.wrap(buffer_bytes)
|
127
|
+
buffer.get(iv_bytes)
|
128
|
+
buffer.get(cipher_bytes)
|
129
|
+
cipher.init(Cipher::DECRYPT_MODE, key, IvParameterSpec.new(iv_bytes))
|
130
|
+
String.from_java_bytes(cipher.doFinal(cipher_bytes))
|
131
|
+
end
|
132
|
+
module_function :decipher
|
133
|
+
|
134
|
+
|
135
|
+
# Convert a byte array to a base-64 encoded string representation.
|
136
|
+
#
|
137
|
+
# @param bytes [String, Array<byte>] A String or Java byte-array to be
|
138
|
+
# encoded.
|
139
|
+
# @return [String] A base-64 encoded String.
|
140
|
+
def base64_encode(bytes)
|
141
|
+
bytes = bytes.to_java_bytes if bytes.is_a?(String)
|
142
|
+
javax.xml.bind.DatatypeConverter.printBase64Binary(bytes)
|
143
|
+
end
|
144
|
+
module_function :base64_encode
|
145
|
+
|
146
|
+
|
147
|
+
# Convert a base-64 encoded string to a byte array.
|
148
|
+
#
|
149
|
+
# @param str [String] A base-64 encoded String.
|
150
|
+
# @return [Array<byte>] A Java byte-array containing the decoded bytes.
|
151
|
+
def base64_decode(str)
|
152
|
+
javax.xml.bind.DatatypeConverter.parseBase64Binary(str)
|
153
|
+
end
|
154
|
+
module_function :base64_decode
|
155
|
+
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Generates the key to be used for encryption/decryption, based on
|
160
|
+
# +key_text+ and +salt+.
|
161
|
+
#
|
162
|
+
# @param key_text [String] A clear-text password or pass phrase that is
|
163
|
+
# to be used to derive the encryption key.
|
164
|
+
# @param salt [Array<Byte>] An 8-byte array of random values to use as
|
165
|
+
# the salt.
|
166
|
+
# @return [SecretKey] A key that can be used for encryption/decryption.
|
167
|
+
def generate_key(key_text, salt)
|
168
|
+
factory = SecretKeyFactory.getInstance(KEY_ALGORITHM)
|
169
|
+
key_spec = PBEKeySpec.new(key_text.to_s.to_java.toCharArray(), salt,
|
170
|
+
KEY_ITERATION_COUNT, KEY_DERIVED_LENGTH)
|
171
|
+
key = factory.generateSecret(key_spec)
|
172
|
+
SecretKeySpec.new(key.getEncoded(), CIPHER_ALGORITHM.split('/').first)
|
173
|
+
end
|
174
|
+
module_function :generate_key
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
|
6
|
+
class BatchKit
|
7
|
+
|
8
|
+
# Performs password-based encryption (PBE) and decryption using the AES 128-bit
|
9
|
+
# cipher algorithm. Password-based encryption uses a password or passphrase
|
10
|
+
# as the shared secret key with which to encrypt/decrypt other text.
|
11
|
+
#
|
12
|
+
# As with all key-based encryption schemes, the encrypted values are only as
|
13
|
+
# secure as the key used to encrypt them. If the key is accessible to a
|
14
|
+
# hacker, any values encrypted with the key can be decrypted, so the key
|
15
|
+
# should be stored separately from the encrypted values, and be kept as
|
16
|
+
# secure as possible.
|
17
|
+
module Encryption
|
18
|
+
|
19
|
+
# Use PBKDF2 with SHA-1 as the key hashing algorithm
|
20
|
+
# The NIST specifically names SHA1 as an acceptable hashing algorithm
|
21
|
+
# for PKBDF2
|
22
|
+
KEY_ALGORITHM = :pbkdf2_hmac_sha1
|
23
|
+
|
24
|
+
# Iteration count; NIST recommends at least 1000 iterations
|
25
|
+
KEY_ITERATION_COUNT = 5000
|
26
|
+
|
27
|
+
# Length of the generated key in bits
|
28
|
+
KEY_DERIVED_LENGTH = 128
|
29
|
+
|
30
|
+
# Algorithm to use for encryption
|
31
|
+
CIPHER_ALGORITHM = 'AES-128-CBC'
|
32
|
+
|
33
|
+
# Salt; a random 8 bytes used to generate an encryption key from a key
|
34
|
+
# password / passphrase.
|
35
|
+
#
|
36
|
+
# @note The same value *must* be used for the salt when converting a key
|
37
|
+
# password/passphrase into an encryption key for both encryptionn and
|
38
|
+
# decryption.
|
39
|
+
SALT = [70, 211, 28, 57, 192, 6, 78, 163].pack("CCCCCCCC")
|
40
|
+
|
41
|
+
|
42
|
+
# Generate a random master key that can be used to encrypt/decrypt other
|
43
|
+
# sensitive data such as passwords. The master key must be stored some
|
44
|
+
# place separate from the values it is used to encrypt.
|
45
|
+
#
|
46
|
+
# @return [String] A random string of text that can be used as a master
|
47
|
+
# key for encrypting/decrypting other values.
|
48
|
+
def generate_master_key()
|
49
|
+
require 'securerandom'
|
50
|
+
SecureRandom.uuid
|
51
|
+
end
|
52
|
+
module_function :generate_master_key
|
53
|
+
|
54
|
+
|
55
|
+
# Encrypt the supplied +clear_text+, using +key_text+ as the pass-phrase.
|
56
|
+
#
|
57
|
+
# @param key_text [String] The clear-text pass-phrase to use as the key
|
58
|
+
# for encryption.
|
59
|
+
# @param clear_text [String] The cleartext string to be encrypted.
|
60
|
+
# @param salt [Array<Byte>] An 8-byte array of random values to use as
|
61
|
+
# the salt.
|
62
|
+
# @return [String] A base-64 encoded string representing the encrypted
|
63
|
+
# +clear_text+ value.
|
64
|
+
def encrypt(key_text, clear_text, salt = SALT)
|
65
|
+
key = generate_key(key_text, salt)
|
66
|
+
encipher(key, clear_text)
|
67
|
+
end
|
68
|
+
module_function :encrypt
|
69
|
+
|
70
|
+
|
71
|
+
# Encipher the supplied +clear_text+, using +key+ as the encryption key.
|
72
|
+
#
|
73
|
+
# Note: this method is possibly less secure than #encrypt, since it uses
|
74
|
+
# the actual key in the enciphering, rather than an SHA1 hash of the key.
|
75
|
+
#
|
76
|
+
# @param key [String] The key to use for encryption.
|
77
|
+
# @param clear_text [String] The cleartext string to be encrypted.
|
78
|
+
# @param cipher_algorithm [String] The name of the cipher algorithm to
|
79
|
+
# use when encrypting the clear_text.
|
80
|
+
# @return [String] A base-64 encoded string representing the encrypted
|
81
|
+
# +clear_text+ value.
|
82
|
+
def encipher(key, clear_text, cipher_algorithm = CIPHER_ALGORITHM)
|
83
|
+
cipher = OpenSSL::Cipher.new(cipher_algorithm)
|
84
|
+
cipher.encrypt
|
85
|
+
cipher.key = key
|
86
|
+
|
87
|
+
iv = cipher.random_iv
|
88
|
+
cipher_bytes = cipher.update(clear_text) + cipher.final
|
89
|
+
|
90
|
+
# Combine IV and cipher bytes, and base-64 encode
|
91
|
+
base64_encode([iv + cipher_bytes])
|
92
|
+
end
|
93
|
+
module_function :encipher
|
94
|
+
|
95
|
+
|
96
|
+
# Decrypt the supplied +cipher_text+, using +key_text+ as the pass-phrase.
|
97
|
+
#
|
98
|
+
# @param key_text [String] The clear-text pass-phrase to use as the key
|
99
|
+
# for decryption.
|
100
|
+
# @param cipher_text [String] A base-64 encoded cipher text string that
|
101
|
+
# is to be decrypted.
|
102
|
+
# @param salt [Array<Byte>] An 8-byte array of random values to use as
|
103
|
+
# the salt. Like the +key_text+, it is imperative that the same value
|
104
|
+
# is used for the salt when decrypting a previously encrypted value.
|
105
|
+
# @return [String] The clear text that was encrypted.
|
106
|
+
def decrypt(key_text, cipher_text, salt = SALT)
|
107
|
+
key = generate_key(key_text, salt)
|
108
|
+
decipher(key, cipher_text)
|
109
|
+
end
|
110
|
+
module_function :decrypt
|
111
|
+
|
112
|
+
|
113
|
+
# Decipher the supplited +cipher_text+, using +key+ as the decipher key.
|
114
|
+
#
|
115
|
+
# @param key [String] The key used for encryption.
|
116
|
+
# @param cipher_text [String] A base-64 encoded cipher text string that
|
117
|
+
# is to be decrypted.
|
118
|
+
# @param cipher_algorithm [String] The name of the cipher algorithm used
|
119
|
+
# to encrypt the clear_text.
|
120
|
+
# @return [String] The clear text that was encrypted.
|
121
|
+
def decipher(key, cipher_text, cipher_algorithm = CIPHER_ALGORITHM)
|
122
|
+
cipher = OpenSSL::Cipher.new(cipher_algorithm)
|
123
|
+
cipher.decrypt
|
124
|
+
cipher.key = key
|
125
|
+
|
126
|
+
# Base-64 decode, and unpack IV and cipher from cipher_text
|
127
|
+
iv_cipher = base64_decode(cipher_text)
|
128
|
+
iv_len = KEY_DERIVED_LENGTH / 8
|
129
|
+
cipher.iv = iv_cipher[0...iv_len]
|
130
|
+
cipher_bytes = iv_cipher[iv_len..-1]
|
131
|
+
cipher.update(cipher_bytes) + cipher.final
|
132
|
+
end
|
133
|
+
module_function :decipher
|
134
|
+
|
135
|
+
|
136
|
+
# Convert a byte array to a base-64 encoded string representation.
|
137
|
+
#
|
138
|
+
# @param bytes [String, Array<String>] A String or Java byte-array to be
|
139
|
+
# encoded.
|
140
|
+
# @return [String] A base-64 encoded String.
|
141
|
+
def base64_encode(bytes)
|
142
|
+
bytes = [bytes] if bytes.is_a?(String)
|
143
|
+
bytes.pack('m0').chomp
|
144
|
+
end
|
145
|
+
module_function :base64_encode
|
146
|
+
|
147
|
+
|
148
|
+
# Convert a base-64 encoded string to a byte array.
|
149
|
+
#
|
150
|
+
# @param str [String] A base-64 encoded String.
|
151
|
+
# @return [Array<byte>] A String containing the decoded bytes.
|
152
|
+
def base64_decode(str)
|
153
|
+
str.unpack('m0').first
|
154
|
+
end
|
155
|
+
module_function :base64_decode
|
156
|
+
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# Generates the key to be used for encryption/decryption, based on
|
161
|
+
# +key_text+ and +salt+.
|
162
|
+
#
|
163
|
+
# @param key_text [String] A clear-text password or pass phrase that is
|
164
|
+
# to be used to derive the encryption key.
|
165
|
+
# @param salt [String] A String containing a random salt.
|
166
|
+
# @return [SecretKey] A key that can be used for encryption/decryption.
|
167
|
+
def generate_key(key_text, salt)
|
168
|
+
OpenSSL::PKCS5.send(KEY_ALGORITHM, key_text, salt,
|
169
|
+
KEY_ITERATION_COUNT, KEY_DERIVED_LENGTH)
|
170
|
+
end
|
171
|
+
module_function :generate_key
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|