Class: SymmetricEncryption::Writer

Inherits:
Object
  • Object
show all
Defined in:
lib/symmetric_encryption/writer.rb

Overview

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.

  • 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.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ios, version: nil, cipher_name: nil, header: true, random_key: true, random_iv: true, compress: false) ⇒ Writer

Encrypt data before writing to the supplied stream

Raises:

  • (ArgumentError)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/symmetric_encryption/writer.rb', line 97

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
  @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

  # 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
  header = Header.new(version: cipher.version, compress: compress, cipher_name: cipher_name) if (header == true) || compress || random_key || random_iv

  @stream_cipher = ::OpenSSL::Cipher.new(cipher_name || cipher.cipher_name)
  @stream_cipher.encrypt

  if random_key
    header.key = @stream_cipher.key = @stream_cipher.random_key
  else
    @stream_cipher.key = cipher.send(:key)
  end

  if random_iv
    header.iv = @stream_cipher.iv = @stream_cipher.random_iv
  elsif cipher.iv
    @stream_cipher.iv = cipher.iv
  end

  @ios.write(header.to_s) if header

  @size   = 0
  @closed = false
end

Instance Attribute Details

#sizeObject (readonly)

Returns [Integer] the number of unencrypted and uncompressed bytes written to the file so far.



205
206
207
# File 'lib/symmetric_encryption/writer.rb', line 205

def size
  @size
end

Class Method Details

.encrypt(source:, target:, **args) ⇒ Object

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

Notes:

  • The file contents are streamed so that the entire file is not loaded into memory.



92
93
94
# File 'lib/symmetric_encryption/writer.rb', line 92

def self.encrypt(source:, target:, **args)
  Writer.open(target, **args) { |output_file| IO.copy_stream(source, output_file) }
end

.open(file_name_or_stream, compress: nil, **args) ⇒ Object

Open a file for writing, or use the supplied IO Stream.

Parameters:

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.

compress: [true|false]
  Uses Zlib to compress the data before it is encrypted and
  written to the file/stream.
  Default: true, unless the file_name extension indicates it is already compressed.

Note: Compression occurs before encryption

# Example: Encrypt and write data to a 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.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.enc'), row_sep: "\n")
  csv << [1,2,3,4,5]
ensure
  csv.close if csv
end


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/symmetric_encryption/writer.rb', line 50

def self.open(file_name_or_stream, compress: nil, **args)
  if file_name_or_stream.is_a?(String)
    file_name_or_stream = ::File.open(file_name_or_stream, 'wb')
    compress            = !(/\.(zip|gz|gzip|xls.|)\z/i === file_name_or_stream) if compress.nil?
  else
    compress = true if compress.nil?
  end

  begin
    file = new(file_name_or_stream, compress: compress, **args)
    file = Zlib::GzipWriter.new(file) if compress
    block_given? ? yield(file) : file
  ensure
    file.close if block_given? && file && (file.respond_to?(:closed?) && !file.closed?)
  end
end

.write(file_name_or_stream, data, **args) ⇒ Object

Write the contents of a string in memory to an encrypted file / stream.

Notes:

  • Do not use this method for writing large files.



71
72
73
# File 'lib/symmetric_encryption/writer.rb', line 71

def self.write(file_name_or_stream, data, **args)
  Writer.open(file_name_or_stream, **args) { |f| f.write(data) }
end

Instance Method Details

#<<(data) ⇒ Object

Write to the IO Stream as encrypted data.

Returns [SymmetricEncryption::Writer] self

Example:

file << "Hello.\n" << 'This is Jack'


185
186
187
188
# File 'lib/symmetric_encryption/writer.rb', line 185

def <<(data)
  write(data)
  self
end

#close(close_child_stream = true) ⇒ Object

Close the IO Stream.

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.

It is recommended to call Symmetric::EncryptedStream.open rather than creating an instance of Symmetric::Writer directly to ensure that the encrypted stream is closed before the stream itself is closed.



143
144
145
146
147
148
149
150
151
152
# File 'lib/symmetric_encryption/writer.rb', line 143

def close(close_child_stream = true)
  return if closed?

  if size.positive?
    final = @stream_cipher.final
    @ios.write(final) unless final.empty?
  end
  @ios.close if close_child_stream
  @closed = true
end

#closed?Boolean

Returns [true|false] whether this stream is closed.

Returns:

  • (Boolean)


199
200
201
# File 'lib/symmetric_encryption/writer.rb', line 199

def closed?
  @closed || @ios.respond_to?(:closed?) && @ios.closed?
end

#flushObject

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.


194
195
196
# File 'lib/symmetric_encryption/writer.rb', line 194

def flush
  @ios.flush
end

#write(data) ⇒ Object



158
159
160
161
162
163
164
165
166
# File 'lib/symmetric_encryption/writer.rb', line 158

def write(data)
  return unless data

  bytes   = data.to_s
  @size  += bytes.size
  partial = @stream_cipher.update(bytes)
  @ios.write(partial) unless partial.empty?
  data.length
end