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 +4 -4
- data/.gitignore +1 -0
- data/README.md +5 -6
- data/Rakefile +9 -1
- data/block_cipher_kit.gemspec +3 -1
- data/lib/block_cipher_kit/aes_256_cbc_scheme.rb +13 -5
- data/lib/block_cipher_kit/aes_256_cfb_civ_scheme.rb +2 -1
- data/lib/block_cipher_kit/aes_256_cfb_scheme.rb +3 -1
- data/lib/block_cipher_kit/aes_256_ctr_scheme.rb +5 -2
- data/lib/block_cipher_kit/aes_256_gcm_scheme.rb +14 -5
- data/lib/block_cipher_kit/base_scheme.rb +49 -0
- data/lib/block_cipher_kit/block_writable.rb +11 -7
- data/lib/block_cipher_kit/cipher_io.rb +1 -0
- data/lib/block_cipher_kit/io_types.rb +39 -0
- data/lib/block_cipher_kit/key_material.rb +1 -0
- data/lib/block_cipher_kit/read_window_io.rb +35 -0
- data/lib/block_cipher_kit/version.rb +1 -1
- data/lib/block_cipher_kit/{io_lens.rb → write_window_io.rb} +4 -3
- data/lib/block_cipher_kit.rb +9 -2
- data/rbi/block_cipher_kit.rbi +424 -0
- data/test/read_window_io_test.rb +36 -0
- data/test/write_window_io_test.rb +30 -0
- metadata +22 -4
- data/test/io_lens_test.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d70126d0dac1d18bd5918e846fd0c098b812f262c7fc3f4f38a710fc0bce9d39
|
4
|
+
data.tar.gz: e3a30df8f291d59f78c658e13113431f9edb80a7e7b19e03bd05a89bf15041be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7dec668bbbe0be54b38e0f086bd375e698e840a6f083321d91797a6ca904a00b7b6b2845c79b05d0f4cc74d0041858a2b1151572f1fb37c38579dc2c5049c8f5
|
7
|
+
data.tar.gz: 75a36bdd66dc8331b119f9b2a5f520b6b0214b6ea1f45f586ff8ff6558b8e5c22f574722f99172086886572444f56b75ce2a1351e56dbd0cfde18e4bbb3e099b
|
data/.gitignore
CHANGED
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
|
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 (
|
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 (
|
11
|
-
* AES-256-GCM (
|
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
|
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]
|
data/block_cipher_kit.gemspec
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
#
|
53
|
-
#
|
54
|
-
|
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::
|
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::
|
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::
|
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
|
-
|
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
|
-
|
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:
|
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
|
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
|
-
|
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
|
-
|
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
|
@@ -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,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
|
-
|
5
|
-
|
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
|
|
data/lib/block_cipher_kit.rb
CHANGED
@@ -17,10 +17,18 @@ end
|
|
17
17
|
|
18
18
|
require "securerandom"
|
19
19
|
module BlockCipherKit
|
20
|
-
autoload :
|
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.
|
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-
|
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/
|
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
|