block_cipher_kit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +37 -0
  3. data/.gitignore +57 -0
  4. data/.standard.yml +1 -0
  5. data/Gemfile +3 -0
  6. data/README.md +136 -0
  7. data/Rakefile +18 -0
  8. data/block_cipher_kit.gemspec +37 -0
  9. data/lib/block_cipher_kit/aes_256_cbc_scheme.rb +56 -0
  10. data/lib/block_cipher_kit/aes_256_cfb_civ_scheme.rb +35 -0
  11. data/lib/block_cipher_kit/aes_256_cfb_scheme.rb +37 -0
  12. data/lib/block_cipher_kit/aes_256_ctr_scheme.rb +87 -0
  13. data/lib/block_cipher_kit/aes_256_gcm_scheme.rb +108 -0
  14. data/lib/block_cipher_kit/base_scheme.rb +43 -0
  15. data/lib/block_cipher_kit/block_writable.rb +20 -0
  16. data/lib/block_cipher_kit/cipher_io.rb +16 -0
  17. data/lib/block_cipher_kit/io_lens.rb +46 -0
  18. data/lib/block_cipher_kit/key_material.rb +16 -0
  19. data/lib/block_cipher_kit/passthru_scheme.rb +26 -0
  20. data/lib/block_cipher_kit/version.rb +3 -0
  21. data/lib/block_cipher_kit.rb +32 -0
  22. data/test/cipher_io_test.rb +18 -0
  23. data/test/io_lens_test.rb +30 -0
  24. data/test/key_material_test.rb +12 -0
  25. data/test/known_ciphertexts/AES256CBCScheme.ciphertext.bin +0 -0
  26. data/test/known_ciphertexts/AES256CFBCIVScheme.ciphertext.bin +0 -0
  27. data/test/known_ciphertexts/AES256CFBScheme.ciphertext.bin +0 -0
  28. data/test/known_ciphertexts/AES256CTRScheme.ciphertext.bin +0 -0
  29. data/test/known_ciphertexts/AES256GCMScheme.ciphertext.bin +0 -0
  30. data/test/known_ciphertexts/PassthruScheme.ciphertext.bin +0 -0
  31. data/test/known_ciphertexts/known_plain.bin +0 -0
  32. data/test/schemes_test.rb +221 -0
  33. data/test/test_helper.rb +6 -0
  34. data/test/test_known_ciphertext.rb +88 -0
  35. metadata +162 -0
