block_cipher_kit 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ffa6157fd34839dbc9c8af5cd121b647974d3c869ce61fb50f5d3027f8fb3c2
4
- data.tar.gz: 8c14d6643ef3635d8e5ce8a66cbb16b30748b5e205989aacb3169842405c59f2
3
+ metadata.gz: 20609183e16f9958a035b581945c5d891851a85df83cbd19741b3e94efe3a676
4
+ data.tar.gz: bee694246cdd3eae36af264e7fb3b90dc3179c22203f82e38d028a3733ee99a5
5
5
  SHA512:
6
- metadata.gz: b1c94c2b391c0bfe8f51ee94fd79563dbac77de43db7617eee679cca9adff6821a4e840152a091fe41bfdde1257875e7ed9d1d20bb77bcb5c19e888b1127478f
7
- data.tar.gz: 281cfb7fc70c84629fb5a15226fc560053ba183dd0b3ad75d8b819eded71b61f534541a5aa486e794d6d73a1d9cddc2affaea3147406ae86f23673418c7d4e6c
6
+ metadata.gz: ac9365aa614c6b9f88fdad333da7577f6b2e1080430df7bafdbb15aba14f031183269407423e6ba79ba6757d9851f141bc09af7c82ed692830f95a7ca9d3ea48
7
+ data.tar.gz: 5620bce257285e2c18be847ce3557cc5876136562b4c5f619c6eda9c7d761de884dee493f218ae781879af17e7dd94ab7aa6fe7d1f58e884453ac1da1ab07673
data/.gitignore CHANGED
@@ -55,3 +55,4 @@ tmp
55
55
  # Rubocop report
56
56
  rubocop.html
57
57
 
