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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +37 -0
- data/.gitignore +57 -0
- data/.standard.yml +1 -0
- data/Gemfile +3 -0
- data/README.md +136 -0
- data/Rakefile +18 -0
- data/block_cipher_kit.gemspec +37 -0
- data/lib/block_cipher_kit/aes_256_cbc_scheme.rb +56 -0
- data/lib/block_cipher_kit/aes_256_cfb_civ_scheme.rb +35 -0
- data/lib/block_cipher_kit/aes_256_cfb_scheme.rb +37 -0
- data/lib/block_cipher_kit/aes_256_ctr_scheme.rb +87 -0
- data/lib/block_cipher_kit/aes_256_gcm_scheme.rb +108 -0
- data/lib/block_cipher_kit/base_scheme.rb +43 -0
- data/lib/block_cipher_kit/block_writable.rb +20 -0
- data/lib/block_cipher_kit/cipher_io.rb +16 -0
- data/lib/block_cipher_kit/io_lens.rb +46 -0
- data/lib/block_cipher_kit/key_material.rb +16 -0
- data/lib/block_cipher_kit/passthru_scheme.rb +26 -0
- data/lib/block_cipher_kit/version.rb +3 -0
- data/lib/block_cipher_kit.rb +32 -0
- data/test/cipher_io_test.rb +18 -0
- data/test/io_lens_test.rb +30 -0
- data/test/key_material_test.rb +12 -0
- data/test/known_ciphertexts/AES256CBCScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/AES256CFBCIVScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/AES256CFBScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/AES256CTRScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/AES256GCMScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/PassthruScheme.ciphertext.bin +0 -0
- data/test/known_ciphertexts/known_plain.bin +0 -0
- data/test/schemes_test.rb +221 -0
- data/test/test_helper.rb +6 -0
- data/test/test_known_ciphertext.rb +88 -0
- 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,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
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|