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

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(parms = {}) ⇒ 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


68
69
70
71
72
73
74
75
76
77
# File 'lib/symmetric_encryption/cipher.rb', line 68

def initialize(parms={})
  raise "Missing mandatory parameter :key" unless @key = parms[:key]
  @iv = parms[:iv]
  @cipher_name = parms[:cipher_name] || parms[:cipher] || 'aes-256-cbc'
  @version = parms[:version]
  raise "Cipher version has a maximum of 255. #{@version} is too high" if @version.to_i > 255
  @encoding = (parms[:encoding] || :base64).to_sym

  raise("Invalid Encoding: #{@encoding}") unless ENCODINGS.include?(@encoding)
end

Instance Attribute Details

#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

#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

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


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

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

  # Encryption version indicator if available
  flags = version || 0 # Same as 0b0000_0000_0000_0000

  # Replace version with global cipher that will be used to encrypt the random key
  if iv || key
    flags = (SymmetricEncryption.cipher.version || 0)
  end

  # 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)
    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

.parse_magic_header!(buffer, default_version = nil, default_compressed = false) ⇒ Object

Returns an Array of the following values extracted from header or nil if any value was not specified in the header

compressed [true|false]
iv [String]
key [String]
cipher_name [String}
decryption_cipher [SymmetricEncryption::Cipher]

The supplied buffer will be updated directly and will have the header portion removed

Parameters

buffer
  String to extract the header from if present

default_version
  If no header is present, this is the default value for the version
  of the cipher to use

default_compressed
  If no header is present, this is the default value for the compression


217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/symmetric_encryption/cipher.rb', line 217

def self.parse_magic_header!(buffer, default_version=nil, default_compressed=false)
  buffer.force_encoding(SymmetricEncryption::BINARY_ENCODING) if buffer
  return [default_compressed, nil, nil, nil, nil, SymmetricEncryption.cipher(default_version)] unless buffer && buffer.start_with?(MAGIC_HEADER)

  # Header includes magic header and version byte
  # 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))
  end
  if include_cipher
    len    = buffer.slice!(0..1).unpack('v').first
    cipher_name = buffer.slice!(0..len-1)
  end

  [compressed, iv, key, cipher_name, version, decryption_cipher]
end

.random_key_pair(cipher_name = 'aes-256-cbc', generate_iv = true) ⇒ 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



24
25
26
27
28
29
30
31
32
33
# File 'lib/symmetric_encryption/cipher.rb', line 24

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

  {
    :key         => openssl_cipher.random_key,
    :iv          => generate_iv ? openssl_cipher.random_iv : nil,
    :cipher_name => cipher_name
  }
end

Instance Method Details

#binary_decrypt(encrypted_string) ⇒ Object

Advanced use only

Returns a Binary decrypted string without decoding the string first

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

See #decrypt to decrypt encoded strings



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/symmetric_encryption/cipher.rb', line 345

def binary_decrypt(encrypted_string)
  str = encrypted_string.to_s
  if str.start_with?(MAGIC_HEADER)
    str = str.dup
    compressed, iv, key, cipher_name = self.class.parse_magic_header!(str)
    openssl_cipher = ::OpenSSL::Cipher.new(cipher_name || self.cipher_name)
    openssl_cipher.decrypt
    openssl_cipher.key = key || @key
    iv ||= @iv
    openssl_cipher.iv = iv if iv
    result = openssl_cipher.update(str)
    result << openssl_cipher.final
    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(encrypted_string)
    result << openssl_cipher.final
  end
end

#binary_encrypt(string, random_iv = false, compress = false) ⇒ Object

Advanced use only

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

Adds the 'magic' header if a random_iv is required or compression is enabled

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



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/symmetric_encryption/cipher.rb', line 316

def binary_encrypt(string, random_iv=false, compress=false)
  openssl_cipher = ::OpenSSL::Cipher.new(self.cipher_name)
  openssl_cipher.encrypt
  openssl_cipher.key = @key
  result = if random_iv || compress
    # Random iv and compress both add the magic header
    iv = random_iv ? openssl_cipher.random_iv : @iv
    openssl_cipher.iv = iv if iv
    self.class.magic_header(version, compress, random_iv ? iv : 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



152
153
154
# File 'lib/symmetric_encryption/cipher.rb', line 152

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



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/symmetric_encryption/cipher.rb', line 183

def decode(encoded_string)
  return unless encoded_string

  case encoding
  when :base64, :base64strict
    ::Base64.decode64(encoded_string).force_encoding(SymmetricEncryption::BINARY_ENCODING)
  when :base16
    [encoded_string].pack('H*').force_encoding(SymmetricEncryption::BINARY_ENCODING)
  else
    encoded_string
  end
end

#decrypt(str) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/symmetric_encryption/cipher.rb', line 128

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



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

def encode(binary_string)
  return unless binary_string

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

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

Returns encrypted and then encoded string 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 'magic' header may exceed any benefits of
  compression
  Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
  Default: false


112
113
114
115
116
117
118
# File 'lib/symmetric_encryption/cipher.rb', line 112

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 Excluding the key and iv



370
371
372
# File 'lib/symmetric_encryption/cipher.rb', line 370

def inspect
   "#<#{self.class}:0x#{self.__id__.to_s(16)} @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



147
148
149
# File 'lib/symmetric_encryption/cipher.rb', line 147

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