block_cipher_kit
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.
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. The following schemes are currently implemented:
- AES-256-CBC (random read access with overhead of 3 blocks)
- AES-256-CFB (limited random read access, requires reading to start offset)
- 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) -
- AES-256-CTR (random read access)
- AES-256-GCM (random read access via CTR, random read access does not validate)
Most likely ChaCha20 cam be added fairly easily.
What is a "scheme"?
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.
The following rules hold true for any given Scheme:
- Ciphertext output for known plaintext, randomness source and encryption key of every scheme will come out exactly the same (a scheme encrypts deterministically).
- Plaintext output for known ciphertext and encryption key of every scheme will come out exactly the same (a scheme decrypts deterministically).
- The scheme's output will stay exactly the same throughout the versioning of the gem, provided the underlying cipher (OpenSSL or other) is available on the host system.
Schemes are versioned. We give a guarantee they are not going to change once this gem is at version 1.0.0
or higher, every scheme becomes "frozen" once it is published with the gem.
Interop
Data written by the schemes is compatible with the "bare" uses of the ciphers, layouts are as follows:
- AES-256-CBC - Layout is
[ IV - 16 bytes) ][ Ciphertext in 16 byte blocks, no padding ]
- AES-256-CFB - Layout is
[ IV - 16 bytes) ][ Ciphertext in 16 byte blocks ]
- AES-256-CTR - Layout is
[ nonce - 4 bytes][ IV - 8 bytes ][ Ciphertext in 16 byte blocks ]
- AES-256-GCM - Layout is
[ nonce - 4 bytes][ IV - 8 bytes ][ Ciphertext in 16 byte blocks ][ Validation tag - 16 bytes ]
- AES-256-CFB-CIV - Layout is
[ Ciphertext in 16 byte blocks ]
. Theencryption_key
must be[ IV - 16 bytes][ key - 32 bytes]
(IV is not stored with ciphertext)
You should thus be able to build either a decryptor that outputs in a format compatible with a given Scheme, or an encryptor that produces data valid for that Scheme, in any language that provides standard AES cipher constructions.
Which scheme to use?
Please do some research as the topic is vast. GCM is quite good, I found CBC to be good for files as well. Be aware that both GCM and CTR have a limit of about 64GB of ciphertext before the block counter rolls over.
Basic streaming use
Imagine you want to encrypt some data in a streaming manner with AES-256-CTR, and data is stored in files:
File.open(plain_file_path, "rb", "rb") do |from|
File.open(plain_file_path + ".enc", "wb") do |into|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_encrypt(from_plaintext_io: from, into_ciphertext_io: into)
end
end
To decrypt the same file
File.open(encrypted_file_path, "rb") do |from|
File.open(encrypted_file_path + ".plain", "wb") do |into|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_decrypt(from_ciphertext_io: from, into_plaintext_io: into)
end
end
Note that in both of these cases:
- Only
read
will be called on the source IO (from_ciphertext_io
andfrom_plaintext_io
). They do not need to supportpos
,seek
orrewind
. - Only
write
will be called on the destination IO (to_ciphertext_io
andto_plaintext_io
). They do not need to supportpos
,seek
orrewind
.
Streaming encryption / decryption "head to tail" with blocks
To use streaming encryption, writing the plaintext the data as you go:
File.open(plain_file_path + ".enc", "wb") do |into|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_encrypt(into_ciphertext_io: into) do |sink|
sink.write("This is some very secret data")
sink.write("Very secret indeed")
end
end
The sink
will be an object that responds to write
(it can also be used with IO.copy_stream
).
To use streaming decryption, reading the plaintext data as you go:
File.open(encrypted_file_path, "rb") do |from|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_encrypt(from_ciphertext_io: from) do |decrypted_chunk_of_plaintext|
$stdout.puts "Decrypted: #{decrypted_chunk_of_plaintext.inspect}"
end
end
Random access reads
For random access, you can either recover a String in binary encoding:
File.open(encrypted_file_path, "rb") do |from|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.decrypt_range(from_ciphertext_io: from, range: 15..16) #=> "ab"
end
or pass an IO to receive the decrypted data:
File.open(encrypted_file_path, "rb") do |from|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_decrypt_range(from_ciphertext_io: from, range: 15..16, into_plaintext_io: $stdout) #=> "ab" gets printed to STDOUT
end
or a block (will be called for every meaningful chunk of decrypted data, repeatedly):
File.open(encrypted_file_path, "rb") do |from|
scheme = BlockCipherKit::AES256CTRScheme.new(encryption_key)
scheme.streaming_decrypt_range(from_ciphertext_io: from, range: 15..) do |decrypted_chunk_of_plaintext|
$stderr.puts "Decrypted #{decrypted_chunk_of_plaintext.inspect}"
end
end
For both streaming_decrypt_range
and decrypt_range
:
- The source IO (
from_ciphertext_io
) must supportpos
,size
,seek
andread
- The destination IO must only support
write