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
130
131
132
133
134
135
136
# 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
  if cipher_name && !random_key && !random_iv
    raise(ArgumentError, "Cannot supply a :cipher_name unless both :random_key and :random_iv are true")
  end

  # Cipher to encrypt the random_key, or the entire file
  cipher = SymmetricEncryption.cipher(version)
  unless cipher
    raise(SymmetricEncryption::CipherError,
          "Cipher with version:#{version} not found in any of the configured SymmetricEncryption ciphers")
  end

  # 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

  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.



212
213
214
# File 'lib/symmetric_encryption/writer.rb', line 212

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?
  elsif compress.nil?
    compress = true
  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'


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

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.



150
151
152
153
154
155
156
157
158
159
# File 'lib/symmetric_encryption/writer.rb', line 150

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)


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

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.


201
202
203
# File 'lib/symmetric_encryption/writer.rb', line 201

def flush
  @ios.flush
end

#write(data) ⇒ Object



165
166
167
168
169
170
171
172
173
# File 'lib/symmetric_encryption/writer.rb', line 165

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