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


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

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, binary = false) ⇒ 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

binary
  Whether the data being encrypted is binary.
  When the header is read, it sets the encoding of the string returned to Binary


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/symmetric_encryption/cipher.rb', line 335

def self.build_header(version, compressed=false, iv=nil, key=nil, cipher_name=nil, binary=false)
  # 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
  flags |= 0b0000_1000_0000_0000 if binary
  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)


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

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


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
307
308
# File 'lib/symmetric_encryption/cipher.rb', line 258

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
  binary        = (flags & 0b0000_1000_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, binary=true)
  end
  if include_cipher
    len    = buffer.slice!(0..1).unpack('v').first
    cipher_name = buffer.slice!(0..len-1)
  end

  HeaderStruct.new(compressed, binary, 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



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

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, binary = false) ⇒ Object

Advanced use only See #decrypt to decrypt encoded strings

Returns a Binary decrypted string without decoding the string first

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

binary [true|false]
  If no header is supplied then determines whether the string returned
  is binary or UTF8

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


436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/symmetric_encryption/cipher.rb', line 436

def binary_decrypt(encrypted_string, header=nil, binary=false)
  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?

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

    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

  # Support Ruby 1.9 and above Encoding
  if defined?(Encoding)
    # Sets the encoding of the result string to UTF8 or BINARY based on the binary header
    binary ? decrypted_string.force_encoding(SymmetricEncryption::BINARY_ENCODING) : decrypted_string.force_encoding(SymmetricEncryption::UTF8_ENCODING)
  else
    decrypted_string
  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



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/symmetric_encryption/cipher.rb', line 378

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
    binary = (string.encoding == SymmetricEncryption::BINARY_ENCODING)
    self.class.build_header(version, compress, random_iv ? iv : nil, binary) +
      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



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

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



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

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

header [HeaderStruct]
  Optional header for the supplied encrypted_string

binary [true|false]
  If no header is supplied then determines whether the string returned
  is binary or UTF8

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



171
172
173
174
175
176
177
# File 'lib/symmetric_encryption/cipher.rb', line 171

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

  return decoded if decoded.empty?
  binary_decrypt(decoded).force_encoding(SymmetricEncryption::UTF8_ENCODING)
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



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

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 binary or UTF-8 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


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

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



474
475
476
# File 'lib/symmetric_encryption/cipher.rb', line 474

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}"
end

#random_keyObject

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



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

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