rhubarbcipher 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/rhubarbcipher +576 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/rhubarbcipher
ADDED
@@ -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: []
|