Class: SymmetricEncryption::Cipher

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

Overview

Hold all information related to encryption keys as well as encrypt and decrypt data using those keys

Cipher is thread safe so that the same instance can be called by multiple threads at the same time without needing an instance of Cipher per thread

Defined Under Namespace

Classes: HeaderStruct

Constant Summary collapse

ENCODINGS =

Available encodings

[:none, :base64, :base64strict, :base16]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params = {}) ⇒ Cipher

Create a Symmetric::Key for encryption and decryption purposes

Parameters:

:key [String]
  The Symmetric Key to use for encryption and decryption

:iv [String]
  Optional. The Initialization Vector to use with Symmetric Key
  Highly Recommended as it is the input into the CBC algorithm

:cipher_name [String]
  Optional. Encryption Cipher to use
  Default: aes-256-cbc

:encoding [Symbol]
  :base64strict
    Return as a base64 encoded string that does not include additional newlines
    This is the recommended format since newlines in the values to
    SQL queries are cumbersome. Also the newline reformatting is unnecessary
    It is not the default for backward compatibility
  :base64
    Return as a base64 encoded string
  :base16
    Return as a Hex encoded string
  :none
    Return as raw binary data string. Note: String can contain embedded nulls
  Default: :base64
  Recommended: :base64strict

:version [Fixnum]
  Optional. The version number of this encryption key
  Used by SymmetricEncryption to select the correct key when decrypting data
  Maximum value: 255

:always_add_header [true|false]
  Whether to always include the header when encrypting data.
  ** Highly recommended to set this value to true **
  Increases the length of the encrypted data by a few bytes, but makes
  migration to a new key trivial
  Default: false
  Recommended: true


87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/symmetric_encryption/cipher.rb', line 87

def initialize(params={})
  parms              = params.dup
  @key               = parms.delete(:key)
  @iv                = parms.delete(:iv)
  @cipher_name       = parms.delete(:cipher_name) || parms.delete(:cipher) || 'aes-256-cbc'
  @version           = parms.delete(:version)
  @always_add_header = parms.delete(:always_add_header) || false
  @encoding          = (parms.delete(:encoding) || :base64).to_sym

  raise "Missing mandatory parameter :key" unless @key
  raise "Invalid Encoding: #{@encoding}" unless ENCODINGS.include?(@encoding)
  raise "Cipher version has a valid rage of 0 to 255. #{@version} is too high, or negative" if (@version.to_i > 255) || (@version.to_i < 0)
  parms.each_pair {|k,v| warn "SymmetricEncryption::Cipher Ignoring unknown option #{k.inspect} = #{v.inspect}"}
end

Instance Attribute Details

#always_add_headerObject

Returns the value of attribute always_add_header.



11
12
13
# File 'lib/symmetric_encryption/cipher.rb', line 11

def always_add_header
  @always_add_header
end

#cipher_nameObject (readonly) Also known as: cipher

Cipher to use for encryption and decryption



10
11
12
# File 'lib/symmetric_encryption/cipher.rb', line 10

def cipher_name
  @cipher_name
end

#encodingObject

Returns the value of attribute encoding.



11
12
13
# File 'lib/symmetric_encryption/cipher.rb', line 11

def encoding
  @encoding
end

#ivObject (readonly)

Cipher to use for encryption and decryption



10
11
12
# File 'lib/symmetric_encryption/cipher.rb', line 10

def iv
  @iv
end

#versionObject (readonly)

Cipher to use for encryption and decryption



10
11
12
# File 'lib/symmetric_encryption/cipher.rb', line 10

def version
  @version
end

Class Method Details

.build_header(version, compressed = false, iv = nil, key = nil, cipher_name = nil) ⇒ Object

Returns a magic header for this cipher instance that can be placed at the beginning of a file or stream to indicate how the data was encrypted

Parameters

compressed
  Sets the compressed indicator in the header
  Default: false

iv
  The iv to to put in the header
  Default: nil : Exclude from header

key
  The key to to put in the header
  The key is encrypted using the global encryption key
  Default: nil : Exclude key from header

cipher_name
  Includes the cipher_name used. For example 'aes-256-cbc'
  The cipher_name string to to put in the header
  Default: nil : Exclude cipher_name name from header


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/symmetric_encryption/cipher.rb', line 329

