rhubarbcipher 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rhubarbcipher +576 -0
  3. metadata +96 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e53bc9ca6e48f5794371b6a500e9272978f0faf2db3bf1e6a88fdf053581dc4
4
+ data.tar.gz: 65f99f497e447d4d7fa54db0bee22543a804365407bebd28c09065a5ca341d2c
5
+ SHA512:
6
+ metadata.gz: 63ad6dfa4fe046dcddd3576fa58f22c25da2b9a07fc9fc601141e14a33c10f4a3adae8eb1ab4dd46574ede774d886a93fa4ccb8217fcc9bdcaa82865099bb87b
7
+ data.tar.gz: 62ebd14d902bb761e5ab8366c50a818dd19f06bb8f57732915fcd8841bb7d02c5c03c2f476eb68676e03840eff9d7bd9e8a63e6f2864857b046bb8398af34820
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Title: RHUBARBCIPHER
4
+ # Version: 0.2.4
5
+ # Description: A plausibly deniable multi-key encryption/decryption system for GNU/Linux and BSD.
6
+ #
7
+ # WARNING: Please be aware that this gem has not undergone any form of independent security evaluation.
8
+ #
9
+ # Copyright (C) 2020 Peter Bruce Funnell
10
+ #
11
+ # This program is free software: you can redistribute it and/or modify it under the terms of the GNU
12
+ # General Public License as published by the Free Software Foundation, either version 3 of the License,
13
+ # or (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
16
+ # the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17
+ # License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with this program. If not,
20
+ # see <https://www.gnu.org/licenses/>.
21
+
22
+ # Load the necessary gems
23
+ require "cloversplitter"
24
+ require "securerandom"
25
+ require "optparse"
26
+ require "fileutils"
27
+ require "base64"
28
+ require "zlib"
29
+ require "xorcist"
30
+
31
+ # Define a class for storing core functionality of RHUBARBCIPHER
32
+ class RhubarbCipherCore
33
+ # Store program information in class variables.
34
+ @@program_name = "RHUBARBCIPHER"
35
+ @@program_version = "0.2.4"
36
+ @@executable_name = "rhubarbcipher"
37
+
38
+ # Define chunk size as 500 KiB in bits (used to enforce size similarity between real data and decoy data for the encryption process).
39
+ # NOTE: @@chunk_size must be a multiple of 8.
40
+ @@chunk_size = 500*(2**10)*8
41
+
42
+ # Define parameters for key splitting system.
43
+ @@keysplit_minimum_shares = 5
44
+ @@keysplit_total_shares = 10
45
+ @@keysplit_prime = (2**3217)-1
46
+
47
+ # Define sleep time.
48
+ @@sleep_time = 0.0001
49
+
50
+ # Set a threshold for large file detection (15000 KiB in bits).
51
+ @@large_file_threshold = 15000*(2**10)*8
52
+ @@large_file_threshold_string = "15000KiB"
53
+
54
+ def self.ask(prompt)
55
+ # Loop until the user provides valid input.
56
+ while true
57
+ # Print the prompt to the screen (without a newline character).
58
+ print("#{prompt} [Y/n]: ")
59
+
60
+ # Get user input.
61
+ confirmation = STDIN.gets().chomp()
62
+
63
+ # Parse user input and return either true or false accordingly.
64
+ if confirmation == "Y"
65
+ return true
66
+ elsif ["n", "N"].include?(confirmation)
67
+ return false
68
+ end
69
+ end
70
+ end
71
+
72
+ def self.puts_time(string)
73
+ # Prepend the string with a timestamp before printing it to the screen.
74
+ puts("#{Time.now}: #{string}")
75
+ end
76
+
77
+ def self.calculate_chunk_count(data)
78
+ # Calculate the chunk count for a given piece of data. This method is used for master-key size calculation during the encryption process.
79
+ return (data.length.to_i*8/@@chunk_size)+1
80
+ end
81
+
82
+ def self.encrypt(real_data, decoy_data=nil, output_directory)
83
+ # Provide size estimate and ask for confirmation.
84
+ puts()
85
+ chunk_count = self.calculate_chunk_count(real_data)
86
+ if decoy_data
87
+ n = 20
88
+ else
89
+ n = 10
90
+ end
91
+ size_estimate = (((((@@chunk_size*chunk_count)/2048)*(@@keysplit_prime.to_s.length+7))*(n)+((@@chunk_size/8)*chunk_count)).to_f/1000000.0).ceil
92
+ confirmation = self.ask("The estimated combined size of all output before compression and tagging is #{size_estimate}MB. Please ensure that the amount of storage space and RAM you have available far exceeds this amount. Are you sure you want to continue?")
93
+ if not confirmation
94
+ puts()
95
+ exit()
96
+ end
97
+
98
+ # Let the user know that the encryption process has started.
99
+ puts()
100
+ self.puts_time("Encryption process started. This could take a long time...")
101
+
102
+ # Generate master-key alpha.
103
+ self.puts_time("Generating master-key alpha...")
104
+ mkey_alpha = String.new()
105
+ mkey_alpha = SecureRandom.random_bytes((@@chunk_size/8)*chunk_count)
106
+ self.puts_time("Master-key alpha generated!")
107
+
108
+ # Use master-key alpha to generate encrypted data.
109
+ self.puts_time("Using master-key alpha to encrypt data...")
110
+ real_data_encrypted = Xorcist.xor(real_data, mkey_alpha)
111
+ self.puts_time("Encryption of data with master-key alpha complete!")
112
+
113
+ # Append random bytes to encrypted data such that it is the same length as both master-keys.
114
+ self.puts_time("Padding random bytes to encrypted data...")
115
+ real_data_encrypted << SecureRandom.random_bytes(((@@chunk_size/8)*chunk_count)-real_data.length)
116
+ self.puts_time("Padding complete!")
117
+
118
+ # Generate master-key beta if a decoy file was specified.
119
+ if decoy_data
120
+ # Generate the first section of master-key beta by applying XOR-ing every byte of decoy_data with its corresponding byte in real_data_encrypted.
121
+ self.puts_time("Generating main portion of master-key beta from encrypted data and decoy data...")
122
+ mkey_beta = Xorcist.xor(decoy_data, real_data_encrypted)
123
+
124
+ # Append random bytes to encrypted data such that it is the same length as master-key alpha and the encrypted data.
125
+ self.puts_time("Appending random bytes to master-key beta...")
126
+ mkey_beta << SecureRandom.random_bytes(((@@chunk_size/8)*chunk_count)-decoy_data.length)
127
+ self.puts_time("Master-key beta generated!")
128
+ end
129
+
130
+ # Prepend a tag to each master-key containing version information and the length of the corresponding data in bytes.
131
+ self.puts_time("Tagging master-key alpha with version and data length...")
132
+ mkey_alpha = "[RC:#{@@program_version}:#{real_data.length}]".force_encoding("ASCII-8BIT")+mkey_alpha
133
+ self.puts_time("Master-key alpha tagged!")
134
+ if decoy_data
135
+ self.puts_time("Tagging master-key beta with version and data length...")
136
+ mkey_beta = "[RC:#{@@program_version}:#{decoy_data.length}]".force_encoding("ASCII-8BIT")+mkey_beta
137
+ self.puts_time("Master-key beta tagged!")
138
+ end
139
+
140
+ # Split master-key alpha into 2000 separate 256B (2048-bit) shares using CLOVERSPLITTER.[
141
+ self.puts_time("Splitting master-key alpha into multiple keys...")
142
+ mkey_alpha_shares = Array.new()
143
+ @@keysplit_total_shares.times do
144
+ mkey_alpha_shares << String.new()
145
+ end
146
+ (0..((@@chunk_size*chunk_count)/2048)-1).each.with_index do |i, j|
147
+ part = mkey_alpha[j*256, 256]
148
+ subshares = CloverSplitter.generate_shares(part, minimum=@@keysplit_minimum_shares, shares=@@keysplit_total_shares, prime=@@keysplit_prime)
149
+ sleep(@@sleep_time)
150
+ subshares.each.with_index do |u, v|
151
+ subshare_encoded = "<[#{u[0]}]#{u[1].to_s}>\n"
152
+ mkey_alpha_shares[v] << subshare_encoded
153
+ sleep(@@sleep_time)
154
+ end
155
+ end
156
+ self.puts_time("Master-key alpha splitting process complete!")
157
+
158
+ # If a decoy file was specified, also split master-key beta into 2000 separate 256B (2048-bit) shares.
159
+ if decoy_data
160
+ self.puts_time("Splitting master-key beta into multiple keys...")
161
+ mkey_beta_shares = Array.new()
162
+ @@keysplit_total_shares.times do
163
+ mkey_beta_shares << String.new()
164
+ end
165
+ (0..((@@chunk_size*chunk_count)/2048)-1).each.with_index do |i, j|
166
+ part = mkey_beta[j*256, 256]
167
+ subshares = CloverSplitter.generate_shares(part, minimum=@@keysplit_minimum_shares, shares=@@keysplit_total_shares, prime=@@keysplit_prime)
168
+ sleep(@@sleep_time)
169
+ subshares.each.with_index do |u, v|
170
+ subshare_encoded = "<[#{u[0]}]#{u[1].to_s}>\n"
171
+ mkey_beta_shares[v] << subshare_encoded
172
+ sleep(@@sleep_time)
173
+ end
174
+ end
175
+ self.puts_time("Master-key beta splitting process complete!")
176
+ end
177
+
178
+ # Create timestamp for filename generation.
179
+ timestamp = (Time.now.to_f*1000).to_i
180
+
181
+ # Create version tag.
182
+ version_tag = "[RC:#{@@program_version}]".force_encoding("ASCII-8BIT")
183
+
184
+ # Deflate encrypted data with zlib, tag with @@program_version and save the result to output directory.
185
+ path = output_directory+"encrypted_#{timestamp}"
186
+ self.puts_time("Compressing, tagging and saving encrypted data to '#{path}'...")
187
+ File.open(path, "wb") do |file|
188
+ deflated_data = Zlib::Deflate.deflate(real_data_encrypted)
189
+ tagged_data = version_tag+deflated_data
190
+ file.write(tagged_data)
191
+ end
192
+ self.puts_time("Encrypted data saved!")
193
+
194
+ # Deflate alpha keys with zlib, tag with @@program_version and save the results to output directory.
195
+ self.puts_time("Compressing, tagging and saving real keys (derived from master-key alpha):")
196
+ mkey_alpha_shares.each.with_index do |k, n|
197
+ index = "%0#{@@keysplit_total_shares.to_s.length}d" % (n+1)
198
+ path = output_directory+"real_key_#{index}_#{timestamp}"
199
+ self.puts_time("Saving '#{path}'...")
200
+ File.open(path, "wb") do |file|
201
+ deflated_data = Zlib::Deflate.deflate(k[0..-2]) # The [0..-2] part removes the newline character present at the end of k.
202
+ tagged_data = version_tag+deflated_data
203
+ file.write(tagged_data)
204
+ end
205
+ end
206
+ self.puts_time("Real keys saved!")
207
+
208
+ # If a decoy file was specified, deflate beta keys with zlib, tag with @@program_version and save the results to output directory.
209
+ if decoy_data
210
+ self.puts_time("Compressing, tagging and saving decoy keys (derived from master-key beta):")
211
+ mkey_beta_shares.each.with_index do |k, n|
212
+ index = "%0#{@@keysplit_total_shares.to_s.length}d" % (n+1)
213
+ path = output_directory+"decoy_key_#{index}_#{timestamp}"
214
+ self.puts_time("Saving '#{path}'...")
215
+ File.open(path, "wb") do |file|
216
+ deflated_data = Zlib::Deflate.deflate(k[0..-2]) # The [0..-2] part removes the newline character present at the end of k.
217
+ tagged_data = version_tag+deflated_data
218
+ file.write(tagged_data)
219
+ end
220
+ end
221
+ self.puts_time("Decoy keys saved!")
222
+ end
223
+
224
+ # Let the user know that encryption was successful.
225
+ self.puts_time("Encryption process complete! If you used decoy data, please remember to rename all key files such that adversaries do not become aware of that fact.\n\n")
226
+ exit()
227
+ end
228
+
229
+ def self.version_comparison(version_string)
230
+ # Compares the current version with the version described by version_string to determine compatibility. True means compatible; false means incompatible.
231
+ version_split = version_string.force_encoding("ASCII-8BIT").split(".")
232
+ current_version_split = @@program_version.force_encoding("ASCII-8BIT").split(".")
233
+ if (version_split[0].to_i < current_version_split[0].to_i)
234
+ return true
235
+ elsif (version_split[0].to_i > current_version_split[0].to_i)
236
+ return false
237
+ elsif (version_split[1].to_i < current_version_split[1].to_i)
238
+ return true
239
+ elsif (version_split[1].to_i > current_version_split[1].to_i)
240
+ return false
241
+ elsif (version_split[2].to_i <= current_version_split[2].to_i)
242
+ return true
243
+ else
244
+ return false
245
+ end
246
+ end
247
+
248
+ def self.decrypt(data, key_list, output_directory)
249
+ # Let the user know that the encryption process has started.
250
+ puts()
251
+ self.puts_time("Decryption process started. This could take a long time...")
252
+
253
+ # Untag all keys in key_list.
254
+ self.puts_time("Untagging keys...")
255
+ untagged_key_list = Array.new()
256
+ key_list.each do |k|
257
+ version_string = k[/\[RC:.*?\]/].split(":")[1][0..-2]
258
+ if self.version_comparison(version_string)
259
+ k[/\[RC:.*?\]/] = String.new()
260
+ untagged_key_list << [version_string, k]
261
+ else
262
+ self.puts_time("It would appear that the file specified for decryption was made with #{@@program_name} #{version_string}, which is incompatible with this version of #{@@program_name} (#{@@program_version}). Updating to the latest version of #{@@program_name} may fix this issue.\n\n")
263
+ exit()
264
+ end
265
+ end
266
+ self.puts_time("Keys untagged!")
267
+
268
+ # Untag and inflate data.
269
+ begin
270
+ self.puts_time("Untagging and decompressing data...")
271
+ data[/\[RC:.*?\]/] = String.new()
272
+ data = Zlib::Inflate.inflate(data)
273
+ self.puts_time("Data untagged and decompressed!")
274
+ rescue
275
+ self.puts_time("Something went wrong whilst untagging and decompressing the data.\n\n")
276
+ exit()
277
+ end
278
+
279
+ # Inflate all keys in untagged_key_list with zlib.
280
+ self.puts_time("Decompressing keys...")
281
+ inflated_key_list = Array.new()
282
+ untagged_key_list.each do |k|
283
+ inflated_key_list << [k[0], Zlib::Inflate.inflate(k[1])]
284
+ sleep(@@sleep_time)
285
+ end
286
+ self.puts_time("Keys decompressed!")
287
+
288
+ # Parse keys.
289
+ self.puts_time("Parsing keys...")
290
+ mkey_shares = Array.new()
291
+ inflated_key_list.each do |k|
292
+ k_parts = k[1].split("\n")
293
+ version_split = k[0].split(".")
294
+ base64_shares = (version_split[0].to_i <= 0 and version_split[1].to_i <= 2 and version_split[2].to_i <= 3) # For keys generated by versions <= 0.2.3, shares were encoded in base-64.
295
+ mkey_part_shares = Array.new()
296
+ k_parts.each do |p|
297
+ p_extracted = p[1..-2]
298
+ share_i = p_extracted[/\[.*?\]/][1..-2].to_i
299
+ p_extracted[/\[.*?\]/] = String.new()
300
+ if base64_shares
301
+ share_j = Base64.strict_decode64(p_extracted).to_i
302
+ else
303
+ share_j = p_extracted.to_i
304
+ end
305
+ mkey_part_shares << [share_i, share_j]
306
+ sleep(@@sleep_time)
307
+ end
308
+ mkey_shares << mkey_part_shares
309
+ sleep(@@sleep_time)
310
+ end
311
+ self.puts_time("Keys parsed!")
312
+
313
+ # Calculate chunk count.
314
+ self.puts_time("Calculating chunk count...")
315
+ chunk_count = mkey_shares[0].length
316
+ self.puts_time("Chunk count calculated!")
317
+
318
+ # Attempt recovery of tagged master-key.
319
+ self.puts_time("Attempting master-key recovery...")
320
+ mkey_tagged = String.new()
321
+ (0..chunk_count-1).each do |i|
322
+ chunk_shares = Array.new()
323
+ mkey_shares.each do |j|
324
+ chunk_shares << j[i]
325
+ end
326
+ begin
327
+ recovered_chunk = CloverSplitter.recover_secret(chunk_shares, prime=@@keysplit_prime)
328
+ sleep(@@sleep_time)
329
+ if recovered_chunk.length != 256
330
+ self.puts_time("Something went wrong when attempting to recover the master-key from the provided key list, leading to an invalid chunk length.\n\n")
331
+ exit()
332
+ end
333
+ mkey_tagged << recovered_chunk
334
+ rescue
335
+ self.puts_time("Something went wrong when attempting to recover the master-key from the provided key list.\n\n")
336
+ exit()
337
+ end
338
+ end
339
+
340
+ # Attempt to untag master-key.
341
+ self.puts_time("Untagging master-key...")
342
+ if mkey_tagged[/\[RC:.*?\]/]
343
+ tag = mkey_tagged[/\[RC:.*?\]/]
344
+ mkey_tagged[/\[RC:.*?\]/] = String.new()
345
+ tag_array = tag[1..-2].split(":")
346
+ if not self.version_comparison(tag_array[1])
347
+ self.puts_time("It would appear that the master-key recovered from the provided key list was generated using #{@@program_name} #{version_string}, which is incompatible with this version of #{@@program_name} (#{@@program_version}). Updating to the latest version of #{@@program_version} may fix this issue.\n\n")
348
+ end
349
+ decrypted_data_size = tag_array[-1].to_i
350
+ mkey = mkey_tagged
351
+ else
352
+ # Recovery seems to have failed.
353
+ self.puts_time("Something went wrong when attempting to recover the master-key from the provided key list, resulting in a malformed tag.\n\n")
354
+ exit()
355
+ end
356
+ self.puts_time("Master-key recovered!")
357
+
358
+ # Attempt to decrypt specified data using recovered master-key.
359
+ self.puts_time("Attempting to decrypt data using recovered master-key...")
360
+ decrypted_data = Xorcist.xor(data, mkey)[0..decrypted_data_size-1]
361
+ self.puts_time("Data decrypted!")
362
+
363
+ # Create timestamp for filename generation.
364
+ timestamp = (Time.now.to_f*1000).to_i
365
+
366
+ # Save decrypted data to output directory.
367
+ path = output_directory+"decrypted_#{timestamp}"
368
+ self.puts_time("Saving decrypted data to '#{path}'...")
369
+ File.open(path, "wb") do |file|
370
+ file.write(decrypted_data)
371
+ end
372
+ self.puts_time("Decrypted data saved!")
373
+
374
+ # Let the user know that decryption was successful.
375
+ puts("\nDecryption process complete.")
376
+ exit()
377
+ end
378
+
379
+ def self.start()
380
+ # Print program information to the screen.
381
+ puts("#{@@program_name} #{@@program_version}")
382
+ puts("\nCopyright (C) 2020 Peter Bruce Funnell")
383
+ puts("\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.")
384
+ puts("\nBTC Donation Address (Author): 3EdoXV1w8H7y7M9ZdpjRC7GPnX4aouy18g")
385
+
386
+ # Ensure that the current system is either GNU/Linux or BSD.
387
+ if not (RUBY_PLATFORM.include?("linux") or RUBY_PLATFORM.include?("bsd"))
388
+ puts("#{@@program_name} is intended for GNU/Linux and BSD operating systems only. Exiting...\n\n")
389
+ exit()
390
+ end
391
+
392
+ # Initialise command line argument parser.
393
+ option_parser = OptionParser.new do |options|
394
+ options.banner = "\nUsage: #{@@executable_name} [OPTIONS]\n\n"
395
+ options.on("-h", "--help", "Display help text and exit.\n\n")
396
+ options.on("-v", "--version", "Display version information and exit.\n\n")
397
+ options.on("-e FILE", "--encrypt FILE", "Encrypt the specified file. An output directory must be specified with '-o' or '--output'.\n\n")
398
+ options.on("-d FILE", "--decrypt FILE", "Decrypt the specified file. An output directory must be specified with '-o' or '--output'.\n\n")
399
+ options.on("-D FILE", "--decoy FILE", "Specify a decoy file for plausibly deniable encryption.\n\n")
400
+ options.on("-k KEYS", "--keys KEYS", "Specify a comma-separated list of keys.\n\n")
401
+ options.on("-o DIR", "--output DIR", "Specify an output directory. If the directory already exists, files may be overwritten.\n\n")
402
+ end
403
+
404
+ # Attempt to parse command line arguments.
405
+ begin
406
+ if ARGV.length < 1
407
+ # No command line arguments were detected; there is no reason to continue, so exit.
408
+ puts("\nNo command line arguments were detected. If you would like to view the help text, please execute #{@@program_name} with the -h command line argument.\n\n")
409
+ exit()
410
+ else
411
+ # Command line arguments were detected, so parse those arguments and store the result in a new hash.
412
+ arguments = Hash.new()
413
+ option_parser.parse!(into: arguments)
414
+ end
415
+
416
+ if arguments[:help]
417
+ # Display help text and exit.
418
+ puts(option_parser)
419
+ exit()
420
+ elsif arguments[:version]
421
+ # Version information has already been printed, so exit the program.
422
+ exit()
423
+ elsif arguments[:encrypt] and arguments[:decrypt]
424
+ # The user is trying to encrypt and decrypt at the same time. Do not permit such behaviour.
425
+ puts("\nPlease use either '-e'/'--encrypt' or '-d'/'--decrypt', not both.\n\n")
426
+ exit()
427
+ elsif not (arguments[:encrypt] or arguments[:decrypt])
428
+ # The user is neither encrypting nor decrypting; kindly let them know that they must pick a side.
429
+ puts("\nA run mode must be specified with either '-e'/'--encrypt' or '-d'/'--decrypt'.\n\n")
430
+ exit()
431
+ elsif (arguments[:encrypt] or arguments[:decrypt]) and not (arguments[:output])
432
+ # The user is trying to encrypt or decrypt without specifying an output directory; kindly let them know that one must be specified.
433
+ puts("\nAn output directory must be specified with '-o'/'--output' in order to encrypt or decrypt data.\n\n")
434
+ exit()
435
+ elsif arguments[:decrypt] and not arguments[:keys]
436
+ # The user is trying to decrypt without specifying a list of keys; kindly let them know that keys are required in order to decrypt data.
437
+ puts("\nA comma-separated list of keys must be specified with '-k'/'--keys' in order to decrypt data.\n\n")
438
+ exit()
439
+ end
440
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
441
+ # Invalid command line arguments were detected. Say so, display the help text, and exit.
442
+ puts("\nOne or more command line arguments were invalid.\n")
443
+ puts(option_parser)
444
+ exit()
445
+ end
446
+
447
+ # Create the output directory if it doesn't already exist.
448
+ begin
449
+ output_directory = arguments[:output]
450
+ FileUtils.mkdir_p(output_directory) unless File.exist?(output_directory)
451
+
452
+ # Append a slash to output_directory if it does not already end in one.
453
+ if output_directory[-1] != "/"
454
+ output_directory += "/"
455
+ end
456
+ rescue
457
+ puts("\nThe directory specified for output did not exist and could not be created by the current user. Please attempt manual creation of the specified directory before re-attempting.\n\n")
458
+ exit()
459
+ end
460
+
461
+ # Check that the output directory is actually a directory.
462
+ if not File.directory?(output_directory)
463
+ puts("\nThe directory specified for output appears to be invalid. Please confirm that the path points to a directory and not a file.\n\n")
464
+ exit()
465
+ end
466
+
467
+ # Check that the output directory is writable.
468
+ if not File.writable?(output_directory)
469
+ puts("\nThe current user does not have write permissions for the specified output directory.\n\n")
470
+ exit()
471
+ end
472
+
473
+ # Check that the output directory is empty. If it is not, ask for user confirmation before continuing.
474
+ if not Dir.empty?(output_directory)
475
+ print("\n")
476
+ confirmation = self.ask("The directory specified for output already contains one or more files. Files will be overwritten in the event of a filename clash. Are you sure you want to continue?")
477
+ if not confirmation
478
+ puts()
479
+ exit()
480
+ end
481
+ end
482
+
483
+ # Encrypt or decrypt, depending on run mode.
484
+ if arguments[:encrypt]
485
+ # Load real data from specified file.
486
+ begin
487
+ # Check file size of real data in bits and ask for confirmation if over @@large_file_threshold.
488
+ if File.size(arguments[:encrypt])*8 > @@large_file_threshold
489
+ print("\n")
490
+ confirmation = self.ask("The file you specified for encryption is over #{@@large_file_threshold_string}. Large files may take a very long time to encrypt/decrypt using #{@@program_name}. Are you sure you wish to continue?")
491
+ if not confirmation
492
+ puts()
493
+ exit()
494
+ end
495
+ end
496
+
497
+ real_data = File.read(arguments[:encrypt], mode:"rb")
498
+ rescue
499
+ puts("\nThe file specified for encryption could not be loaded. Please confirm that the file exists and is readable by the current user.\n\n")
500
+ exit()
501
+ end
502
+
503
+ # Load decoy data if specified.
504
+ if arguments[:decoy]
505
+ begin
506
+ decoy_data = File.read(arguments[:decoy], mode:"rb")
507
+ rescue
508
+ puts("\nThe specified decoy file could not be loaded. Please confirm that the file exists and is readable by the current user.\n\n")
509
+ exit()
510
+ end
511
+
512
+ # Calculate chunk count for real data and decoy data for size similarity enforcement.
513
+ real_data_chunk_count = self.calculate_chunk_count(real_data)
514
+ decoy_data_chunk_count = self.calculate_chunk_count(decoy_data)
515
+
516
+ # Compare chunk count of decoy data with that of real data and enforce size similarity.
517
+ if real_data_chunk_count != decoy_data_chunk_count
518
+ puts("\nViolation of size similarity rule detected when comparing size of decoy file with size of file specified for encryption.\n\nFor security reasons, the size of the decoy file must be similar to that of the file specified for encryption. Specifically, if 'A' is the size of the real file in kibibytes (KiB) and 'B' is the size of the decoy file in kibibytes (KiB), the condition 'A\\500 = B\\500' must be satisfied, where '\\' is the integer division operator defined as 'A\\B ≡ ⌊A/B⌋' (alternatively defined as a quotient without the remainder).\n\nThe size similarity rule is strictly enforced as a countermeasure against adversaries determining whether or not decoy data was used during encryption under certain post-decryption circumstances. If this rule were not enforced, an adversary would be able to easily know whether or not a decoy file was used during the encryption process by examining file sizes.\n\n")
519
+ exit()
520
+ end
521
+ else
522
+ # Set decoy_data to nil if no decoy file was specified.
523
+ decoy_data = nil
524
+ end
525
+
526
+ # Pass the real data, decoy data and output directory to the decrypt method.
527
+ begin
528
+ self.encrypt(real_data, decoy_data, output_directory)
529
+ rescue Interrupt
530
+ puts("\nSIGINT received. Exiting...\n\n")
531
+ exit()
532
+ end
533
+ elsif arguments[:decrypt]
534
+ begin
535
+ # Check file size of real data in bits and ask for confirmation if over @@large_file_threshold.
536
+ if File.size(arguments[:decrypt])*8 > @@large_file_threshold
537
+ print("\n")
538
+ confirmation = self.ask("The file you specified for decryption is over 15000KiB. It may take a very long time to decrypt. Are you sure you wish to continue?")
539
+ if not confirmation
540
+ puts()
541
+ exit()
542
+ end
543
+ end
544
+
545
+ # Read data from the file specified for decryption.
546
+ data = File.read(arguments[:decrypt], mode:"rb")
547
+ rescue
548
+ puts("\nSomething went wrong when trying to read the file specified for decryption. Please confirm that the path is valid.\n\n")
549
+ end
550
+
551
+ begin
552
+ # Create an array from the comma-separated list of key files.
553
+ key_file_list = arguments[:keys].split(",")
554
+
555
+ # Read each key file to create a list of keys.
556
+ key_list = Array.new()
557
+ key_file_list.each do |file|
558
+ key_list << File.read(file, mode:"rb")
559
+ end
560
+ rescue
561
+ puts("\nSomething went wrong when trying to read the specified keys. Please confirm that all paths in the comma-separated list are valid.\n\n")
562
+ end
563
+
564
+ # Pass the data, list of keys and output directory to the decrypt method.
565
+ begin
566
+ self.decrypt(data, key_list, output_directory)
567
+ rescue Interrupt
568
+ puts("\nSIGINT received. Exiting...\n\n")
569
+ exit()
570
+ end
571
+ end
572
+ end
573
+ end
574
+
575
+ # Start RHUBARBCIPHER.
576
+ RhubarbCipherCore.start()
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rhubarbcipher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ platform: ruby
6
+ authors:
7
+ - Peter Funnell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cloversplitter
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: xorcist
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 1.1.2
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 1.1.2
53
+ description: 'RHUBARBCIPHER is a plausibly deniable multi-key file encryption/decryption
54
+ system for GNU/Linux and BSD that combines one-time pad encryption/decryption with
55
+ Shamir''s Secret Sharing in an attempt to encrypt files in a versatile yet information-theoretically
56
+ secure manner. RHUBARBCIPHER is only recommended for smaller files (e.g. less than
57
+ 15000KiB) due to the time taken to encrypt/decrypt data, which increases as a function
58
+ of file size. It includes an optional decoy feature which allows users to specify
59
+ a decoy file and generate a set of decoy keys in addition to the real keys. Size
60
+ similarity between the decoy file and the real file is strictly enforced. WARNING:
61
+ Please be aware that this gem has not undergone any form of independent security
62
+ evaluation.'
63
+ email: hello@octetsplicer.com
64
+ executables:
65
+ - rhubarbcipher
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - bin/rhubarbcipher
70
+ homepage: https://github.com/octetsplicer/RHUBARBCIPHER
71
+ licenses:
72
+ - GPL-3.0+
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 2.5.5
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements:
89
+ - A GNU/Linux or BSD operating system.
90
+ rubyforge_project:
91
+ rubygems_version: 2.7.6.2
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A plausibly deniable multi-key encryption/decryption system for GNU/Linux
95
+ and BSD.
96
+ test_files: []