@@ -0,0 +1,20 @@
1
+ # An adapter which allows a block that accepts chunks of
2
+ # written data to be used as an IO and passed to IO.copy_stream
3
+ class BlockCipherKit::BlockWritable
4
+ def initialize(io = nil, &blk)
5
+ if (!io && !blk) || (io && blk)
6
+ raise ArgumentError, "BlockWritable requires io or a block, but not both"
7
+ end
8
+ @io = io
9
+ @blk = blk
10
+ end
11
+
12
+ def write(string)
13
+ if string.bytesize.nonzero? && @io
14
+ @io.write(string.b)
15
+ elsif string.bytesize.nonzero? && @blk
16
+ @blk.call(string.b)
17
+ end
18
+ string.bytesize
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # Allows an OpenSSL::Cipher to be written through as if it were an IO. This
2
+ # allows the cipher to be passed to things like IO.copy_stream
3
+ class BlockCipherKit::CipherIO
4
+ def initialize(io, cipher)
5
+ @io = io
6
+ @cipher = cipher
7
+ end
8
+
9
+ def write(bytes)
10
+ @io.write(@cipher.update(bytes))
11
+ # We must return the amount of bytes of input
12
+ # we have accepted, not the amount of bytes
13
+ # of output we produced from the cipher
14
+ bytes.bytesize
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Allows you to pass through the writes of a particular byte range only, discarding the rest
4
+ class BlockCipherKit::IOLens
5
+ def initialize(io, range)
6
+ @io = io
7
+ @range = range
8
+ @pos = 0
9
+ end
10
+
11
+ def write(bytes)
12
+ previous_pos, @pos = @pos, @pos + bytes.bytesize
13
+ return 0 if bytes.bytesize == 0
14
+
15
+ location_in_output = Range.new(previous_pos, previous_pos + bytes.bytesize - 1)
16
+ overlap = intersection_of(@range, location_in_output)
17
+ if overlap
18
+ at = overlap.begin - previous_pos
19
+ n = overlap.end - overlap.begin + 1
20
+ @io.write(bytes.byteslice(at, n))
21
+ end
22
+
23
+ bytes.bytesize
24
+ end
25
+
26
+ private
27
+
28
+ # lifted from https://github.com/julik/range_utils/blob/master/lib/range_utils.rb
29
+ def intersection_of(range_a, range_b)
30
+ range_a = Range.new(range_a.begin, range_a.end.pred) if range_a.exclude_end?
31
+ range_b = Range.new(range_b.begin, range_b.end.pred) if range_b.exclude_end?
32
+
33
+ range_a = Range.new(0, range_a.end) if range_a.begin.nil?
34
+ range_b = Range.new(0, range_b.end) if range_b.begin.nil?
35
+
36
+ range_a = Range.new(range_a.begin, range_b.end) if range_a.end.nil?
37
+ range_b = Range.new(range_b.begin, range_a.end) if range_b.end.nil?
38
+
39
+ range_a, range_b = [range_a, range_b].sort_by(&:begin)
40
+ return if range_a.end < range_b.begin
41
+
42
+ heads_and_tails = [range_a.begin, range_b.begin, range_a.end, range_b.end].sort
43
+ middle = heads_and_tails[1..-2]
44
+ middle[0]..middle[1]
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ require "forwardable"
2
+
3
+ # Allows a string with key material (like IV and key)
4
+ # to be concealed when an object holding it gets printed or show via #inspect
5
+ class BlockCipherKit::KeyMaterial
6
+ extend Forwardable
7
+ def_delegators :@str, :b, :byteslice, :to_s, :to_str
8
+
9
+ def initialize(str)
10
+ @str = str
11
+ end
12
+
13
+ def inspect
14
+ "[SENSITIVE(#{@str.bytesize * 8} bits)]"
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ class BlockCipherKit::PassthruScheme < BlockCipherKit::BaseScheme
2
+ def initialize(...)
3
+ end
4
+
5
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk)
6
+ w = into_plaintext_io || BlockCipherKit::BlockWritable.new(&blk)
7
+ IO.copy_stream(from_ciphertext_io, w)
8
+ end
9
+
10
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk)
11
+ if from_plaintext_io && !blk
12
+ IO.copy_stream(from_plaintext_io, into_ciphertext_io)
13
+ elsif blk
14
+ blk.call(into_ciphertext_io)
15
+ else
16
+ raise ArgumentError
17
+ end
18
+ end
19
+
20
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk)
21
+ from_ciphertext_io.seek(from_ciphertext_io.pos + range.begin)
22
+ n_bytes = range.end - range.begin + 1
23
+ w = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
24
+ w.write(from_ciphertext_io.read(n_bytes))
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module BlockCipherKit
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,32 @@
1
+ begin
2
+ require "openssl"
3
+ rescue LoadError
4
+ message = <<~ERR
5
+
6
+ Unable to load "openssl". You may be running a version of Ruby where the "openssl"
7
+ library is not contained in the standard library, but must be installed as a gem.
8
+
9
+ We do not specify "openssl" as a dependency of block_cipher_kit because gems spun off
10
+ from the standard library can cause problems if they are specified as transitive dependencies,
11
+ especially on older Ruby versions.
12
+
13
+ Running `bundle add openssl` in your application will likely resolve the issue.
14
+ ERR
15
+ raise LoadError, message
16
+ end
17
+
18
+ require "securerandom"
19
+ module BlockCipherKit
20
+ autoload :IOLens, __dir__ + "/block_cipher_kit/io_lens.rb"
21
+ autoload :BlockWritable, __dir__ + "/block_cipher_kit/block_writable.rb"
22
+ autoload :CipherIO, __dir__ + "/block_cipher_kit/cipher_io.rb"
23
+ autoload :KeyMaterial, __dir__ + "/block_cipher_kit/key_material.rb"
24
+ autoload :BaseScheme, __dir__ + "/block_cipher_kit/base_scheme.rb"
25
+ autoload :PassthruScheme, __dir__ + "/block_cipher_kit/passthru_scheme.rb"
26
+ autoload :AES256CTRScheme, __dir__ + "/block_cipher_kit/aes_256_ctr_scheme.rb"
27
+ autoload :AES256CBCScheme, __dir__ + "/block_cipher_kit/aes_256_cbc_scheme.rb"
28
+ autoload :AES256GCMScheme, __dir__ + "/block_cipher_kit/aes_256_gcm_scheme.rb"
29
+ autoload :AES256CFBScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_scheme.rb"
30
+ autoload :AES256CFBCIVScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_civ_scheme.rb"
31
+ autoload :EncryptedDiskService, __dir__ + "/block_cipher_kit/encrypted_disk_service.rb"
32
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class CipherIOTest < Minitest::Test
6
+ class FakeCipher
7
+ def update(str)
8
+ str + "c"
9
+ end
10
+ end
11
+
12
+ def test_writes_through_the_cipher_and_returns_correct_data
13
+ out = StringIO.new
14
+ cipher_io = BlockCipherKit::CipherIO.new(out, FakeCipher.new)
15
+ assert_equal 2, cipher_io.write("ab")
16
+ assert_equal "abc", out.string
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class IOLensTest < Minitest::Test
6
+ def test_lens_writes
7
+ input = Random.bytes(48)
8
+ (1..input.bytesize).each do |write_size|
9
+ ranges = [
10
+ 0..0,
11
+ 0...1,
12
+ 1..1,
13
+ 1...2,
14
+ 43..120,
15
+ 14..,
16
+ ..14
17
+ ]
18
+ ranges.each do |test_range|
19
+ test_io = StringIO.new.binmode
20
+ readable = StringIO.new(input).binmode
21
+ lens = BlockCipherKit::IOLens.new(test_io, test_range)
22
+ while (chunk = readable.read(write_size))
23
+ lens.write(chunk)
24
+ end
25
+ assert_equal input[test_range].bytesize, test_io.size
26
+ assert_equal input[test_range], test_io.string
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class KeyMaterialTest < Minitest::Test
6
+ def test_conceals_but_provides_string_access
7
+ km = BlockCipherKit::KeyMaterial.new("foo")
8
+ assert_equal "[SENSITIVE(24 bits)]", km.inspect
9
+ assert_equal "foo", [km].join
10
+ assert_equal "foo".b, km.b
11
+ end
12
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class SchemesTest < Minitest::Test
6
+ SCHEME_NAMES = [
7
+ "BlockCipherKit::AES256CFBScheme",
8
+ "BlockCipherKit::AES256CFBCIVScheme",
9
+ "BlockCipherKit::AES256CTRScheme",
10
+ "BlockCipherKit::AES256GCMScheme",
11
+ "BlockCipherKit::AES256CBCScheme"
12
+ ]
13
+ SCHEME_NAMES_INCLUDING_PASSTHRU = SCHEME_NAMES + ["BlockCipherKit::PassthruScheme"]
14
+
15
+ SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
16
+ define_method "test_scheme #{scheme_class_name} encrypts and decrypts using both block and IO for input and output" do
17
+ assert_encrypts_from_block_and_io(scheme_class_name)
18
+ assert_decrypts_into_block_and_io(scheme_class_name)
19
+ end
20
+ end
21
+
22
+ SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
23
+ define_method "test_scheme #{scheme_class_name} encrypts and decrypts the entire message" do
24
+ assert_encrypts_and_decrypts_entire_message(scheme_class_name)
25
+ end
26
+ end
27
+
28
+ SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
29
+ define_method "test_scheme #{scheme_class_name} allows random access reads" do
30
+ assert_allows_random_access(scheme_class_name)
31
+ end
32
+ end
33
+
34
+ SCHEME_NAMES.each do |scheme_class_name|
35
+ define_method("test_scheme #{scheme_class_name} outputs different ciphertext depending on key") do
36
+ assert_key_changes_ciphertext(scheme_class_name)
37
+ end
38
+ end
39
+
40
+ SCHEME_NAMES.each do |scheme_class_name|
41
+ define_method "test_scheme #{scheme_class_name} fails to initialise with a key too small" do
42
+ tiny_key = Random.new.bytes(3)
43
+ assert_raises(ArgumentError) do
44
+ resolve(scheme_class_name).new(tiny_key)
45
+ end
46
+ end
47
+ end
48
+
49
+ SCHEME_NAMES.each do |scheme_class_name|
50
+ define_method "test_scheme #{scheme_class_name} does not expose key material in #inspect" do
51
+ big_key = "wonderful, absolutely incredible easiness of being, combined with unearthly pleasures"
52
+ inspectable = resolve(scheme_class_name).new(big_key).inspect
53
+ big_key.split(/\s/).each do |word|
54
+ refute inspectable.include?(word), "Output of #inspect must not reveal key material - was #{inspectable.inspect}"
55
+ end
56
+ end
57
+ end
58
+
59
+ def assert_key_changes_ciphertext(scheme_class_name)
60
+ rng = Random.new(Minitest.seed)
61
+ keys = 4.times.map { rng.bytes(64) }
62
+
63
+ n_bytes = rng.rand(129..2048)
64
+ plaintext = rng.bytes(n_bytes)
65
+
66
+ ciphertexts = keys.map do |k|
67
+ scheme = resolve(scheme_class_name).new(k)
68
+ encrypted_io = StringIO.new.binmode
69
+ scheme.streaming_encrypt(from_plaintext_io: StringIO.new(plaintext).binmode, into_ciphertext_io: encrypted_io)
70
+ encrypted_io.string
71
+ end
72
+
73
+ assert_equal ciphertexts.length, ciphertexts.uniq.length
74
+ end
75
+
76
+ def assert_encrypts_from_block_and_io(scheme_class_name)
77
+ rng = Random.new(Minitest.seed)
78
+ encryption_key = rng.bytes(64)
79
+
80
+ scheme = resolve(scheme_class_name).new(encryption_key)
81
+
82
+ # Generate a prime number of bytes, so that the plaintext does not
83
+ # subdivide into blocks. This will allow us to find situations where
84
+ # block offsets are not used for reading.
85
+ plaintext = rng.bytes(OpenSSL::BN.generate_prime(12))
86
+
87
+ out1 = StringIO.new.binmode
88
+ scheme.streaming_encrypt(into_ciphertext_io: out1) do |writable|
89
+ writable.write(plaintext.byteslice(0, 417))
90
+ writable.write(plaintext.byteslice(417, plaintext.bytesize))
91
+ end
92
+ assert out1.size > 0
93
+
94
+ out2 = StringIO.new.binmode
95
+ scheme.streaming_encrypt(from_plaintext_io: StringIO.new(plaintext), into_ciphertext_io: out2)
96
+ assert_equal out1.size, out2.size, "The size of the encrypted message must be the same when outputting via a block or a readable IO"
97
+
98
+ out1.rewind
99
+ out2.rewind
100
+
101
+ readback1 = StringIO.new.binmode.tap do |w|
102
+ scheme.streaming_decrypt(from_ciphertext_io: out1, into_plaintext_io: w)
103
+ end.string
104
+
105
+ readback2 = StringIO.new.binmode.tap do |w|
106
+ scheme.streaming_decrypt(from_ciphertext_io: out2, into_plaintext_io: w)
107
+ end.string
108
+
109
+ assert_equal plaintext, readback1
110
+ assert_equal plaintext, readback2
111
+ end
112
+
113
+ def assert_decrypts_into_block_and_io(scheme_class_name)
114
+ rng = Random.new(Minitest.seed)
115
+ encryption_key = rng.bytes(64)
116
+
117
+ scheme = resolve(scheme_class_name).new(encryption_key)
118
+
119
+ # Generate a prime number of bytes, so that the plaintext does not
120
+ # subdivide into blocks. This will allow us to find situations where
121
+ # block offsets are not used for reading.
122
+ plaintext = rng.bytes(OpenSSL::BN.generate_prime(12))
123
+ ciphertext_io = StringIO.new
124
+ scheme.streaming_encrypt(into_ciphertext_io: ciphertext_io, from_plaintext_io: StringIO.new(plaintext))
125
+
126
+ ciphertext_io.rewind
127
+ readback = StringIO.new.binmode
128
+ scheme.streaming_decrypt(from_ciphertext_io: ciphertext_io, into_plaintext_io: readback)
129
+ assert_equal readback.size, plaintext.bytesize
130
+ assert_equal readback.string[0..16], plaintext[0..16]
131
+ assert_equal readback.string[-4..], plaintext[-4..]
132
+
133
+ ciphertext_io.rewind
134
+ readback = StringIO.new.binmode
135
+ scheme.streaming_decrypt(from_ciphertext_io: ciphertext_io) do |chunk|
136
+ readback.write(chunk)
137
+ end
138
+ assert_equal readback.size, plaintext.bytesize
139
+ assert_equal readback.string[0..16], plaintext[0..16]
140
+ assert_equal readback.string[-4..], plaintext[-4..]
141
+ end
142
+
143
+ def assert_encrypts_and_decrypts_entire_message(scheme_class_name)
144
+ rng = Random.new(Minitest.seed)
145
+ random_encryption_key = rng.bytes(64)
146
+
147
+ enc = resolve(scheme_class_name).new(random_encryption_key)
148
+
149
+ # Generate a prime number of bytes, so that the plaintext does not
150
+ # subdivide into blocks. This will allow us to find situations where
151
+ # block offsets are not used for reading. A 24-bit prime we get is
152
+ # 14896667, which is just over 14 megabytes
153
+ amount_of_plain_bytes = OpenSSL::BN.generate_prime(24).to_i
154
+ plain_bytes = rng.bytes(amount_of_plain_bytes)
155
+
156
+ source_io = StringIO.new(plain_bytes)
157
+ enc_io = StringIO.new.binmode
158
+ enc_io.write("HDR") # emulate a header
159
+ enc.streaming_encrypt(from_plaintext_io: source_io, into_ciphertext_io: enc_io)
160
+
161
+ enc_io.seek(3) # Move to the offset where ciphertext starts
162
+
163
+ decrypted_io = StringIO.new.binmode
164
+ enc.streaming_decrypt(from_ciphertext_io: enc_io, into_plaintext_io: decrypted_io)
165
+ assert_equal decrypted_io.size, source_io.size, "#{scheme_class_name} should have decrypted the entire message"
166
+ assert_equal plain_bytes, decrypted_io.string, "#{scheme_class_name} Bytes mismatch when decrypting the entire message"
167
+ end
168
+
169
+ def assert_allows_random_access(scheme_class_name)
170
+ rng = Random.new(Minitest.seed)
171
+ random_encryption_key = rng.bytes(64)
172
+
173
+ enc = resolve(scheme_class_name).new(random_encryption_key)
174
+
175
+ # Generate a prime number of bytes, so that the plaintext does not
176
+ # subdivide into blocks. This will allow us to find situations where
177
+ # block offsets are not used for reading. A 24-bit prime we get is
178
+ # 14896667, which is just over 14 megabytes
179
+ amount_of_plain_bytes = OpenSSL::BN.generate_prime(24).to_i
180
+ plain_bytes = rng.bytes(amount_of_plain_bytes)
181
+
182
+ source_io = StringIO.new(plain_bytes)
183
+ enc_io = StringIO.new.binmode
184
+ enc_io.write("HDR") # emulate a header
185
+ enc.streaming_encrypt(from_plaintext_io: source_io, into_ciphertext_io: enc_io)
186
+
187
+ enc_io.seek(3) # Move to the offset where ciphertext starts
188
+
189
+ ranges = [
190
+ 0..0, # The first byte
191
+ 2..71, # Bytes that overlap block boundaries
192
+ 78..91, # A random located byte
193
+ (amount_of_plain_bytes - 1)..(amount_of_plain_bytes - 1), # The last byte
194
+ 0..(amount_of_plain_bytes - 1) # The entire monty, but via ranges
195
+ ]
196
+ ranges += 8.times.map do
197
+ r_begin = rng.rand(0..(amount_of_plain_bytes - 1))
198
+ n_bytes = rng.rand(1..1204)
199
+ r_begin..(r_begin + n_bytes)
200
+ end
201
+
202
+ ranges.each do |range|
203
+ enc_io.seek(3) # Emulate the header already did get read
204
+
205
+ expected = plain_bytes[range]
206
+ next unless expected
207
+
208
+ got = enc.decrypt_range(from_ciphertext_io: enc_io, range: range)
209
+
210
+ assert got, "#{scheme_class_name} Range #{range} should have been decrypted but no bytes were output"
211
+ assert_equal expected.bytesize, got.bytesize, "#{scheme_class_name} Range #{range} should have decrypted #{expected.bytesize} bytes but decrypted #{got.bytesize}"
212
+ assert_equal expected, got, "#{scheme_class_name} Range #{range} bytes mismatch (#{expected[0..16].inspect} expected but #{got[0..16].inspect} decrypted"
213
+ end
214
+ end
215
+
216
+ def resolve(module_name)
217
+ module_name.split("::").reduce(Kernel) do |namespace, const_name|
218
+ namespace.const_get(const_name)
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "block_cipher_kit"
6
+ require "minitest/autorun"
@@ -0,0 +1,88 @@
1
+ require_relative "test_helper"
2
+
3
+ class TestKnownCiphertext < Minitest::Test
4
+ PREAMBLE = <<~ERR
5
+ This file is used as known plaintext for testing encryption schemes.
6
+ It is designed to be a decent input harness: its size in bytes is a prime,
7
+ so it will require padding to be produced if a cipher needs padding, and it
8
+ will not subdivide into any standard block sizes. The subsequent content is
9
+ not a hoax or a spoof. Bytes following this line are generated using the
10
+ Ruby Random RNG, which is seeded with 42 - the answer to Life, Universe and
11
+ Everything. You can verify that it is indeed so by generating the bytes yourself:
12
+ Random.new(42).bytes(prime_used_to_size_file - preamble_byte_length)
13
+ And now, enjoy randomness!
14
+ =====================================================================
15
+ ERR
16
+
17
+ SCHEME_NAMES = [
18
+ "BlockCipherKit::AES256CFBScheme",
19
+ "BlockCipherKit::AES256CFBCIVScheme",
20
+ "BlockCipherKit::AES256CTRScheme",
21
+ "BlockCipherKit::AES256GCMScheme",
22
+ "BlockCipherKit::AES256CBCScheme"
23
+ ]
24
+ SCHEME_NAMES_INCLUDING_PASSTHRU = SCHEME_NAMES + ["BlockCipherKit::PassthruScheme"]
25
+
26
+ SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
27
+ define_method "test_scheme #{scheme_class_name} produces deterministic ciphertext" do
28
+ assert_stable_ciphertext(scheme_class_name)
29
+ end
30
+ end
31
+
32
+ def io_with_known_plaintext
33
+ StringIO.new.binmode.tap do |out|
34
+ # Generate a prime number of bytes, so that the plaintext does not
35
+ # subdivide into blocks. This will allow us to find situations where
36
+ # block offsets are not used for reading. To get this number, we used
37
+ # OpenSSL::BN.generate_prime(12).to_i
38
+ amount_of_plain_bytes = 3623
39
+ n_bytes_of_randomness = amount_of_plain_bytes - PREAMBLE.bytesize
40
+ out.write(PREAMBLE)
41
+ out.write(Random.new(42).bytes(n_bytes_of_randomness))
42
+ out.rewind
43
+ end
44
+ end
45
+
46
+ def assert_stable_ciphertext(scheme_class_name)
47
+ key = Random.new(21).bytes(64) # The scheme will use as many as it needs
48
+ iv_rng = Random.new(42) # Ensure the cipher generates a deterministic IV, so that ciphertext comes out the same
49
+ scheme = resolve(scheme_class_name).new(key, iv_generator: iv_rng)
50
+
51
+ out = StringIO.new.binmode
52
+ known_plaintext_path = __dir__ + "/known_ciphertexts/known_plain.bin"
53
+ File.open(known_plaintext_path, "rb") do |f|
54
+ scheme.streaming_encrypt(from_plaintext_io: f, into_ciphertext_io: out)
55
+ end
56
+ out.rewind
57
+
58
+ known_ciphertext_path = __dir__ + "/known_ciphertexts/" + scheme.class.to_s.split("::").last + ".ciphertext.bin"
59
+ File.open(known_ciphertext_path, "rb") do |f|
60
+ assert_equal f.size, out.size, "The output of the scheme must be the same size as the known ciphertext"
61
+ while (chunk = f.read(1024))
62
+ assert_equal chunk, out.read(1024)
63
+ end
64
+ end
65
+ end
66
+
67
+ def regenerate_reference_files!
68
+ key = Random.new(21).bytes(64) # The scheme will use as many as it needs
69
+ iv_rng = Random.new(42) # Ensure the cipher generates a deterministic IV, so that ciphertext comes out the same
70
+ scheme = resolve(scheme_class_name).new(key, iv_generator: iv_rng)
71
+
72
+ known_plaintext_path = __dir__ + "/known_ciphertexts/known_plain.bin"
73
+ File.open(known_plaintext_path, "wb") do |f|
74
+ IO.copy_stream(io_with_known_plaintext, f)
75
+ end
76
+
77
+ known_ciphertext_path = __dir__ + "/known_ciphertexts/" + scheme.class.to_s.split("::").last + ".ciphertext.bin"
78
+ File.open(known_ciphertext_path, "wb") do |f|
79
+ scheme.streaming_encrypt(from_plaintext_io: io_with_known_plaintext, into_ciphertext_io: f)
80
+ end
81
+ end
82
+
83
+ def resolve(module_name)
84
+ module_name.split("::").reduce(Kernel) do |namespace, const_name|
85
+ namespace.const_get(const_name)
86
+ end
87
+ end
88
+ end