58
+ pkg/*.gem
data/README.md CHANGED
@@ -2,18 +2,17 @@
2
2
 
3
3
  Is a small shim on top of a few block ciphers. It is useful for encrypting and decrypting data stored in files, or accessible via IOs. The main addition from using "bare" ciphers is the addition of random access reads where it can be realised.
4
4
 
5
- The following constructions are currently implemented:
5
+ The gem provides a number of **schemes** which are known, mostly correct ways to use a particular block cipher. You can use those schemes to do block encryption and decryption.
6
+ The following schemes are currently implemented:
6
7
 
7
- * AES-256-CBC (limited random read access, requires reading to end of source)
8
+ * AES-256-CBC (random read access with overhead of 3 blocks)
8
9
  * AES-256-CFB (limited random read access, requires reading to start offset)
9
10
  * AES-256-CFB-CIV - CIV for "concatenated IV", The IV is provided together with the encryption key (assumes unique key per message (limited random read access, requires reading to start offset) -
10
- * AES-256-CTR (with random read access)
11
- * AES-256-GCM (with random read access via CTR, random read access does not validate)
11
+ * AES-256-CTR (random read access)
12
+ * AES-256-GCM (random read access via CTR, random read access does not validate)
12
13
 
13
14
  Most likely ChaCha20 cam be added fairly easily.
14
15
 
15
- The gem provides a number of **schemes** which are known, mostly correct ways to use a particular block cipher. You can use those schemes to do block encryption and decryption.
16
-
17
16
  ## What is a "scheme"?
18
17
 
19
18
  A scheme is a crypto **construction** - a particular way to use a particular block cipher. In this gem, the schemes are guaranteed not to change between releases. Once a scheme is part of the gem, you will be able to use that scheme to read data you have encrypted using that scheme. Most of the **schemes** provided by the gem are constructed from standard AES block ciphers, used in a standard, transparent manner.
data/Rakefile CHANGED
@@ -15,4 +15,12 @@ task :format do
15
15
  `bundle exec magic_frozen_string_literal .`
16
16
  end
17
17
 
18
- task default: [:test, :standard]
18
+ task :generate_typedefs do
19
+ `bundle exec sord rbi/block_cipher_kit.rbi`
20
+ end
21
+
22
+ # When building the gem, generate typedefs beforehand,
23
+ # so that they get included
24
+ Rake::Task["build"].enhance(["generate_typedefs"])
25
+
26
+ task default: [:test, :standard, :generate_typedefs]
@@ -10,7 +10,12 @@ Gem::Specification.new do |spec|
10
10
  spec.license = "MIT"
11
11
  spec.summary = "A thin toolkit for working with block cipher encryption."
12
12
  spec.description = "A thin toolkit for working with block cipher encryption."
13
+
13
14
  spec.homepage = "https://github.com/julik/block_cipher_kit"
15
+ # The homepage link on rubygems.org only appears if you add homepage_uri. Just spec.homepage is not enough.
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
14
19
 
15
20
  spec.required_ruby_version = ">= 2.7.0"
16
21
 
@@ -27,7 +32,9 @@ Gem::Specification.new do |spec|
27
32
  spec.add_development_dependency "rake"
28
33
  spec.add_development_dependency "magic_frozen_string_literal"
29
34
  spec.add_development_dependency "standard", "1.28.5" # Needed for 2.6
30
- spec.add_development_dependency "yard"
35
+
36
+ spec.add_development_dependency "yard", "~> 0.9"
37
+ spec.add_development_dependency "sord"
31
38
  # redcarpet is needed for the yard gem to enable Github Flavored Markdown
32
39
  spec.add_development_dependency "redcarpet"
33
40
 
@@ -3,10 +3,12 @@ require "securerandom"
3
3
  class BlockCipherKit::AES256CBCScheme < BlockCipherKit::BaseScheme
4
4
  IV_LENGTH = 16
5
5
 
6
+ # @param encryption_key[String] a String in binary encoding containing the key for the cipher
7
+ # @param iv_generator[Random,SecureRandom] RNG that can output bytes. A deterministic substitute can be used for testing.
6
8
  def initialize(encryption_key, iv_generator: SecureRandom)
7
9
  raise ArgumentError, "#{required_encryption_key_length} bytes of key material needed, at the minimum" unless encryption_key.bytesize >= required_encryption_key_length
8
10
  @iv_generator = iv_generator
9
- @key = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 32))
11
+ @key = encryption_key.byteslice(0, 32)
10
12
  end
11
13
 
12
14
  def required_encryption_key_length
@@ -36,6 +38,10 @@ class BlockCipherKit::AES256CBCScheme < BlockCipherKit::BaseScheme
36
38
  n_bytes_to_decrypt = range.end - range.begin + 1
37
39
  n_blocks_to_skip, offset_into_first_block = range.begin.divmod(block_size)
38
40
 
41
+ # We need to read ahead to know well whether to call "final" on the cipher
42
+ n_blocks_to_read = (n_bytes_to_decrypt.to_f / block_size).ceil + 2
43
+ n_bytes_to_read = (n_blocks_to_read * block_size)
44
+
39
45
  cipher = OpenSSL::Cipher.new("aes-256-cbc")
40
46
  cipher.decrypt
41
47
  cipher.key = @key
@@ -46,11 +52,13 @@ class BlockCipherKit::AES256CBCScheme < BlockCipherKit::BaseScheme
46
52
  cipher.iv = from_ciphertext_io.read(IV_LENGTH)
47
53
 
48
54
  writable = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
49
- lens_range = offset_into_first_block...(offset_into_first_block + n_bytes_to_decrypt)
50
- lens = BlockCipherKit::IOLens.new(writable, lens_range)
55
+ lens = BlockCipherKit::WriteWindowIO.new(writable, offset_into_first_block, n_bytes_to_decrypt)
51
56
 
52
- # TODO: it seems that if we read only the blocks we touch, we need to call cipher.final to get all the output - the cipher buffers,
53
- # but if we call .final without having read the entire ciphertext the cipher will barf. This needs to be fixed as it is certainly possible with CBC.
54
- read_copy_stream_via_cipher(source_io: from_ciphertext_io, destination_io: lens, cipher: cipher, finalize_cipher: true, read_limit: from_ciphertext_io.size - from_ciphertext_io.pos)
57
+ # We need to know whether we are going to be finishing our read with a block that may be shorter than
58
+ # block_size. In that case we must call `.final` on the cipher so that it releases us the decrypted
59
+ # plaintext instead of waiting for the remainder of the bits the last block consists of
60
+ bytes_remaining = from_ciphertext_io.size - from_ciphertext_io.pos
61
+ do_finalize = bytes_remaining < n_bytes_to_read
62
+ read_copy_stream_via_cipher(source_io: from_ciphertext_io, destination_io: lens, cipher: cipher, finalize_cipher: do_finalize, read_limit: n_bytes_to_read)
55
63
  end
56
64
  end
@@ -1,10 +1,11 @@
1
1
  require "tempfile"
2
2
 
3
3
  class BlockCipherKit::AES256CFBCIVScheme < BlockCipherKit::BaseScheme
4
+ # @param encryption_key[String] a String in binary encoding containing the IV concatenated with the key for the cipher
4
5
  def initialize(encryption_key, **)
5
6
  raise ArgumentError, "#{required_encryption_key_length} bytes of key material needed, at the minimum" unless encryption_key.bytesize >= required_encryption_key_length
6
- @iv = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 16))
7
- @key = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(16, 32))
7
+ @iv = encryption_key.byteslice(0, 16)
8
+ @key = encryption_key.byteslice(16, 32)
8
9
  end
9
10
 
10
11
  def required_encryption_key_length
@@ -29,7 +30,7 @@ class BlockCipherKit::AES256CFBCIVScheme < BlockCipherKit::BaseScheme
29
30
 
30
31
  def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk)
31
32
  writable = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
32
- lens = BlockCipherKit::IOLens.new(writable, range)
33
+ lens = BlockCipherKit::WriteWindowIO.new(writable, range.begin, range.end - range.begin + 1)
33
34
  streaming_decrypt(from_ciphertext_io: from_ciphertext_io, into_plaintext_io: lens)
34
35
  end
35
36
  end
@@ -4,7 +4,7 @@ class BlockCipherKit::AES256CFBScheme < BlockCipherKit::BaseScheme
4
4
  def initialize(encryption_key, iv_generator: SecureRandom)
5
5
  raise ArgumentError, "#{required_encryption_key_length} bytes of key material needed, at the minimum" unless encryption_key.bytesize >= required_encryption_key_length
6
6
  @iv_generator = iv_generator
7
- @key = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 32))
7
+ @key = encryption_key.byteslice(0, 32)
8
8
  end
9
9
 
10
10
  def required_encryption_key_length
@@ -30,8 +30,10 @@ class BlockCipherKit::AES256CFBScheme < BlockCipherKit::BaseScheme
30
30
  end
31
31
 
32
32
  def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk)
33
+ # There is potential, but I don't have time for this at the moment
34
+ # https://crypto.stackexchange.com/a/87007
33
35
  writable = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
34
- lens = BlockCipherKit::IOLens.new(writable, range)
36
+ lens = BlockCipherKit::WriteWindowIO.new(writable, range.begin, range.end - range.begin + 1)
35
37
  streaming_decrypt(from_ciphertext_io: from_ciphertext_io, into_plaintext_io: lens)
36
38
  end
37
39
  end
@@ -2,10 +2,12 @@ class BlockCipherKit::AES256CTRScheme < BlockCipherKit::BaseScheme
2
2
  NONCE_LENGTH_BYTES = 4
3
3
  IV_LENGTH_BYTES = 8
4
4
 
5
+ # @param encryption_key[String] a String in binary encoding containing the key for the cipher
6
+ # @param iv_generator[Random,SecureRandom] RNG that can output bytes. A deterministic substitute can be used for testing.
5
7
  def initialize(encryption_key, iv_generator: SecureRandom)
6
8
  raise ArgumentError, "#{required_encryption_key_length} bytes of key material needed, at the minimum" unless encryption_key.bytesize >= required_encryption_key_length
7
9
  @iv_generator = iv_generator
8
- @key = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 32))
10
+ @key = encryption_key.byteslice(0, 32)
9
11
  end
10
12
 
11
13
  def required_encryption_key_length
@@ -46,9 +48,8 @@ class BlockCipherKit::AES256CTRScheme < BlockCipherKit::BaseScheme
46
48
  cipher.key = @key
47
49
  cipher.iv = ctr_iv(nonce_and_iv, n_blocks_to_skip) # Set the counter for the first block we will be reading
48
50
 
49
- lens_range = offset_into_first_block...(offset_into_first_block + n_bytes_to_read)
50
51
  writable = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
51
- lens = BlockCipherKit::IOLens.new(writable, lens_range)
52
+ lens = BlockCipherKit::WriteWindowIO.new(writable, offset_into_first_block, n_bytes_to_read)
52
53
 
53
54
  # With CTR we do not need to read until the end of ciphertext as the cipher does not validate
54
55
  from_ciphertext_io.seek(ciphertext_starts_at + (n_blocks_to_skip * block_size))
@@ -56,6 +57,8 @@ class BlockCipherKit::AES256CTRScheme < BlockCipherKit::BaseScheme
56
57
  read_copy_stream_via_cipher(source_io: from_ciphertext_io, destination_io: lens, cipher: cipher, read_limit: n_blocks_to_read * block_size)
57
58
  end
58
59
 
60
+ private
61
+
59
62
  def ctr_iv(nonce_and_iv, for_block_n)
60
63
  # The IV is the counter block
61
64
  # see spec https://datatracker.ietf.org/doc/html/rfc3686#section-4
@@ -1,11 +1,14 @@
1
1
  class BlockCipherKit::AES256GCMScheme < BlockCipherKit::BaseScheme
2
2
  IV_LENGTH = 12
3
3
 
4
+ # @param encryption_key[String] a String in binary encoding containing the key for the cipher
5
+ # @param iv_generator[Random,SecureRandom] RNG that can output bytes. A deterministic substitute can be used for testing.
6
+ # @param auth_data[String] optional auth data for the cipher. If provided, this auth data will be used to write ciphertext and to validate.
4
7
  def initialize(encryption_key, iv_generator: SecureRandom, auth_data: "")
5
8
  raise ArgumentError, "#{required_encryption_key_length} bytes of key material needed, at the minimum" unless encryption_key.bytesize >= required_encryption_key_length
6
9
  @iv_generator = iv_generator
7
- @auth_data = BlockCipherKit::KeyMaterial.new(auth_data.b)
8
- @key = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 32))
10
+ @auth_data = auth_data.b
11
+ @key = encryption_key.byteslice(0, 32)
9
12
  end
10
13
 
11
14
  def required_encryption_key_length
@@ -63,7 +66,11 @@ class BlockCipherKit::AES256GCMScheme < BlockCipherKit::BaseScheme
63
66
  read_copy_stream_via_cipher(source_io: from_ciphertext_io, cipher: cipher, read_limit: n_bytes_to_read_excluding_auth_tag, destination_io: into_plaintext_io, &blk)
64
67
  end
65
68
 
66
- def decrypt_range(from_ciphertext_io:, range:)
69
+ # Range decryption with GCM is performed by downgrading the GCM cipher to a CTR cipher, validation
70
+ # gets skipped.
71
+ #
72
+ # @see BaseScheme#streaming_decrypt_range
73
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk)
67
74
  # GCM uses 16 byte blocks, but it writes the block
68
75
  # and the tag of 16 bytes. So actual block boundaries
69
76
  # are at 2x AES block size of 16 bytes. This is also
@@ -90,13 +97,15 @@ class BlockCipherKit::AES256GCMScheme < BlockCipherKit::BaseScheme
90
97
  cipher.iv = ctr_iv(initial_iv_from_input, n_blocks_to_skip) # Set the IV for the first block we will be reading
91
98
  cipher.key = @key
92
99
 
93
- buf = StringIO.new.binmode
100
+ writable = BlockCipherKit::BlockWritable.new(into_plaintext_io, &blk)
101
+ lens = BlockCipherKit::WriteWindowIO.new(writable, offset_into_first_block, n_bytes_to_read)
102
+
94
103
  from_ciphertext_io.seek(ciphertext_starts_at + (n_blocks_to_skip * block_and_tag_size))
95
- read_copy_stream_via_cipher(source_io: from_ciphertext_io, cipher: cipher, read_limit: n_blocks_to_read * block_and_tag_size, destination_io: buf)
96
- buf.seek(offset_into_first_block) # Discard the bytes beyound the offset
97
- buf.read(n_bytes_to_read) # return just the amount of bytes requested
104
+ read_copy_stream_via_cipher(source_io: from_ciphertext_io, cipher: cipher, read_limit: n_blocks_to_read * block_and_tag_size, destination_io: lens)
98
105
  end
99
106
 
107
+ private
108
+
100
109
  def ctr_iv(initial_iv_from_input, for_block_n)
101
110
  raise ArgumentError unless initial_iv_from_input.bytesize == 12
102
111
  # The counter gets incremented twice per block with GCM and the
@@ -1,22 +1,97 @@
1
1
  class BlockCipherKit::BaseScheme
2
+ # Decrypts the entire ciphered message, reading ciphertext out of `from_ciphertext_io`
3
+ # until its `read` returns `nil` (until EOF is implicitly reached). The scheme
4
+ # will also read any data at the start of the IO that it requires for
5
+ # operation, and consume the IO until exhaustion.
6
+ #
7
+ # @param from_ciphertext_io[StraightReadableIO] An IO-ish that responds to `read` with one argument,
8
+ # ciphertext will be read from that IO
9
+ # @param into_plaintext_io[WritableIO] An IO-ish that responds to `write` with one argument.
10
+ # If into_plaintext_io is not provided, the block passed to the method will receive
11
+ # String objects in binary encoding with chunks of decrypted ciphertext. The sizing
12
+ # of the chunks is defined by the cipher and the read size used by `IO.copy_stream`
13
+ # @yield [String] the chunk of decrypted bytes
14
+ # @return [void]
2
15
  def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk)
3
16
  raise "Unimplemented"
4
17
  end
5
18
 
19
+ # Encrypts the entire ciphered message, reading plaintext either from the `from_plaintext_io`
20
+ # until its `read` returns `nil` (until EOF is implicitly reached) or from writes to
21
+ # the object it yields (for streaming writes).
22
+ #
23
+ # The scheme will also write any leading data at the start of the output that should prefix the
24
+ # ciphertext (usually the IV) and any trailing data after the ciphertext (like a validation
25
+ # tag for cipher authentication) into the `into_ciphertext_io`.
26
+ #
27
+ # @param from_plaintext_io[StraightReadableIO,nil] An IO-ish that responds to `read` with one argument.
28
+ # If from_plaintext_io is not provided, the block passed to the method will receive
29
+ # an IO-ish object that responds to `#write` that plaintext can be written into.
30
+ # @param into_ciphertext_io[WritableIO] An IO-ish that responds to `write` with one argument,
31
+ # @yield [#write] IO-ish writable that accepts strings of plaintext into `#write`
32
+ # @return [void]
6
33
  def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk)
7
34
  raise "Unimplemented"
8
35
  end
9
36
 
37
+ # Decrypts the desired range of the ciphered message, reading ciphertext out of `from_ciphertext_io`.
38
+ # Reading requires the `from_ciphertext_io` to be seekable - it must support `#pos`, `#read`and `#seek`.
39
+ # The decrypted plaintext either gets written into `into_plaintext_io` if it is provided, or yielded
40
+ # to the passed block in String chunks.
41
+ #
42
+ # @param from_ciphertext_io[RandomReadIO] Ciphertext will be read from that IO. The IO must support random access.
43
+ # @param range[Range] range of bytes in plaintext offsets to decrypt. Endless ranges are supported.
44
+ # @param into_plaintext_io[WritableIO] An IO-ish that responds to `write` with one argument.
45
+ # If into_plaintext_io is not provided, the block passed to the method will receive
46
+ # String objects in binary encoding with chunks of decrypted ciphertext. The sizing
47
+ # of the chunks is defined by the cipher and the read size used by `IO.copy_stream`
48
+ # @yield [String] the chunk of decrypted bytes
49
+ # @return [void]
10
50
  def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk)
11
51
  raise "Unimplemented"
12
52
  end
13
53
 
54
+ # Decrypts the desired range of the ciphered message, reading ciphertext out of `from_ciphertext_io`.
55
+ # Reading requires the `from_ciphertext_io` to be seekable - it must support `#pos`, `#read`and `#seek`.
56
+ # The decrypted plaintext gets returned as a single concatenated String.
57
+ #
58
+ # @param from_ciphertext_io[RandomReadIO] Ciphertext will be read from that IO. The IO must support random access.
59
+ # @param range[Range] range of bytes in plaintext offsets to decrypt. Endless ranges are supported.
60
+ # @return [String] the decrypted bytes located at the given offset range inside the plaintext
14
61
  def decrypt_range(from_ciphertext_io:, range:)
15
62
  buf = StringIO.new.binmode
16
63
  streaming_decrypt_range(from_ciphertext_io: from_ciphertext_io, range: range, into_plaintext_io: buf)
17
64
  buf.string
18
65
  end
19
66
 
67
+ def inspect
68
+ # A reimplementation of #inspect based largely on
69
+ # https://alchemists.io/articles/ruby_object_inspection
70
+ pattern = +""
71
+ values = []
72
+
73
+ instance_variables.each do |name|
74
+ pattern << "#{name}=%s "
75
+ ivar_value = instance_variable_get(name)
76
+ if ivar_value.is_a?(String) && key_material_instance_variable_names.include?(name)
77
+ values.push("[SENSITIVE(#{ivar_value.bytesize * 8} bits)]")
78
+ else
79
+ values.push(ivar_value.inspect)
80
+ end
81
+ end
82
+
83
+ format "#<%s:%#018x #{pattern.strip}>", self.class, object_id << 1, *values
84
+ end
85
+
86
+ private
87
+
88
+ # The names of instance variables which contain key material and need to be masked in the
89
+ # output of BaseScheme#inspect. This prevents us from leaking the key, while allowing each
90
+ # subclass to define which ivars it considers sensitive.
91
+ def key_material_instance_variable_names
92
+ [:@key, :@iv]
93
+ end
94
+
20
95
  def read_copy_stream_via_cipher(source_io:, cipher:, read_limit: nil, destination_io: nil, finalize_cipher: true, &block_accepting_byte_chunks)
21
96
  writable = BlockCipherKit::BlockWritable.new(destination_io, &block_accepting_byte_chunks)
22
97
  cipher_io = BlockCipherKit::CipherIO.new(writable, cipher)
@@ -1,20 +1,24 @@
1
+ # :nodoc:
1
2
  # An adapter which allows a block that accepts chunks of
2
3
  # written data to be used as an IO and passed to IO.copy_stream
3
4
  class BlockCipherKit::BlockWritable
4
- def initialize(io = nil, &blk)
5
+ def self.new(io = nil, &blk)
5
6
  if (!io && !blk) || (io && blk)
6
7
  raise ArgumentError, "BlockWritable requires io or a block, but not both"
7
8
  end
8
- @io = io
9
+ # If the IO is given, it is better to just pass it through
10
+ # as IO.copy_stream will do optimisations for native IOs like
11
+ # File, Socket etc.
12
+ return io if io
13
+ super(&blk)
14
+ end
15
+
16
+ def initialize(&blk)
9
17
  @blk = blk
10
18
  end
11
19
 
12
20
  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
21
+ @blk.call(string.b)
18
22
  string.bytesize
19
23
  end
20
24
  end
@@ -1,5 +1,6 @@
1
1
  # Allows an OpenSSL::Cipher to be written through as if it were an IO. This
2
2
  # allows the cipher to be passed to things like IO.copy_stream
3
+ # :nodoc:
3
4
  class BlockCipherKit::CipherIO
4
5
  def initialize(io, cipher)
5
6
  @io = io
@@ -0,0 +1,39 @@
1
+ # Used as a stand-in for any IO-ish that responds to #read. This module is defined for YARD docs
2
+ # so that Sorbet has a proper type definition.
3
+ module StraightReadableIO
4
+ # @param n[Integer] how many bytes to read from the IO
5
+ # @return [String,nil] a String in binary encoding or nil
6
+ def read(n)
7
+ end
8
+ end
9
+
10
+ # Used as a stand-in for any IO-ish that responds to `#read`, `#seek`, `#pos` and `#size`
11
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
12
+ module RandomReadIO
13
+ # @param n[Integer] how many bytes to read from the IO
14
+ # @return [String,nil] a String in binary encoding or nil
15
+ def read(n)
16
+ end
17
+
18
+ # @param to_absolute_offset[Integer] the absolute offset in the IO to seek to
19
+ # @return [0]
20
+ def seek(to_absolute_offset)
21
+ end
22
+
23
+ # @return [Integer] current position in the IO
24
+ def pos
25
+ end
26
+
27
+ # @return [Integer] the total size of the data in the IO
28
+ def size
29
+ end
30
+ end
31
+
32
+ # Used as a stand-in for any IO that responds to `#write`
33
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
34
+ module WritableIO
35
+ # @param string[String] the bytes to write into the IO
36
+ # @return [Integer] the amount of bytes consumed. Will usually be `bytes.bytesize`
37
+ def write(string)
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ class BlockCipherKit::ReadWindowIO
2
+ def initialize(io, starting_at_offset, window_size)
3
+ @io = io
4
+ @starting_at_offset = starting_at_offset.to_i
5
+ @window_size = window_size.to_i
6
+ @pos = 0
7
+ end
8
+
9
+ def size
10
+ @window_size
11
+ end
12
+
13
+ attr_reader :pos
14
+
15
+ def read(n_bytes)
16
+ return "" if n_bytes == 0 # As hardcoded for all Ruby IO objects
17
+ raise ArgumentError, "negative length #{n_bytes} given" if n_bytes < 0 # also as per Ruby IO objects
18
+
19
+ window_limit = @starting_at_offset + @window_size
20
+ wants_upto = @starting_at_offset + @pos + n_bytes
21
+
22
+ read_limit = [window_limit, wants_upto].compact.min
23
+ actual_n = read_limit - (@starting_at_offset + @pos)
24
+ return if actual_n <= 0
25
+
26
+ @io.seek(@starting_at_offset + @pos)
27
+ @io.read(actual_n).tap { @pos += actual_n }
28
+ end
29
+
30
+ def seek(to_offset_in_window)
31
+ raise ArgumentError, "negative seek destination #{to_offset_in_window}" if to_offset_in_window < 0 # also as per Ruby IO objects
32
+ @pos = to_offset_in_window
33
+ 0
34
+ end
35
+ end
@@ -1,3 +1,3 @@
1
1
  module BlockCipherKit
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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)
4
+ # :nodoc:
5
+ class BlockCipherKit::WriteWindowIO
6
+ def initialize(io, offset, size)
7
+ @range = Range.new(offset, offset + size - 1)
6
8
  @io = io
7
- @range = range
8
9
  @pos = 0
9
10
  end
10
11
 
@@ -17,10 +17,16 @@ end
17
17
 
18
18
  require "securerandom"
19
19
  module BlockCipherKit
20
- autoload :IOLens, __dir__ + "/block_cipher_kit/io_lens.rb"
20
+ autoload :WriteWindowIO, __dir__ + "/block_cipher_kit/write_window_io.rb"
21
+ autoload :ReadWindowIO, __dir__ + "/block_cipher_kit/read_window_io.rb"
21
22
  autoload :BlockWritable, __dir__ + "/block_cipher_kit/block_writable.rb"
22
23
  autoload :CipherIO, __dir__ + "/block_cipher_kit/cipher_io.rb"
23
- autoload :KeyMaterial, __dir__ + "/block_cipher_kit/key_material.rb"
24
+
25
+ # private_constant :WriteWindowIO
26
+ # private_constant :ReadWindowIO
27
+ # private_constant :BlockWritable
28
+ # private_constant :CipherIO
29
+
24
30
  autoload :BaseScheme, __dir__ + "/block_cipher_kit/base_scheme.rb"
25
31
  autoload :PassthruScheme, __dir__ + "/block_cipher_kit/passthru_scheme.rb"
26
32
  autoload :AES256CTRScheme, __dir__ + "/block_cipher_kit/aes_256_ctr_scheme.rb"
@@ -28,5 +34,4 @@ module BlockCipherKit
28
34
  autoload :AES256GCMScheme, __dir__ + "/block_cipher_kit/aes_256_gcm_scheme.rb"
29
35
  autoload :AES256CFBScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_scheme.rb"
30
36
  autoload :AES256CFBCIVScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_civ_scheme.rb"
31
- autoload :EncryptedDiskService, __dir__ + "/block_cipher_kit/encrypted_disk_service.rb"
32
37
  end
@@ -0,0 +1,420 @@
1
+ # typed: strong
2
+ module BlockCipherKit
3
+ VERSION = T.let("0.0.3", T.untyped)
4
+
5
+ # Allows an OpenSSL::Cipher to be written through as if it were an IO. This
6
+ # allows the cipher to be passed to things like IO.copy_stream
7
+ # :nodoc:
8
+ class CipherIO
9
+ # sord omit - no YARD type given for "io", using untyped
10
+ # sord omit - no YARD type given for "cipher", using untyped
11
+ sig { params(io: T.untyped, cipher: T.untyped).void }
12
+ def initialize(io, cipher); end
13
+
14
+ # sord omit - no YARD type given for "bytes", using untyped
15
+ # sord omit - no YARD return type given, using untyped
16
+ sig { params(bytes: T.untyped).returns(T.untyped) }
17
+ def write(bytes); end
18
+ end
19
+
20
+ class BaseScheme
21
+ # Decrypts the entire ciphered message, reading ciphertext out of `from_ciphertext_io`
22
+ # until its `read` returns `nil` (until EOF is implicitly reached). The scheme
23
+ # will also read any data at the start of the IO that it requires for
24
+ # operation, and consume the IO until exhaustion.
25
+ #
26
+ # _@param_ `from_ciphertext_io` — An IO-ish that responds to `read` with one argument, ciphertext will be read from that IO
27
+ #
28
+ # _@param_ `into_plaintext_io` — An IO-ish that responds to `write` with one argument. If into_plaintext_io is not provided, the block passed to the method will receive String objects in binary encoding with chunks of decrypted ciphertext. The sizing of the chunks is defined by the cipher and the read size used by `IO.copy_stream`
29
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
30
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
31
+
32
+ # Encrypts the entire ciphered message, reading plaintext either from the `from_plaintext_io`
33
+ # until its `read` returns `nil` (until EOF is implicitly reached) or from writes to
34
+ # the object it yields (for streaming writes).
35
+ #
36
+ # The scheme will also write any leading data at the start of the output that should prefix the
37
+ # ciphertext (usually the IV) and any trailing data after the ciphertext (like a validation
38
+ # tag for cipher authentication) into the `into_ciphertext_io`.
39
+ #
40
+ # _@param_ `from_plaintext_io` — An IO-ish that responds to `read` with one argument. If from_plaintext_io is not provided, the block passed to the method will receive an IO-ish object that responds to `#write` that plaintext can be written into.
41
+ #
42
+ # _@param_ `into_ciphertext_io` — An IO-ish that responds to `write` with one argument,
43
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
44
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
45
+
46
+ # Decrypts the desired range of the ciphered message, reading ciphertext out of `from_ciphertext_io`.
47
+ # Reading requires the `from_ciphertext_io` to be seekable - it must support `#pos`, `#read`and `#seek`.
48
+ # The decrypted plaintext either gets written into `into_plaintext_io` if it is provided, or yielded
49
+ # to the passed block in String chunks.
50
+ #
51
+ # _@param_ `from_ciphertext_io` — Ciphertext will be read from that IO. The IO must support random access.
52
+ #
53
+ # _@param_ `range` — range of bytes in plaintext offsets to decrypt. Endless ranges are supported.
54
+ #
55
+ # _@param_ `into_plaintext_io` — An IO-ish that responds to `write` with one argument. If into_plaintext_io is not provided, the block passed to the method will receive String objects in binary encoding with chunks of decrypted ciphertext. The sizing of the chunks is defined by the cipher and the read size used by `IO.copy_stream`
56
+ sig do
57
+ params(
58
+ from_ciphertext_io: RandomReadIO,
59
+ range: T::Range[T.untyped],
60
+ into_plaintext_io: T.nilable(WritableIO),
61
+ blk: T.untyped
62
+ ).void
63
+ end
64
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
65
+
66
+ # Decrypts the desired range of the ciphered message, reading ciphertext out of `from_ciphertext_io`.
67
+ # Reading requires the `from_ciphertext_io` to be seekable - it must support `#pos`, `#read`and `#seek`.
68
+ # The decrypted plaintext gets returned as a single concatenated String.
69
+ #
70
+ # _@param_ `from_ciphertext_io` — Ciphertext will be read from that IO. The IO must support random access.
71
+ #
72
+ # _@param_ `range` — range of bytes in plaintext offsets to decrypt. Endless ranges are supported.
73
+ #
74
+ # _@return_ — the decrypted bytes located at the given offset range inside the plaintext
75
+ sig { params(from_ciphertext_io: RandomReadIO, range: T::Range[T.untyped]).returns(String) }
76
+ def decrypt_range(from_ciphertext_io:, range:); end
77
+
78
+ # sord omit - no YARD return type given, using untyped
79
+ sig { returns(T.untyped) }
80
+ def inspect; end
81
+
82
+ # sord omit - no YARD return type given, using untyped
83
+ # The names of instance variables which contain key material and need to be masked in the
84
+ # output of BaseScheme#inspect. This prevents us from leaking the key, while allowing each
85
+ # subclass to define which ivars it considers sensitive.
86
+ sig { returns(T.untyped) }
87
+ def key_material_instance_variable_names; end
88
+
89
+ # sord omit - no YARD type given for "source_io:", using untyped
90
+ # sord omit - no YARD type given for "cipher:", using untyped
91
+ # sord omit - no YARD type given for "read_limit:", using untyped
92
+ # sord omit - no YARD type given for "destination_io:", using untyped
93
+ # sord omit - no YARD type given for "finalize_cipher:", using untyped
94
+ # sord omit - no YARD return type given, using untyped
95
+ sig do
96
+ params(
97
+ source_io: T.untyped,
98
+ cipher: T.untyped,
99
+ read_limit: T.untyped,
100
+ destination_io: T.untyped,
101
+ finalize_cipher: T.untyped,
102
+ block_accepting_byte_chunks: T.untyped
103
+ ).returns(T.untyped)
104
+ end
105
+ def read_copy_stream_via_cipher(source_io:, cipher:, read_limit: nil, destination_io: nil, finalize_cipher: true, &block_accepting_byte_chunks); end
106
+
107
+ # sord omit - no YARD type given for "cipher:", using untyped
108
+ # sord omit - no YARD type given for "destination_io:", using untyped
109
+ # sord omit - no YARD type given for "source_io:", using untyped
110
+ # sord omit - no YARD type given for "read_limit:", using untyped
111
+ # sord omit - no YARD return type given, using untyped
112
+ sig do
113
+ params(
114
+ cipher: T.untyped,
115
+ destination_io: T.untyped,
116
+ source_io: T.untyped,
117
+ read_limit: T.untyped,
118
+ block_accepting_writable_io: T.untyped
119
+ ).returns(T.untyped)
120
+ end
121
+ def write_copy_stream_via_cipher(cipher:, destination_io:, source_io: nil, read_limit: nil, &block_accepting_writable_io); end
122
+ end
123
+
124
+ # :nodoc:
125
+ # An adapter which allows a block that accepts chunks of
126
+ # written data to be used as an IO and passed to IO.copy_stream
127
+ class BlockWritable
128
+ # sord omit - no YARD type given for "io", using untyped
129
+ # sord omit - no YARD return type given, using untyped
130
+ sig { params(io: T.untyped, blk: T.untyped).returns(T.untyped) }
131
+ def self.new(io = nil, &blk); end
132
+
133
+ sig { params(blk: T.untyped).void }
134
+ def initialize(&blk); end
135
+
136
+ # sord omit - no YARD type given for "string", using untyped
137
+ # sord omit - no YARD return type given, using untyped
138
+ sig { params(string: T.untyped).returns(T.untyped) }
139
+ def write(string); end
140
+ end
141
+
142
+ class ReadWindowIO
143
+ # sord omit - no YARD type given for "io", using untyped
144
+ # sord omit - no YARD type given for "starting_at_offset", using untyped
145
+ # sord omit - no YARD type given for "window_size", using untyped
146
+ sig { params(io: T.untyped, starting_at_offset: T.untyped, window_size: T.untyped).void }
147
+ def initialize(io, starting_at_offset, window_size); end
148
+
149
+ # sord omit - no YARD return type given, using untyped
150
+ sig { returns(T.untyped) }
151
+ def size; end
152
+
153
+ # sord omit - no YARD type given for "n_bytes", using untyped
154
+ # sord omit - no YARD return type given, using untyped
155
+ sig { params(n_bytes: T.untyped).returns(T.untyped) }
156
+ def read(n_bytes); end
157
+
158
+ # sord omit - no YARD type given for "to_offset_in_window", using untyped
159
+ # sord omit - no YARD return type given, using untyped
160
+ sig { params(to_offset_in_window: T.untyped).returns(T.untyped) }
161
+ def seek(to_offset_in_window); end
162
+
163
+ # sord omit - no YARD type given for :pos, using untyped
164
+ # Returns the value of attribute pos.
165
+ sig { returns(T.untyped) }
166
+ attr_reader :pos
167
+ end
168
+
169
+ class PassthruScheme < BlockCipherKit::BaseScheme
170
+ sig { void }
171
+ def initialize; end
172
+
173
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
174
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
175
+
176
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
177
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
178
+
179
+ sig do
180
+ params(
181
+ from_ciphertext_io: RandomReadIO,
182
+ range: T::Range[T.untyped],
183
+ into_plaintext_io: T.nilable(WritableIO),
184
+ blk: T.untyped
185
+ ).void
186
+ end
187
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
188
+ end
189
+
190
+ # Allows you to pass through the writes of a particular byte range only, discarding the rest
191
+ # :nodoc:
192
+ class WriteWindowIO
193
+ # sord omit - no YARD type given for "io", using untyped
194
+ # sord omit - no YARD type given for "offset", using untyped
195
+ # sord omit - no YARD type given for "size", using untyped
196
+ sig { params(io: T.untyped, offset: T.untyped, size: T.untyped).void }
197
+ def initialize(io, offset, size); end
198
+
199
+ # sord omit - no YARD type given for "bytes", using untyped
200
+ # sord omit - no YARD return type given, using untyped
201
+ sig { params(bytes: T.untyped).returns(T.untyped) }
202
+ def write(bytes); end
203
+
204
+ # sord omit - no YARD type given for "range_a", using untyped
205
+ # sord omit - no YARD type given for "range_b", using untyped
206
+ # sord omit - no YARD return type given, using untyped
207
+ # lifted from https://github.com/julik/range_utils/blob/master/lib/range_utils.rb
208
+ sig { params(range_a: T.untyped, range_b: T.untyped).returns(T.untyped) }
209
+ def intersection_of(range_a, range_b); end
210
+ end
211
+
212
+ class AES256CBCScheme < BlockCipherKit::BaseScheme
213
+ IV_LENGTH = T.let(16, T.untyped)
214
+
215
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
216
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
217
+ #
218
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
219
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom)).void }
220
+ def initialize(encryption_key, iv_generator: SecureRandom); end
221
+
222
+ # sord omit - no YARD return type given, using untyped
223
+ sig { returns(T.untyped) }
224
+ def required_encryption_key_length; end
225
+
226
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
227
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
228
+
229
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
230
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
231
+
232
+ sig do
233
+ params(
234
+ from_ciphertext_io: RandomReadIO,
235
+ range: T::Range[T.untyped],
236
+ into_plaintext_io: T.nilable(WritableIO),
237
+ blk: T.untyped
238
+ ).void
239
+ end
240
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
241
+ end
242
+
243
+ class AES256CFBScheme < BlockCipherKit::BaseScheme
244
+ IV_LENGTH = T.let(16, T.untyped)
245
+
246
+ # sord omit - no YARD type given for "encryption_key", using untyped
247
+ # sord omit - no YARD type given for "iv_generator:", using untyped
248
+ sig { params(encryption_key: T.untyped, iv_generator: T.untyped).void }
249
+ def initialize(encryption_key, iv_generator: SecureRandom); end
250
+
251
+ # sord omit - no YARD return type given, using untyped
252
+ sig { returns(T.untyped) }
253
+ def required_encryption_key_length; end
254
+
255
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
256
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
257
+
258
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
259
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
260
+
261
+ sig do
262
+ params(
263
+ from_ciphertext_io: RandomReadIO,
264
+ range: T::Range[T.untyped],
265
+ into_plaintext_io: T.nilable(WritableIO),
266
+ blk: T.untyped
267
+ ).void
268
+ end
269
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
270
+ end
271
+
272
+ class AES256CTRScheme < BlockCipherKit::BaseScheme
273
+ NONCE_LENGTH_BYTES = T.let(4, T.untyped)
274
+ IV_LENGTH_BYTES = T.let(8, T.untyped)
275
+
276
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
277
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
278
+ #
279
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
280
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom)).void }
281
+ def initialize(encryption_key, iv_generator: SecureRandom); end
282
+
283
+ # sord omit - no YARD return type given, using untyped
284
+ sig { returns(T.untyped) }
285
+ def required_encryption_key_length; end
286
+
287
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
288
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
289
+
290
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
291
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
292
+
293
+ sig do
294
+ params(
295
+ from_ciphertext_io: RandomReadIO,
296
+ range: T::Range[T.untyped],
297
+ into_plaintext_io: T.nilable(WritableIO),
298
+ blk: T.untyped
299
+ ).void
300
+ end
301
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
302
+
303
+ # sord omit - no YARD type given for "nonce_and_iv", using untyped
304
+ # sord omit - no YARD type given for "for_block_n", using untyped
305
+ # sord omit - no YARD return type given, using untyped
306
+ sig { params(nonce_and_iv: T.untyped, for_block_n: T.untyped).returns(T.untyped) }
307
+ def ctr_iv(nonce_and_iv, for_block_n); end
308
+ end
309
+
310
+ class AES256GCMScheme < BlockCipherKit::BaseScheme
311
+ IV_LENGTH = T.let(12, T.untyped)
312
+
313
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
314
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
315
+ #
316
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
317
+ #
318
+ # _@param_ `auth_data` — optional auth data for the cipher. If provided, this auth data will be used to write ciphertext and to validate.
319
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom), auth_data: String).void }
320
+ def initialize(encryption_key, iv_generator: SecureRandom, auth_data: ""); end
321
+
322
+ # sord omit - no YARD return type given, using untyped
323
+ sig { returns(T.untyped) }
324
+ def required_encryption_key_length; end
325
+
326
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
327
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
328
+
329
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
330
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
331
+
332
+ # Range decryption with GCM is performed by downgrading the GCM cipher to a CTR cipher, validation
333
+ # gets skipped.
334
+ #
335
+ # _@see_ `BaseScheme#streaming_decrypt_range`
336
+ sig do
337
+ params(
338
+ from_ciphertext_io: RandomReadIO,
339
+ range: T::Range[T.untyped],
340
+ into_plaintext_io: T.nilable(WritableIO),
341
+ blk: T.untyped
342
+ ).void
343
+ end
344
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
345
+
346
+ # sord omit - no YARD type given for "initial_iv_from_input", using untyped
347
+ # sord omit - no YARD type given for "for_block_n", using untyped
348
+ # sord omit - no YARD return type given, using untyped
349
+ sig { params(initial_iv_from_input: T.untyped, for_block_n: T.untyped).returns(T.untyped) }
350
+ def ctr_iv(initial_iv_from_input, for_block_n); end
351
+ end
352
+
353
+ class AES256CFBCIVScheme < BlockCipherKit::BaseScheme
354
+ # _@param_ `encryption_key` — a String in binary encoding containing the IV concatenated with the key for the cipher
355
+ sig { params(encryption_key: String).void }
356
+ def initialize(encryption_key); end
357
+
358
+ # sord omit - no YARD return type given, using untyped
359
+ sig { returns(T.untyped) }
360
+ def required_encryption_key_length; end
361
+
362
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
363
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
364
+
365
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
366
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
367
+
368
+ sig do
369
+ params(
370
+ from_ciphertext_io: RandomReadIO,
371
+ range: T::Range[T.untyped],
372
+ into_plaintext_io: T.nilable(WritableIO),
373
+ blk: T.untyped
374
+ ).void
375
+ end
376
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
377
+ end
378
+ end
379
+
380
+ # Used as a stand-in for any IO-ish that responds to #read. This module is defined for YARD docs
381
+ # so that Sorbet has a proper type definition.
382
+ module StraightReadableIO
383
+ # _@param_ `n` — how many bytes to read from the IO
384
+ #
385
+ # _@return_ — a String in binary encoding or nil
386
+ sig { params(n: Integer).returns(T.nilable(String)) }
387
+ def read(n); end
388
+ end
389
+
390
+ # Used as a stand-in for any IO-ish that responds to `#read`, `#seek`, `#pos` and `#size`
391
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
392
+ module RandomReadIO
393
+ # _@param_ `n` — how many bytes to read from the IO
394
+ #
395
+ # _@return_ — a String in binary encoding or nil
396
+ sig { params(n: Integer).returns(T.nilable(String)) }
397
+ def read(n); end
398
+
399
+ # _@param_ `to_absolute_offset` — the absolute offset in the IO to seek to
400
+ sig { params(to_absolute_offset: Integer).returns(Integer) }
401
+ def seek(to_absolute_offset); end
402
+
403
+ # _@return_ — current position in the IO
404
+ sig { returns(Integer) }
405
+ def pos; end
406
+
407
+ # _@return_ — the total size of the data in the IO
408
+ sig { returns(Integer) }
409
+ def size; end
410
+ end
411
+
412
+ # Used as a stand-in for any IO that responds to `#write`
413
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
414
+ module WritableIO
415
+ # _@param_ `string` — the bytes to write into the IO
416
+ #
417
+ # _@return_ — the amount of bytes consumed. Will usually be `bytes.bytesize`
418
+ sig { params(string: String).returns(Integer) }
419
+ def write(string); end
420
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class BlockCipherKit::ReadWindowTest < Minitest::Test
6
+ def test_read_window
7
+ text = "mary had a little lamb, riding on a pony"
8
+
9
+ window = BlockCipherKit::ReadWindowIO.new(StringIO.new(text), 0, 0)
10
+ assert_nil window.read(1)
11
+
12
+ window = BlockCipherKit::ReadWindowIO.new(StringIO.new(text), 0, 4)
13
+ assert_equal "m", window.read(1)
14
+ assert_equal "a", window.read(1)
15
+ assert_equal "ry", window.read(2)
16
+ assert_nil window.read(1)
17
+
18
+ io = StringIO.new(text)
19
+ window = BlockCipherKit::ReadWindowIO.new(io, 0, 4)
20
+ assert_equal 0, window.pos
21
+ assert_equal "m", window.read(1)
22
+ assert_equal 1, window.pos
23
+
24
+ io.seek(0)
25
+ assert_equal "a", window.read(1)
26
+ assert_equal 0, window.seek(0)
27
+
28
+ window = BlockCipherKit::ReadWindowIO.new(StringIO.new(text), 8, 23)
29
+ assert_equal " a l", window.read(4)
30
+ assert_equal "ittle la", window.read(8)
31
+
32
+ window = BlockCipherKit::ReadWindowIO.new(StringIO.new(text), 8, text.bytesize)
33
+ assert_equal " a little lamb, riding on a pony", window.read(400)
34
+ assert_nil window.read(1)
35
+ end
36
+ end
data/test/schemes_test.rb CHANGED
@@ -25,6 +25,12 @@ class SchemesTest < Minitest::Test
25
25
  end
26
26
  end
27
27
 
28
+ SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
29
+ define_method "test_scheme #{scheme_class_name} encrypts and decrypts an empty message" do
30
+ assert_encrypts_and_decrypts_empty_message(scheme_class_name)
31
+ end
32
+ end
33
+
28
34
  SCHEME_NAMES_INCLUDING_PASSTHRU.each do |scheme_class_name|
29
35
  define_method "test_scheme #{scheme_class_name} allows random access reads" do
30
36
  assert_allows_random_access(scheme_class_name)
@@ -73,6 +79,20 @@ class SchemesTest < Minitest::Test
73
79
  assert_equal ciphertexts.length, ciphertexts.uniq.length
74
80
  end
75
81
 
82
+ def assert_encrypts_and_decrypts_empty_message(scheme_class_name)
83
+ rng = Random.new(Minitest.seed)
84
+ key = rng.bytes(48)
85
+
86
+ scheme = resolve(scheme_class_name).new(key)
87
+ ciphered_io = StringIO.new
88
+ scheme.streaming_encrypt(from_plaintext_io: StringIO.new, into_ciphertext_io: ciphered_io)
89
+
90
+ ciphered_io.rewind
91
+ decrypted = StringIO.new
92
+ scheme.streaming_decrypt(from_ciphertext_io: ciphered_io, into_plaintext_io: decrypted)
93
+ assert_equal 0, decrypted.size
94
+ end
95
+
76
96
  def assert_encrypts_from_block_and_io(scheme_class_name)
77
97
  rng = Random.new(Minitest.seed)
78
98
  encryption_key = rng.bytes(64)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class WriteWindowIOTest < Minitest::Test
6
+ def test_lens_writes
7
+ input = Random.bytes(48)
8
+ (1..input.bytesize).each do |write_size|
9
+ ranges = [
10
+ [0, 1],
11
+ [0, 0],
12
+ [1, 1],
13
+ [1, 2],
14
+ [43, 120],
15
+ [14, write_size],
16
+ [0, 14]
17
+ ]
18
+ ranges.each do |(offset, size)|
19
+ test_io = StringIO.new.binmode
20
+ readable = StringIO.new(input).binmode
21
+ lens = BlockCipherKit::WriteWindowIO.new(test_io, offset, size)
22
+ while (chunk = readable.read(write_size))
23
+ lens.write(chunk)
24
+ end
25
+ assert_equal input.byteslice(offset, size).bytesize, test_io.size
26
+ assert_equal input.byteslice(offset, size), test_io.string
27
+ end
28
+ end
29
+ end
30
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: block_cipher_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2025-02-15 00:00:00.000000000 Z
12
+ date: 2025-02-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -69,6 +69,20 @@ dependencies:
69
69
  version: 1.28.5
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: yard
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.9'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.9'
84
+ - !ruby/object:Gem::Dependency
85
+ name: sord
72
86
  requirement: !ruby/object:Gem::Requirement
73
87
  requirements:
74
88
  - - ">="
@@ -118,13 +132,13 @@ files:
118
132
  - lib/block_cipher_kit/base_scheme.rb
119
133
  - lib/block_cipher_kit/block_writable.rb
120
134
  - lib/block_cipher_kit/cipher_io.rb
121
- - lib/block_cipher_kit/io_lens.rb
122
- - lib/block_cipher_kit/key_material.rb
135
+ - lib/block_cipher_kit/io_types.rb
123
136
  - lib/block_cipher_kit/passthru_scheme.rb
137
+ - lib/block_cipher_kit/read_window_io.rb
124
138
  - lib/block_cipher_kit/version.rb
139
+ - lib/block_cipher_kit/write_window_io.rb
140
+ - rbi/block_cipher_kit.rbi
125
141
  - test/cipher_io_test.rb
126
- - test/io_lens_test.rb
127
- - test/key_material_test.rb
128
142
  - test/known_ciphertexts/AES256CBCScheme.ciphertext.bin
129
143
  - test/known_ciphertexts/AES256CFBCIVScheme.ciphertext.bin
130
144
  - test/known_ciphertexts/AES256CFBScheme.ciphertext.bin
@@ -132,13 +146,18 @@ files:
132
146
  - test/known_ciphertexts/AES256GCMScheme.ciphertext.bin
133
147
  - test/known_ciphertexts/PassthruScheme.ciphertext.bin
134
148
  - test/known_ciphertexts/known_plain.bin
149
+ - test/read_window_io_test.rb
135
150
  - test/schemes_test.rb
136
151
  - test/test_helper.rb
137
152
  - test/test_known_ciphertext.rb
153
+ - test/write_window_io_test.rb
138
154
  homepage: https://github.com/julik/block_cipher_kit
139
155
  licenses:
140
156
  - MIT
141
157
  metadata:
158
+ homepage_uri: https://github.com/julik/block_cipher_kit
159
+ source_code_uri: https://github.com/julik/block_cipher_kit
160
+ changelog_uri: https://github.com/julik/block_cipher_kit/blob/main/CHANGELOG.md
142
161
  allowed_push_host: https://rubygems.org
143
162
  post_install_message:
144
163
  rdoc_options: []
@@ -1,16 +0,0 @@
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
data/test/io_lens_test.rb DELETED
@@ -1,30 +0,0 @@
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
@@ -1,12 +0,0 @@
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