lib/symmetric_encryption/writer.rb in symmetric-encryption-3.9.1 vs lib/symmetric_encryption/writer.rb in symmetric-encryption-4.0.0.beta3
- old
+ new
@@ -1,219 +1,206 @@
require 'openssl'
module SymmetricEncryption
- # Write to encrypted files and other IO streams
+ # Write to encrypted files and other IO streams.
#
# Features:
# * Encryption on the fly whilst writing files.
- # * Large file support by only buffering small amounts of data in memory
+ # * Large file support by only buffering small amounts of data in memory.
# * Underlying buffering to ensure that encrypted data fits
- # into the Symmetric Encryption Cipher block size
- # Only the last block in the file will be padded if it is less than the block size
+ # into the Symmetric Encryption Cipher block size.
+ # Only the last block in the file will be padded if it is less than the block size.
class Writer
- # Open a file for writing, or use the supplied IO Stream
+ # Open a file for writing, or use the supplied IO Stream.
#
# Parameters:
- # filename_or_stream:
- # The filename to open if a string, otherwise the stream to use
+ # file_name_or_stream: [String|IO]
+ # The file_name to open if a string, otherwise the stream to use.
# The file or stream will be closed on completion, use .initialize to
- # avoid having the stream closed automatically
+ # avoid having the stream closed automatically.
#
- # options:
- # :compress [true|false]
- # Uses Zlib to compress the data before it is encrypted and
- # written to the file
- # If true, it forces header to true.
- # Default: false
+ # compress: [true|false]
+ # Uses Zlib to compress the data before it is encrypted and
+ # written to the file/stream.
+ # Default: false
#
- # :random_key [true|false]
- # Generates a new random key for every new file or stream
- # If true, it forces header to true. Version below then has no effect
- # The Random key will be written to the file/stream in encrypted
- # form as part of the header
- # The key is encrypted using the global key
- # Default: true
- # Recommended: true.
- # Setting to false will eventually expose the
- # encryption key since too much data will be encrypted using the
- # same encryption key
- #
- # :random_iv [true|false]
- # Generates a new random iv for every new file or stream
- # If true, it forces header to true.
- # The Random iv will be written to the file/stream in encrypted
- # form as part of the header
- # Default: Value supplied above for :random_key
- # Recommended: true. Setting to false will eventually expose the
- # encryption key since too much data will be encrypted using the
- # same encryption key
- #
- # :header [true|false]
- # Whether to include the magic header that indicates the file
- # is encrypted and whether its contents are compressed
- #
- # The header contains:
- # Version of the encryption key used to encrypt the file
- # Indicator if the data was compressed
- # Default: true
- #
- # :version
- # When random_key is true, the version of the encryption key to use
- # when encrypting the header portion of the file
- #
- # When random_key is false, the version of the encryption key to use
- # to encrypt the entire file
- # Default: SymmetricEncryption.cipher
- #
- # :mode
- # See File.open for open modes
- # Default: 'w'
- #
- # :cipher_name
- # The name of the cipher to use only if both :random_key and
- # :random_iv are true.
- # Default: SymmetricEncryption.cipher.cipher_name
- #
# Note: Compression occurs before encryption
#
- #
# # Example: Encrypt and write data to a file
- # SymmetricEncryption::Writer.open('test_file') do |file|
+ # SymmetricEncryption::Writer.open('test_file.enc') do |file|
# file.write "Hello World\n"
# file.write 'Keep this secret'
# end
#
# # Example: Compress, Encrypt and write data to a file
- # SymmetricEncryption::Writer.open('encrypted_compressed.zip', compress: true) do |file|
+ # SymmetricEncryption::Writer.open('encrypted_compressed.enc', compress: true) do |file|
# file.write "Hello World\n"
# file.write "Compress this\n"
# file.write "Keep this safe and secure\n"
# end
#
# # Example: Writing to a CSV file
# require 'csv'
# begin
# # Must supply :row_sep for CSV otherwise it will attempt to read from and then rewind the file
- # csv = CSV.new(SymmetricEncryption::Writer.open('csv_encrypted'), row_sep: "\n")
+ # csv = CSV.new(SymmetricEncryption::Writer.open('csv.enc'), row_sep: "\n")
# csv << [1,2,3,4,5]
# ensure
# csv.close if csv
# end
- def self.open(filename_or_stream, options={}, &block)
- raise(ArgumentError, 'options must be a hash') unless options.respond_to?(:each_pair)
- mode = options.fetch(:mode, 'wb')
- compress = options.fetch(:compress, false)
- ios = filename_or_stream.is_a?(String) ? ::File.open(filename_or_stream, mode) : filename_or_stream
+ def self.open(file_name_or_stream, compress: false, **args)
+ ios = file_name_or_stream.is_a?(String) ? ::File.open(file_name_or_stream, 'wb') : file_name_or_stream
begin
- file = self.new(ios, options)
+ file = self.new(ios, compress: compress, **args)
file = Zlib::GzipWriter.new(file) if compress
- block ? block.call(file) : file
+ block_given? ? yield(file) : file
ensure
- file.close if block && file && (file.respond_to?(:closed?) && !file.closed?)
+ file.close if block_given? && file && (file.respond_to?(:closed?) && !file.closed?)
end
end
+ # Write the contents of a string in memory to an encrypted file / stream.
+ #
+ # Notes:
+ # * Do not use this method for writing large files.
+ def self.write(file_name_or_stream, data, **args)
+ open(file_name_or_stream, **args) { |f| f.write(data) }
+ end
+
+ # Encrypt an entire file.
+ #
+ # Returns [Integer] the number of encrypted bytes written to the target file.
+ #
+ # Params:
+ # source: [String|IO]
+ # Source file_name or IOStream
+ #
+ # target: [String|IO]
+ # Target file_name or IOStream
+ #
+ # compress: [true|false]
+ # Whether to compress the target file prior to encryption.
+ # Default: false
+ #
+ # block_size: [Integer]
+ # Number of bytes to read into memory for each read.
+ # For very large files using a larger block size is faster.
+ # Default: 65535
+ #
+ # Notes:
+ # * The file contents are streamed so that the entire file is _not_ loaded into memory.
+ def self.encrypt(source:, target:, block_size: 65535, **args)
+ source_ios = source.is_a?(String) ? ::File.open(source, 'rb') : source
+ bytes_written = 0
+ open(target, **args) do |output_file|
+ while !source_ios.eof?
+ bytes_written += output_file.write(source_ios.read(block_size))
+ end
+ end
+ bytes_written
+ ensure
+ source_ios.close if source_ios && source_ios.respond_to?(:closed?) && !source_ios.closed?
+ end
+
# Encrypt data before writing to the supplied stream
- def initialize(ios, options={})
- @ios = ios
- header = options.fetch(:header, true)
- random_key = options.fetch(:random_key, true)
- random_iv = options.fetch(:random_iv, random_key)
- raise(ArgumentError, 'When :random_key is true, :random_iv must also be true') if random_key && !random_iv
+ def initialize(ios, version: nil, cipher_name: nil, header: true, random_key: true, random_iv: true, compress: false)
# Compress is only used at this point for setting the flag in the header
- compress = options.fetch(:compress, false)
- version = options[:version]
- cipher_name = options[:cipher_name]
+ @ios = ios
+ raise(ArgumentError, 'When :random_key is true, :random_iv must also be true') if random_key && !random_iv
raise(ArgumentError, 'Cannot supply a :cipher_name unless both :random_key and :random_iv are true') if cipher_name && !random_key && !random_iv
- # Force header if compressed or using random iv, key
- header = true if compress || random_key || random_iv
-
# Cipher to encrypt the random_key, or the entire file
cipher = SymmetricEncryption.cipher(version)
raise(SymmetricEncryption::CipherError, "Cipher with version:#{version} not found in any of the configured SymmetricEncryption ciphers") unless cipher
+ # Force header if compressed or using random iv, key
+ if (header == true) || compress || random_key || random_iv
+ header = Header.new(version: cipher.version, compress: compress, cipher_name: cipher_name)
+ end
+
@stream_cipher = ::OpenSSL::Cipher.new(cipher_name || cipher.cipher_name)
@stream_cipher.encrypt
- key = random_key ? @stream_cipher.random_key : cipher.send(:key)
- iv = random_iv ? @stream_cipher.random_iv : cipher.send(:iv)
+ if random_key
+ header.key = @stream_cipher.key = @stream_cipher.random_key
+ else
+ @stream_cipher.key = cipher.send(:key)
+ end
- @stream_cipher.key = key
- @stream_cipher.iv = iv if iv
-
- # Write the Encryption header including the random iv, key, and cipher
- if header
- @ios.write(Cipher.build_header(
- cipher.version,
- compress,
- random_iv ? iv : nil,
- random_key ? key : nil,
- cipher_name))
+ if random_iv
+ header.iv = @stream_cipher.iv = @stream_cipher.random_iv
+ else
+ @stream_cipher.iv = cipher.iv if cipher.iv
end
+
+ @ios.write(header.to_s) if header
+
@size = 0
@closed = false
end
- # Close the IO Stream
- # Flushes any unwritten data
+ # Close the IO Stream.
#
- # Note: Once an EncryptionWriter has been closed a new instance must be
- # created before writing again
+ # Notes:
+ # * Flushes any unwritten data.
+ # * Once an EncryptionWriter has been closed a new instance must be
+ # created before writing again.
+ # * Closes the passed in io stream or file.
+ # * `close` must be called _before_ the supplied stream is closed.
#
- # Note: Also closes the passed in io stream or file
- # Note: This method must be called _before_ the supplied stream is closed
- #
# It is recommended to call Symmetric::EncryptedStream.open
- # rather than creating an instance of Symmetric::EncryptedStream directly to
- # ensure that the encrypted stream is closed before the stream itself is closed
+ # rather than creating an instance of Symmetric::Writer directly to
+ # ensure that the encrypted stream is closed before the stream itself is closed.
def close(close_child_stream = true)
return if closed?
if size > 0
final = @stream_cipher.final
@ios.write(final) if final.length > 0
end
@ios.close if close_child_stream
@closed = true
end
- # Write to the IO Stream as encrypted data
- # Returns the number of bytes written
+ # Write to the IO Stream as encrypted data.
+ #
+ # Returns [Integer] the number of bytes written.
def write(data)
return unless data
bytes = data.to_s
@size += bytes.size
partial = @stream_cipher.update(bytes)
@ios.write(partial) if partial.length > 0
data.length
end
- # Write to the IO Stream as encrypted data
- # Returns self
+ # Write to the IO Stream as encrypted data.
#
+ # Returns [SymmetricEncryption::Writer] self
+ #
# Example:
# file << "Hello.\n" << 'This is Jack'
def <<(data)
write(data)
self
end
- # Flush the output stream
+ # Flush the output stream.
# Does not flush internal buffers since encryption requires all data to
- # be written following the encryption block size
- # Needed by XLS gem
+ # be written following the encryption block size.
+ # Needed by XLS gem.
def flush
@ios.flush
end
+ # Returns [true|false] whether this stream is closed.
def closed?
@closed || @ios.respond_to?(:closed?) && @ios.closed?
end
# Returns [Integer] the number of unencrypted and uncompressed bytes
- # written to the file so far
+ # written to the file so far.
attr_reader :size
end
end