def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil)
  # Ruby V2 named parameters would be perfect here

  # Version number of supplied encryption key, or use the global cipher version if none was supplied
  flags = iv || key ? (SymmetricEncryption.cipher.version || 0) : (version || 0) # Same as 0b0000_0000_0000_0000

  # If the data is to be compressed before being encrypted, set the
  # compressed bit in the flags word
  flags |= 0b1000_0000_0000_0000 if compressed
  flags |= 0b0100_0000_0000_0000 if iv
  flags |= 0b0010_0000_0000_0000 if key
  flags |= 0b0001_0000_0000_0000 if cipher_name
  header = "#{MAGIC_HEADER}#{[flags].pack('v')}".force_encoding(SymmetricEncryption::BINARY_ENCODING)
  if iv
    header << [iv.length].pack('v')
    header << iv
  end
  if key
    encrypted = SymmetricEncryption.cipher.binary_encrypt(key, false, false, false)
    header << [encrypted.length].pack('v').force_encoding(SymmetricEncryption::BINARY_ENCODING)
    header << encrypted
  end
  if cipher_name
    header << [cipher_name.length].pack('v')
    header << cipher_name
  end
  header
end

.has_header?(buffer) ⇒ Boolean

Returns whether the supplied buffer starts with a symmetric_encryption header Note: The encoding of the supplied buffer is forced to binary if not already binary

Returns:

  • (Boolean)


241
242
243
244
245
# File 'lib/symmetric_encryption/cipher.rb', line 241

def self.has_header?(buffer)
  return false if buffer.nil? || (buffer == '')
  buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer.respond_to?(:force_encoding)
  buffer.start_with?(MAGIC_HEADER)
end

.parse_header!(buffer) ⇒ Object

Returns HeaderStruct of the header parsed from the supplied string Returns nil if no header is present

The supplied buffer will be updated directly and its header will be stripped if present

Parameters

buffer
  String to extract the header from


257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/symmetric_encryption/cipher.rb', line 257

def self.parse_header!(buffer)
  return unless has_header?(buffer)

  # Header includes magic header and version byte
  #
  # The encryption header consists of:
  #    4 Byte Magic Header Prefix: @Enc
  #    Followed by 2 Bytes (16 bits)
  #       Bit 0 through 7: The version of the cipher used to encrypt the header
  #       Bit 8 though 10: Reserved
  #       Bit 11: Whether the encrypted data is Binary (otherwise UTF8 text)
  #       Bit 12: Whether the Cipher Name is included
  #       Bit 13: Whether the Key is included
  #       Bit 14: Whether the IV is included
  #       Bit 15: Whether the data is compressed
  #    2 Byte IV Length if included
  #    IV in binary form
  #    2 Byte Key Length if included
  #    Key in binary form
  #    2 Byte Cipher Name Length if included
  #    Cipher name it UTF8 text

  # Remove header and extract flags
  _, flags      = buffer.slice!(0..MAGIC_HEADER_SIZE+1).unpack(MAGIC_HEADER_UNPACK)
  compressed    = (flags & 0b1000_0000_0000_0000) != 0
  include_iv    = (flags & 0b0100_0000_0000_0000) != 0
  include_key   = (flags & 0b0010_0000_0000_0000) != 0
  include_cipher= (flags & 0b0001_0000_0000_0000) != 0
  # Version of the key to use to decrypt the key if present,
  # otherwise to decrypt the data following the header
  version       = flags & 0b0000_0000_1111_1111
  decryption_cipher = SymmetricEncryption.cipher(version)
  raise "Cipher with version:#{version.inspect} not found in any of the configured SymmetricEncryption ciphers" unless decryption_cipher
  iv, key, cipher_name   = nil

  if include_iv
    len = buffer.slice!(0..1).unpack('v').first
    iv  = buffer.slice!(0..len-1)
  end
  if include_key
    len = buffer.slice!(0..1).unpack('v').first
    key = decryption_cipher.binary_decrypt(buffer.slice!(0..len-1), header=false)
  end
  if include_cipher
    len    = buffer.slice!(0..1).unpack('v').first
    cipher_name = buffer.slice!(0..len-1)
  end

  HeaderStruct.new(compressed, iv, key, cipher_name, version, decryption_cipher)
end

.random_key_pair(cipher_name = 'aes-256-cbc') ⇒ Object

Generate a new Symmetric Key pair

Returns a hash containing a new random symmetric_key pair consisting of a :key and :iv. The cipher_name is also included for compatibility with the Cipher initializer



34
35
36
37
38
39
40
41
42
43
# File 'lib/symmetric_encryption/cipher.rb', line 34

