batch-kit 0.3
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 +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
|