block_cipher_kit 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ffa6157fd34839dbc9c8af5cd121b647974d3c869ce61fb50f5d3027f8fb3c2
4
- data.tar.gz: 8c14d6643ef3635d8e5ce8a66cbb16b30748b5e205989aacb3169842405c59f2
3
+ metadata.gz: d70126d0dac1d18bd5918e846fd0c098b812f262c7fc3f4f38a710fc0bce9d39
4
+ data.tar.gz: e3a30df8f291d59f78c658e13113431f9edb80a7e7b19e03bd05a89bf15041be
5
5
  SHA512:
6
- metadata.gz: b1c94c2b391c0bfe8f51ee94fd79563dbac77de43db7617eee679cca9adff6821a4e840152a091fe41bfdde1257875e7ed9d1d20bb77bcb5c19e888b1127478f
7
- data.tar.gz: 281cfb7fc70c84629fb5a15226fc560053ba183dd0b3ad75d8b819eded71b61f534541a5aa486e794d6d73a1d9cddc2affaea3147406ae86f23673418c7d4e6c
6
+ metadata.gz: 7dec668bbbe0be54b38e0f086bd375e698e840a6f083321d91797a6ca904a00b7b6b2845c79b05d0f4cc74d0041858a2b1151572f1fb37c38579dc2c5049c8f5
7
+ data.tar.gz: 75a36bdd66dc8331b119f9b2a5f520b6b0214b6ea1f45f586ff8ff6558b8e5c22f574722f99172086886572444f56b75ce2a1351e56dbd0cfde18e4bbb3e099b
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]
@@ -27,7 +27,9 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "rake"
28
28
  spec.add_development_dependency "magic_frozen_string_literal"
29
29
  spec.add_development_dependency "standard", "1.28.5" # Needed for 2.6
30
- spec.add_development_dependency "yard"
30
+
31
+ spec.add_development_dependency "yard", "~> 0.9"
32
+ spec.add_development_dependency "sord"
31
33
  # redcarpet is needed for the yard gem to enable Github Flavored Markdown
32
34
  spec.add_development_dependency "redcarpet"
33
35
 
@@ -3,6 +3,8 @@ 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
@@ -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,6 +1,7 @@
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
7
  @iv = BlockCipherKit::KeyMaterial.new(encryption_key.byteslice(0, 16))
@@ -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
@@ -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,6 +2,8 @@ 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
@@ -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,6 +1,9 @@
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
@@ -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,71 @@
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
+ private
68
+
20
69
  def read_copy_stream_via_cipher(source_io:, cipher:, read_limit: nil, destination_io: nil, finalize_cipher: true, &block_accepting_byte_chunks)
21
70
  writable = BlockCipherKit::BlockWritable.new(destination_io, &block_accepting_byte_chunks)
22
71
  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
@@ -2,6 +2,7 @@ require "forwardable"
2
2
 
3
3
  # Allows a string with key material (like IV and key)
4
4
  # to be concealed when an object holding it gets printed or show via #inspect
5
+ # :nodoc:
5
6
  class BlockCipherKit::KeyMaterial
6
7
  extend Forwardable
7
8
  def_delegators :@str, :b, :byteslice, :to_s, :to_str
@@ -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.2"
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,18 @@ 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
24
  autoload :KeyMaterial, __dir__ + "/block_cipher_kit/key_material.rb"
25
+
26
+ # private_constant :WriteWindowIO
27
+ # private_constant :ReadWindowIO
28
+ # private_constant :BlockWritable
29
+ # private_constant :CipherIO
30
+ # private_constant :KeyMaterial
31
+
24
32
  autoload :BaseScheme, __dir__ + "/block_cipher_kit/base_scheme.rb"
25
33
  autoload :PassthruScheme, __dir__ + "/block_cipher_kit/passthru_scheme.rb"
26
34
  autoload :AES256CTRScheme, __dir__ + "/block_cipher_kit/aes_256_ctr_scheme.rb"
@@ -28,5 +36,4 @@ module BlockCipherKit
28
36
  autoload :AES256GCMScheme, __dir__ + "/block_cipher_kit/aes_256_gcm_scheme.rb"