def self.random_key_pair(cipher_name = 'aes-256-cbc')
  openssl_cipher = ::OpenSSL::Cipher.new(cipher_name)
  openssl_cipher.encrypt

  {
    key:         openssl_cipher.random_key,
    iv:          openssl_cipher.random_iv,
    cipher_name: cipher_name
  }
end

Instance Method Details

#binary_decrypt(encrypted_string, header = nil) ⇒ Object

Advanced use only See #decrypt to decrypt encoded strings

Returns a Binary decrypted string without decoding the string first The returned string has BINARY encoding

Decryption of supplied string

Returns the decrypted string
Returns nil if encrypted_string is nil
Returns '' if encrypted_string == ''

Parameters

encrypted_string [String]
  Binary encrypted string to decrypt

header [HeaderStruct]
  Optional header for the supplied encrypted_string

Reads the ‘magic’ header if present for key, iv, cipher_name and compression

encrypted_string must be in raw binary form when calling this method

Creates a new OpenSSL::Cipher with every call so that this call is thread-safe and can be called concurrently by multiple threads with the same instance of Cipher

Note:

When a string is encrypted and the header is used, its decrypted form
is automatically set to the same UTF-8 or Binary encoding


425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/symmetric_encryption/cipher.rb', line 425

def binary_decrypt(encrypted_string, header=nil)
  return if encrypted_string.nil?
  str = encrypted_string.to_s
  str.force_encoding(SymmetricEncryption::BINARY_ENCODING) if str.respond_to?(:force_encoding)
  return str if str.empty?

  if header || self.class.has_header?(str)
    str = str.dup
    header ||= self.class.parse_header!(str)

    openssl_cipher = ::OpenSSL::Cipher.new(header.cipher_name || self.cipher_name)
    openssl_cipher.decrypt
    openssl_cipher.key = header.key || @key
    iv = header.iv || @iv
    openssl_cipher.iv = iv if iv
    result = openssl_cipher.update(str)
    result << openssl_cipher.final
    header.compressed ? Zlib::Inflate.inflate(result) : result
  else
    openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
    openssl_cipher.decrypt
    openssl_cipher.key = @key
    openssl_cipher.iv = @iv if @iv
    result = openssl_cipher.update(str)
    result << openssl_cipher.final
  end
end

#binary_encrypt(str, random_iv = false, compress = false, add_header = nil) ⇒ Object

Advanced use only

Returns a Binary encrypted string without applying any Base64, or other encoding

add_header [nil|true|false]
  Whether to add a header to the encrypted string
  If not supplied it defaults to true if always_add_header || random_iv || compress
  Default: nil

Creates a new OpenSSL::Cipher with every call so that this call is thread-safe

See #encrypt to encrypt and encode the result as a string



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/symmetric_encryption/cipher.rb', line 371

def binary_encrypt(str, random_iv=false, compress=false, add_header=nil)
  return if str.nil?
  string = str.to_s
  return string if string.empty?

  # Creates a new OpenSSL::Cipher with every call so that this call
  # is thread-safe
  openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
  openssl_cipher.encrypt
  openssl_cipher.key = @key
  add_header = always_add_header || random_iv || compress if add_header.nil?
  result = if add_header
    # Random iv and compress both add the magic header
    iv = random_iv ? openssl_cipher.random_iv : @iv
    openssl_cipher.iv = iv if iv
    # Set the binary indicator on the header if string is Binary Encoded
    self.class.build_header(version, compress, random_iv ? iv : nil, nil, nil) +
      openssl_cipher.update(compress ? Zlib::Deflate.deflate(string) : string)
  else
    openssl_cipher.iv = @iv if @iv
    openssl_cipher.update(string)
  end
  result << openssl_cipher.final
end

#block_sizeObject

Returns the block size for the configured cipher_name



235
236
237
# File 'lib/symmetric_encryption/cipher.rb', line 235

def block_size
  ::OpenSSL::Cipher::Cipher.new(@cipher_name).block_size
end

#decode(encoded_string) ⇒ Object

Decode the supplied string using the encoding in this cipher instance Note: No encryption or decryption is performed

Returned string is Binary encoded



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/symmetric_encryption/cipher.rb', line 211

def decode(encoded_string)
  return encoded_string if encoded_string.nil? || (encoded_string == '')

  case encoding
  when :base64, :base64strict
    decoded_string = ::Base64.decode64(encoded_string)
    # Support Ruby 1.9 encoding
    defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
  when :base16
    decoded_string = [encoded_string].pack('H*')
    # Support Ruby 1.9 encoding
    defined?(Encoding) ? decoded_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decoded_string
  else
    encoded_string
  end
