block_cipher_kit 0.0.1

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.
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