29
37
  autoload :AES256CFBScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_scheme.rb"
30
38
  autoload :AES256CFBCIVScheme, __dir__ + "/block_cipher_kit/aes_256_cfb_civ_scheme.rb"
31
- autoload :EncryptedDiskService, __dir__ + "/block_cipher_kit/encrypted_disk_service.rb"
32
39
  end
@@ -0,0 +1,424 @@
1
+ # typed: strong
2
+ module BlockCipherKit
3
+ VERSION = T.let("0.0.2", 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 type given for "source_io:", using untyped
79
+ # sord omit - no YARD type given for "cipher:", using untyped
80
+ # sord omit - no YARD type given for "read_limit:", using untyped
81
+ # sord omit - no YARD type given for "destination_io:", using untyped
82
+ # sord omit - no YARD type given for "finalize_cipher:", using untyped
83
+ # sord omit - no YARD return type given, using untyped
84
+ sig do
85
+ params(
86
+ source_io: T.untyped,
87
+ cipher: T.untyped,
88
+ read_limit: T.untyped,
89
+ destination_io: T.untyped,
90
+ finalize_cipher: T.untyped,
91
+ block_accepting_byte_chunks: T.untyped
92
+ ).returns(T.untyped)
93
+ end
94
+ def read_copy_stream_via_cipher(source_io:, cipher:, read_limit: nil, destination_io: nil, finalize_cipher: true, &block_accepting_byte_chunks); end
95
+
96
+ # sord omit - no YARD type given for "cipher:", using untyped
97
+ # sord omit - no YARD type given for "destination_io:", using untyped
98
+ # sord omit - no YARD type given for "source_io:", using untyped
99
+ # sord omit - no YARD type given for "read_limit:", using untyped
100
+ # sord omit - no YARD return type given, using untyped
101
+ sig do
102
+ params(
103
+ cipher: T.untyped,
104
+ destination_io: T.untyped,
105
+ source_io: T.untyped,
106
+ read_limit: T.untyped,
107
+ block_accepting_writable_io: T.untyped
108
+ ).returns(T.untyped)
109
+ end
110
+ def write_copy_stream_via_cipher(cipher:, destination_io:, source_io: nil, read_limit: nil, &block_accepting_writable_io); end
111
+ end
112
+
113
+ # Allows a string with key material (like IV and key)
114
+ # to be concealed when an object holding it gets printed or show via #inspect
115
+ # :nodoc:
116
+ class KeyMaterial
117
+ extend Forwardable
118
+
119
+ # sord omit - no YARD type given for "str", using untyped
120
+ sig { params(str: T.untyped).void }
121
+ def initialize(str); end
122
+
123
+ # sord omit - no YARD return type given, using untyped
124
+ sig { returns(T.untyped) }
125
+ def inspect; end
126
+ end
127
+
128
+ # :nodoc:
129
+ # An adapter which allows a block that accepts chunks of
130
+ # written data to be used as an IO and passed to IO.copy_stream
131
+ class BlockWritable
132
+ # sord omit - no YARD type given for "io", using untyped
133
+ # sord omit - no YARD return type given, using untyped
134
+ sig { params(io: T.untyped, blk: T.untyped).returns(T.untyped) }
135
+ def self.new(io = nil, &blk); end
136
+
137
+ sig { params(blk: T.untyped).void }
138
+ def initialize(&blk); end
139
+
140
+ # sord omit - no YARD type given for "string", using untyped
141
+ # sord omit - no YARD return type given, using untyped
142
+ sig { params(string: T.untyped).returns(T.untyped) }
143
+ def write(string); end
144
+ end
145
+
146
+ class ReadWindowIO
147
+ # sord omit - no YARD type given for "io", using untyped
148
+ # sord omit - no YARD type given for "starting_at_offset", using untyped
149
+ # sord omit - no YARD type given for "window_size", using untyped
150
+ sig { params(io: T.untyped, starting_at_offset: T.untyped, window_size: T.untyped).void }
151
+ def initialize(io, starting_at_offset, window_size); end
152
+
153
+ # sord omit - no YARD return type given, using untyped
154
+ sig { returns(T.untyped) }
155
+ def size; end
156
+
157
+ # sord omit - no YARD type given for "n_bytes", using untyped
158
+ # sord omit - no YARD return type given, using untyped
159
+ sig { params(n_bytes: T.untyped).returns(T.untyped) }
160
+ def read(n_bytes); end
161
+
162
+ # sord omit - no YARD type given for "to_offset_in_window", using untyped
163
+ # sord omit - no YARD return type given, using untyped
164
+ sig { params(to_offset_in_window: T.untyped).returns(T.untyped) }
165
+ def seek(to_offset_in_window); end
166
+
167
+ # sord omit - no YARD type given for :pos, using untyped
168
+ # Returns the value of attribute pos.
169
+ sig { returns(T.untyped) }
170
+ attr_reader :pos
171
+ end
172
+
173
+ class PassthruScheme < BlockCipherKit::BaseScheme
174
+ sig { void }
175
+ def initialize; end
176
+
177
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
178
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
179
+
180
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
181
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
182
+
183
+ sig do
184
+ params(
185
+ from_ciphertext_io: RandomReadIO,
186
+ range: T::Range[T.untyped],
187
+ into_plaintext_io: T.nilable(WritableIO),
188
+ blk: T.untyped
189
+ ).void
190
+ end
191
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
192
+ end
193
+
194
+ # Allows you to pass through the writes of a particular byte range only, discarding the rest
195
+ # :nodoc:
196
+ class WriteWindowIO
197
+ # sord omit - no YARD type given for "io", using untyped
198
+ # sord omit - no YARD type given for "offset", using untyped
199
+ # sord omit - no YARD type given for "size", using untyped
200
+ sig { params(io: T.untyped, offset: T.untyped, size: T.untyped).void }
201
+ def initialize(io, offset, size); end
202
+
203
+ # sord omit - no YARD type given for "bytes", using untyped
204
+ # sord omit - no YARD return type given, using untyped
205
+ sig { params(bytes: T.untyped).returns(T.untyped) }
206
+ def write(bytes); end
207
+
208
+ # sord omit - no YARD type given for "range_a", using untyped
209
+ # sord omit - no YARD type given for "range_b", using untyped
210
+ # sord omit - no YARD return type given, using untyped
211
+ # lifted from https://github.com/julik/range_utils/blob/master/lib/range_utils.rb
212
+ sig { params(range_a: T.untyped, range_b: T.untyped).returns(T.untyped) }
213
+ def intersection_of(range_a, range_b); end
214
+ end
215
+
216
+ class AES256CBCScheme < BlockCipherKit::BaseScheme
217
+ IV_LENGTH = T.let(16, T.untyped)
218
+
219
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
220
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
221
+ #
222
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
223
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom)).void }
224
+ def initialize(encryption_key, iv_generator: SecureRandom); end
225
+
226
+ # sord omit - no YARD return type given, using untyped
227
+ sig { returns(T.untyped) }
228
+ def required_encryption_key_length; end
229
+
230
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
231
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
232
+
233
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
234
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
235
+
236
+ sig do
237
+ params(
238
+ from_ciphertext_io: RandomReadIO,
239
+ range: T::Range[T.untyped],
240
+ into_plaintext_io: T.nilable(WritableIO),
241
+ blk: T.untyped
242
+ ).void
243
+ end
244
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
245
+ end
246
+
247
+ class AES256CFBScheme < BlockCipherKit::BaseScheme
248
+ IV_LENGTH = T.let(16, T.untyped)
249
+
250
+ # sord omit - no YARD type given for "encryption_key", using untyped
251
+ # sord omit - no YARD type given for "iv_generator:", using untyped
252
+ sig { params(encryption_key: T.untyped, iv_generator: T.untyped).void }
253
+ def initialize(encryption_key, iv_generator: SecureRandom); end
254
+
255
+ # sord omit - no YARD return type given, using untyped
256
+ sig { returns(T.untyped) }
257
+ def required_encryption_key_length; end
258
+
259
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
260
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
261
+
262
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
263
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
264
+
265
+ sig do
266
+ params(
267
+ from_ciphertext_io: RandomReadIO,
268
+ range: T::Range[T.untyped],
269
+ into_plaintext_io: T.nilable(WritableIO),
270
+ blk: T.untyped
271
+ ).void
272
+ end
273
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
274
+ end
275
+
276
+ class AES256CTRScheme < BlockCipherKit::BaseScheme
277
+ NONCE_LENGTH_BYTES = T.let(4, T.untyped)
278
+ IV_LENGTH_BYTES = T.let(8, T.untyped)
279
+
280
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
281
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
282
+ #
283
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
284
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom)).void }
285
+ def initialize(encryption_key, iv_generator: SecureRandom); end
286
+
287
+ # sord omit - no YARD return type given, using untyped
288
+ sig { returns(T.untyped) }
289
+ def required_encryption_key_length; end
290
+
291
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
292
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
293
+
294
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
295
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
296
+
297
+ sig do
298
+ params(
299
+ from_ciphertext_io: RandomReadIO,
300
+ range: T::Range[T.untyped],
301
+ into_plaintext_io: T.nilable(WritableIO),
302
+ blk: T.untyped
303
+ ).void
304
+ end
305
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
306
+
307
+ # sord omit - no YARD type given for "nonce_and_iv", using untyped
308
+ # sord omit - no YARD type given for "for_block_n", using untyped
309
+ # sord omit - no YARD return type given, using untyped
310
+ sig { params(nonce_and_iv: T.untyped, for_block_n: T.untyped).returns(T.untyped) }
311
+ def ctr_iv(nonce_and_iv, for_block_n); end
312
+ end
313
+
314
+ class AES256GCMScheme < BlockCipherKit::BaseScheme
315
+ IV_LENGTH = T.let(12, T.untyped)
316
+
317
+ # sord warn - SecureRandom wasn't able to be resolved to a constant in this project
318
+ # _@param_ `encryption_key` — a String in binary encoding containing the key for the cipher
319
+ #
320
+ # _@param_ `iv_generator` — RNG that can output bytes. A deterministic substitute can be used for testing.
321
+ #
322
+ # _@param_ `auth_data` — optional auth data for the cipher. If provided, this auth data will be used to write ciphertext and to validate.
323
+ sig { params(encryption_key: String, iv_generator: T.any(Random, SecureRandom), auth_data: String).void }
324
+ def initialize(encryption_key, iv_generator: SecureRandom, auth_data: ""); end
325
+
326
+ # sord omit - no YARD return type given, using untyped
327
+ sig { returns(T.untyped) }
328
+ def required_encryption_key_length; end
329
+
330
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
331
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
332
+
333
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
334
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
335
+
336
+ # Range decryption with GCM is performed by downgrading the GCM cipher to a CTR cipher, validation
337
+ # gets skipped.
338
+ #
339
+ # _@see_ `BaseScheme#streaming_decrypt_range`
340
+ sig do
341
+ params(
342
+ from_ciphertext_io: RandomReadIO,
343
+ range: T::Range[T.untyped],
344
+ into_plaintext_io: T.nilable(WritableIO),
345
+ blk: T.untyped
346
+ ).void
347
+ end
348
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
349
+
350
+ # sord omit - no YARD type given for "initial_iv_from_input", using untyped
351
+ # sord omit - no YARD type given for "for_block_n", using untyped
352
+ # sord omit - no YARD return type given, using untyped
353
+ sig { params(initial_iv_from_input: T.untyped, for_block_n: T.untyped).returns(T.untyped) }
354
+ def ctr_iv(initial_iv_from_input, for_block_n); end
355
+ end
356
+
357
+ class AES256CFBCIVScheme < BlockCipherKit::BaseScheme
358
+ # _@param_ `encryption_key` — a String in binary encoding containing the IV concatenated with the key for the cipher
359
+ sig { params(encryption_key: String).void }
360
+ def initialize(encryption_key); end
361
+
362
+ # sord omit - no YARD return type given, using untyped
363
+ sig { returns(T.untyped) }
364
+ def required_encryption_key_length; end
365
+
366
+ sig { params(from_ciphertext_io: StraightReadableIO, into_plaintext_io: T.nilable(WritableIO), blk: T.untyped).void }
367
+ def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk); end
368
+
369
+ sig { params(into_ciphertext_io: WritableIO, from_plaintext_io: T.nilable(StraightReadableIO), blk: T.untyped).void }
370
+ def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk); end
371
+
372
+ sig do
373
+ params(
374
+ from_ciphertext_io: RandomReadIO,
375
+ range: T::Range[T.untyped],
376
+ into_plaintext_io: T.nilable(WritableIO),
377
+ blk: T.untyped
378
+ ).void
379
+ end
380
+ def streaming_decrypt_range(from_ciphertext_io:, range:, into_plaintext_io: nil, &blk); end
381
+ end
382
+ end
383
+
384
+ # Used as a stand-in for any IO-ish that responds to #read. This module is defined for YARD docs
385
+ # so that Sorbet has a proper type definition.
386
+ module StraightReadableIO
387
+ # _@param_ `n` — how many bytes to read from the IO
388
+ #
389
+ # _@return_ — a String in binary encoding or nil
390
+ sig { params(n: Integer).returns(T.nilable(String)) }
391
+ def read(n); end
392
+ end
393
+
394
+ # Used as a stand-in for any IO-ish that responds to `#read`, `#seek`, `#pos` and `#size`
395
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
396
+ module RandomReadIO
397
+ # _@param_ `n` — how many bytes to read from the IO
398
+ #
399
+ # _@return_ — a String in binary encoding or nil
400
+ sig { params(n: Integer).returns(T.nilable(String)) }
401
+ def read(n); end
402
+
403
+ # _@param_ `to_absolute_offset` — the absolute offset in the IO to seek to
404
+ sig { params(to_absolute_offset: Integer).returns(Integer) }
405
+ def seek(to_absolute_offset); end
406
+
407
+ # _@return_ — current position in the IO
408
+ sig { returns(Integer) }
409
+ def pos; end
410
+
411
+ # _@return_ — the total size of the data in the IO
412
+ sig { returns(Integer) }
413
+ def size; end
414
+ end
415
+
416
+ # Used as a stand-in for any IO that responds to `#write`
417
+ # This module is defined for YARD docs so that Sorbet has a proper type definition.
418
+ module WritableIO
419
+ # _@param_ `string` — the bytes to write into the IO
420
+ #
421
+ # _@return_ — the amount of bytes consumed. Will usually be `bytes.bytesize`
422
+ sig { params(string: String).returns(Integer) }
423
+ def write(string); end
424
+ 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
@@ -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.2
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-17 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,12 +132,14 @@ 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
135
+ - lib/block_cipher_kit/io_types.rb
122
136
  - lib/block_cipher_kit/key_material.rb
123
137
  - lib/block_cipher_kit/passthru_scheme.rb
138
+ - lib/block_cipher_kit/read_window_io.rb
124
139
  - lib/block_cipher_kit/version.rb
140
+ - lib/block_cipher_kit/write_window_io.rb
141
+ - rbi/block_cipher_kit.rbi
125
142
  - test/cipher_io_test.rb
126
- - test/io_lens_test.rb
127
143
  - test/key_material_test.rb
128
144
  - test/known_ciphertexts/AES256CBCScheme.ciphertext.bin
129
145
  - test/known_ciphertexts/AES256CFBCIVScheme.ciphertext.bin
@@ -132,9 +148,11 @@ files:
132
148
  - test/known_ciphertexts/AES256GCMScheme.ciphertext.bin
133
149
  - test/known_ciphertexts/PassthruScheme.ciphertext.bin
134
150
  - test/known_ciphertexts/known_plain.bin
151
+ - test/read_window_io_test.rb
135
152
  - test/schemes_test.rb
136
153
  - test/test_helper.rb
137
154
  - test/test_known_ciphertext.rb
155
+ - test/write_window_io_test.rb
138
156
  homepage: https://github.com/julik/block_cipher_kit
139
157
  licenses:
140
158
  - MIT
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