end

#decrypt(str) ⇒ Object

Decode and Decrypt string

Returns a decrypted string after decoding it first according to the
        encoding setting of this cipher
Returns nil if encrypted_string is nil
Returns '' if encrypted_string == ''

Parameters

encrypted_string [String]
  Binary encrypted string to decrypt

Reads the header if present for key, iv, cipher_name and compression

encrypted_string must be in raw binary form when calling this method

Creates a new OpenSSL::Cipher with every call so that this call is thread-safe and can be called concurrently by multiple threads with the same instance of Cipher



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/symmetric_encryption/cipher.rb', line 163

def decrypt(str)
  decoded = self.decode(str)
  return unless decoded

  return decoded if decoded.empty?
  decrypted = binary_decrypt(decoded)
  if defined?(Encoding)
    # Try to force result to UTF-8 encoding, but if it is not valid, force it back to Binary
    unless decrypted.force_encoding(SymmetricEncryption::UTF8_ENCODING).valid_encoding?
      decrypted.force_encoding(SymmetricEncryption::BINARY_ENCODING)
    end
  end
  decrypted
end

#encode(binary_string) ⇒ Object

Returns UTF8 encoded string after encoding the supplied Binary string

Encode the supplied string using the encoding in this cipher instance Returns nil if the supplied string is nil Note: No encryption or decryption is performed

Returned string is UTF8 encoded except for encoding :none



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/symmetric_encryption/cipher.rb', line 185

def encode(binary_string)
  return binary_string if binary_string.nil? || (binary_string == '')

  # Now encode data based on encoding setting
  case encoding
  when :base64
    encoded_string = ::Base64.encode64(binary_string)
    # Support Ruby 1.9 encoding
    defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
  when :base64strict
    encoded_string = ::Base64.encode64(binary_string).gsub(/\n/, '')
    # Support Ruby 1.9 encoding
    defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
  when :base16
    encoded_string = binary_string.to_s.unpack('H*').first
    # Support Ruby 1.9 encoding
    defined?(Encoding) ? encoded_string.force_encoding(SymmetricEncryption::UTF8_ENCODING) : encoded_string
  else
    binary_string
  end
end

#encrypt(str, random_iv = false, compress = false) ⇒ Object

Encrypt and then encode a string

Returns data encrypted and then encoded according to the encoding setting

of this cipher

Returns nil if str is nil Returns “” str is empty

Parameters

str [String]
  String to be encrypted. If str is not a string, #to_s will be called on it
  to convert it to a string

random_iv [true|false]
  Whether the encypted value should use a random IV every time the
  field is encrypted.
  It is recommended to set this to true where feasible. If the encrypted
  value could be used as part of a SQL where clause, or as part
  of any lookup, then it must be false.
  Setting random_iv to true will result in a different encrypted output for
  the same input string.
  Note: Only set to true if the field will never be used as part of
    the where clause in an SQL query.
  Note: When random_iv is true it will add a 8 byte header, plus the bytes
    to store the random IV in every returned encrypted string, prior to the
    encoding if any.
  Default: false
  Highly Recommended where feasible: true

compress [true|false]
  Whether to compress str before encryption
  Should only be used for large strings since compression overhead and
  the overhead of adding the encryption header may exceed any benefits of
  compression
  Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
  Default: false


138
139
140
141
142
143
144
# File 'lib/symmetric_encryption/cipher.rb', line 138

def encrypt(str, random_iv=false, compress=false)
  return if str.nil?
  str = str.to_s
  return str if str.empty?
  encrypted = binary_encrypt(str, random_iv, compress)
  self.encode(encrypted)
end

#inspectObject

Returns [String] object represented as a string, filtering out the key



454
455
456
# File 'lib/symmetric_encryption/cipher.rb', line 454

def inspect
  "#<#{self.class}:0x#{self.__id__.to_s(16)} @key=\"[FILTERED]\" @iv=#{iv.inspect} @cipher_name=#{cipher_name.inspect}, @version=#{version.inspect}, @encoding=#{encoding.inspect}, @always_add_header=#{always_add_header.inspect}"
end

#random_keyObject

Return a new random key using the configured cipher_name Useful for generating new symmetric keys



230
231
232
# File 'lib/symmetric_encryption/cipher.rb', line 230

def random_key
  ::OpenSSL::Cipher::Cipher.new(@cipher_name).random_key
end