Class: SafeDb::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/utils/keys/key.rb

Overview

First use the class methods to source keys, then use a key’s instance methods to access its properties and in concert with other symmetrical information, you can use the keys to lock (encrypt) or unlock (decrypt) other keys and objecs.

Sourcing and Deriving Keys

Keys can be

  • sourced from a secure random byte generating function

  • sourced from ciphertext and another (decryption) key

  • generated by passing a secret through key derivation functions

  • regenerated from a secret and previously stored salts

  • sourced from the current unique workstation shell environment

  • sourced from an environment variable containing ciphertext

Keys need to be viewed (represented) in multiple ways and the essence of the key viewer is to input keys as_bits, as_bytes and as_base64 and then output the same key (in as far as is possible) - as bits, as bytes and as base64.

Key | To and From Behaviour

Use the From methods to create Keys from a variety of resources such as

  • a base64 encoded string

  • a binary byte string

  • a string of one and zero bits

  • a hexadecimal representation

Once you have instantiated the key, you will then be able to convert it (within reason due to bit, byte and base64 lengths) to any of the above key representations.

Key | Bits Bytes and Base64

The shoe doesn’t always fit when its on the other foot and this is best illustratd with a table that maps bits to 8 bit bytes and 6 bit Base64 characters.

| --------- | -------- | ------------ | ------------------------------- |
| Fit?      | Bits     | Bytes        | (and) Base64                    |
| --------- | -------- | ------------ | ------------------------------- |
| Perfect   | 168 Bits | is 21 bytes  | 28 Chars - bcrypt chops to this |
| Perfect   | 216 Bits | is 27 bytes  | 36 Chars -                      |
| Perfect   | 264 Bits | is 33 bytes  | 44 Chars - holder 4 256bit keys |
| Perfect   | 384 Bits | is 48 bytes  | 64 Chars - 216 + 168 equals 384 |
| --------- | -------- | ------------ | ------------------------------- |
| Imperfect | 128 Bits | 16 precisely | 22 Chars - 21 + 2 remain bits   |
| Imperfect | 186 Bits | 23 remain 2  | 31 Characers precisely          |
| Imperfect | 256 Bits | 32 precisely | 43 Chars - 42 + 4 remain bits   |
| --------- | -------- | ------------ | ------------------------------- |

Yes, the shoe doesn’t always fit when it’s on the other foot.

Schoolboy Error

The strategy is so simple, we call it a schoolboy error.

If we want to use a key with n bits and either n % 6 or n % 8 (or both) are not zero - we instantiate a Key with the lowest common denominator of 6 and 8 that exceeds n.

So when we request a byte, or base64 representation the viewer will truncate (not round down) to the desired length.

Mapping Each Character to 6 Binary Bits

We need 6 binary bits to represent a base64 character (and 4 bits for hexadecimal). Here is an example mapping between a base 64 character, an integer and the six bit binary.

Character   Integer  Binary (6 Bit)

   a           0        000000
   b           1        000001
   c           2        000010

   y           25       011001
   z           26       011010
   A           27       011011
   B           28       011100

   8           60       111100
   9           61       111101
   /           62       111110
   +           63       111111

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(the_bit_string) ⇒ Key

Initialize a key object from a bit string of ones and zeroes provided in the parameter string.

For example a string of 384 bits (ones and zeroes) can be thought of as a 48 byte key which can also be represented with 64 more compact base64 characters.

| -------- | ------------ | -------------------------------- |
| Bits     | Bytes        | Base64                           |
| -------- | ------------ | -------------------------------- |
| 384 Bits | is 48 bytes  | and 64 characters                |
| -------- | ------------ | -------------------------------- |

Parameters:

  • the_bit_string (String)

    the bit string of ones and zeroes that represents the bits that represent this key



113
114
115
# File 'lib/utils/keys/key.rb', line 113

def initialize the_bit_string
  @bit_string = the_bit_string
end

Class Method Details

.from_binary(binary_text) ⇒ SafeDb::Key

Return a key represented by the parameter binary string.

Parameters:

  • binary_text (String)

    The binary string that the returned key will be instantiated from.

Returns:

  • (SafeDb::Key)

    return a key from the binary byte string parameter



193
194
195
196
# File 'lib/utils/keys/key.rb', line 193

def self.from_binary binary_text
  ones_and_zeroes = binary_text.unpack("B*")[0]
  return Key.new( ones_and_zeroes )
end

.from_char64(char64_string) ⇒ SafeDb::Key

Return the key represented by the parameter sequence of base64 characters.

Parameters:

  • char64_string (String)

    The base64 character sequence which the returned key is instantiated from. Naturally this character sequencee cannot be nil, nor can it contain any characters that are not present in SafeDb::Key64::YACHT64_CHARACTER_SET.

    Ideally the number of parameter characters multiplied by 6 should be a multiple of eight (8) otherwise the new key’s bit string will require padding and extension.

Returns:

  • (SafeDb::Key)

    return a key from the parameter sequence of base64 characters.

Raises:

  • (ArgumentError)

    If a nil or zero length byte array is received. Or if the number of bytes multiplied by 8 is not a multiple of 6.



180
181
182
# File 'lib/utils/keys/key.rb', line 180

def self.from_char64 char64_string
  return Key.new( Key64.to_bits( char64_string ) )
end

.from_radix64(radix64_string) ⇒ SafeDb::Key

Convert a string of Radix64 characters into a key.

This method converts the base64 string into the internal YACHT64 format and then converts that into a bit string so that a key can be instantiated.

Parameters:

  • radix64_string (String)

    the radix64 string to convert into akey. This string will be a subset of the usual 62 character suspects together with period and forward slash characters.

    This parameter should not contain newlines nor carriage returns.

Returns:

  • (SafeDb::Key)

    return a key from the parameter sequence of base64 characters.

Raises:

  • (ArgumentError)

    If a nil or zero length parameter array is received.



216
217
218
# File 'lib/utils/keys/key.rb', line 216

def self.from_radix64 radix64_string
  return Key.new( Key64.from_radix64_to_bits( radix64_string ) )
end

.from_randomSafeDb::Key

Return a (secure) randomly generated super high entropy 384 bit key that can be stored with 64 base64 characters and used to source digest functions that can unreversibly convert the key to a 256 bit symmetric encryption key.

| -------- | ------------ | -------------------------------- |
| Bits     | Bytes        | Base64                           |
| -------- | ------------ | -------------------------------- |
| 384 Bits | is 48 bytes  | and 64 characters                |
| -------- | ------------ | -------------------------------- |

This key easily translates to a base64 and/or byte array format because the 384 bit count is a multiple of both 6 and 8.

Returns:

  • (SafeDb::Key)

    return a key containing 384 random bits (or a random array of 48 bytes) which can if necessary be serialized into 64 base64 characters.

Raises:

  • (ArgumentError)

    If a nil or zero length byte array is received. Or if the number of bytes multiplied by 8 is not a multiple of 6.



140
141
142
# File 'lib/utils/keys/key.rb', line 140

def self.from_random
  return Key.new( to_random_bits( RANDOM_KEY_BYTE_LENGTH ) )
end

.to_binary_from_bit_string(bit_string_to_convert) ⇒ Byte

Return the un-printable binary bytes representation of this key. If you store 128 bits it will produce 22 characters because 128 divide by 6 is 21 characters and a remainder of two (2) bits.

The re-conversion of the 22 characters will now produce 132 bits which is different from the original 128 bits.

Returns:

  • (Byte)

    a non-printable binary string of eight (8) bit bytes which can be used as input to both digest and symmetric cipher functions.



311
312
313
# File 'lib/utils/keys/key.rb', line 311

def self.to_binary_from_bit_string bit_string_to_convert
  return [ bit_string_to_convert ].pack("B*")
end

.to_random_bits(the_byte_length) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/utils/keys/key.rb', line 145

def self.to_random_bits the_byte_length

  require 'securerandom'

  random_bit_string = ""
  for n in 1 .. the_byte_length
    random_integer = SecureRandom.random_number( EIGHT_BIT_INTEGER_SIZE )
    random_bit_string += "%08d" % [ random_integer.to_s(2) ]
  end
  return random_bit_string

end

Instance Method Details

#can_decrypt_key(key_iv_ciphertext) ⇒ Boolean

Return true if this key object can decrypt the parameter ciphertext that represents a key.

The parameter cipher text must comprise of an 80 character key and a 16 character random initialization vector (IV). When transformed to a bit string of ones and zeroes, the bit string length must be 96 * 8 = 768.

Parameters:

  • key_iv_ciphertext (String)

    Provide the ciphertext produced by our sister key encryption method. The ciphertext should hold 96 bytes which equates to 128 base64 characters. The random initialization vector (iv) accounts for the first 16 bytes. The actual crypt ciphertext then accounts for the final 80 bytes.

Returns:

  • (Boolean)

    return true if the ciphertext is of the expected length and the dry run of the decrypt operation completes without a cipher error from OpenSSL::Cipher::CipherError being thrown.



499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File 'lib/utils/keys/key.rb', line 499

def can_decrypt_key( key_iv_ciphertext )

  bit_text = Key64.to_bits( key_iv_ciphertext )
  ciphertext_size_msg = "Expected bit length of #{EXPECTED_CIPHER_BIT_LENGTH} not #{bit_text.length}."
  is_expected_length = bit_text.length == EXPECTED_CIPHER_BIT_LENGTH
  log.warn(x) { ciphertext_size_msg } unless is_expected_length
  return false unless is_expected_length

  the_cipher = OpenSSL::Cipher::AES256.new(:CBC)
  the_cipher.decrypt()
  rawbytes = [ bit_text ].pack("B*")
  the_cipher.key = to_aes_key()
  the_cipher.iv  = rawbytes[ 0 .. ( RANDOM_IV_BYTE_COUNT - 1 ) ]

  begin
    the_cipher.update( rawbytes[ RANDOM_IV_BYTE_COUNT .. -1 ] ) + the_cipher.final
    return true
  rescue OpenSSL::Cipher::CipherError => e
    log.warn(x) { "Auth failure decrypting this ciphertext ~~ #{key_iv_ciphertext}" }
    log.warn(x) { "Auth (maybe login failure) error message is ~~ #{e.message}" }
    return false
  end

end

#do_decrypt_key(ciphertext_to_decrypt) ⇒ Key

Use the OpenSSL::Cipher::AES256 block cipher in CBC mode and the binary 256bit representation of this key to decrypt the parameter ciphertext and return the previously encrypted key.

To re-acquire (reconstitute) the original key call this method with the stored ciphertext that was returned by the #do_encrypt_key.

Only Encrypt Strong Keys

Never encrypt a potentially weak key, like one derived from a human password (even though it is put through key derivation functions).

Once generated (or regenerated) a potentially weak key should live only as long as it takes for it to encrypt a strong key. The strong key can then be used to encrypt valuable assets.

Enforcing Strong Key Size

If one key is potentially weaker than the other, the weaker key must be this object and the strong key is reconstituted and returned by this method.

Parameters:

  • ciphertext_to_decrypt (String)

    Provide the ciphertext produced by our sister key encryption method. The ciphertext should hold 96 bytes which equates to 128 base64 characters. The random initialization vector (iv) accounts for the first 16 bytes. The actual crypt ciphertext then accounts for the final 80 bytes.

Returns:

  • (Key)

    return the key that was serialized into base64 and then encrypted (locked down) with the 256 bit binary symmetric encryption key from this host object.

Raises:

  • (ArgumentError)

    the size of the parameter ciphertext must be 128 base 64 characters.



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'lib/utils/keys/key.rb', line 461

def do_decrypt_key ciphertext_to_decrypt

  bit_text = Key64.to_bits(ciphertext_to_decrypt)
  size_msg = "Expected bit count is #{EXPECTED_CIPHER_BIT_LENGTH} not #{bit_text.length}."
  raise RuntimeError, size_msg unless bit_text.length == EXPECTED_CIPHER_BIT_LENGTH

  cipher_x = OpenSSL::Cipher::AES256.new(:CBC)
  cipher_x.decrypt()

  rawbytes = [ bit_text ].pack("B*")

  cipher_x.key = to_aes_key()
  cipher_x.iv  = rawbytes[ 0 .. ( RANDOM_IV_BYTE_COUNT - 1 ) ]
  key_chars_64 = cipher_x.update( rawbytes[ RANDOM_IV_BYTE_COUNT .. -1 ] ) + cipher_x.final

  return Key.from_char64( key_chars_64 )

end

#do_decrypt_text(random_iv, cipher_text) ⇒ String

Use the OpenSSL::Cipher::AES256 block cipher in CBC mode and the binary 256bit representation of this key to decrypt the parameter ciphertext using the parameter random initialization vector.

Use this method to re-acquire (reconstitute) the plaintext that was converted to ciphertext by the #do_encrypt_text encryption method, naturally using the same initialization vector for both calls.

Only Decrypt Once

Consider a key spent as soon as it decrypts the one object it was created to decrypt. Like a bee dying after a sting, a key should die after it decrypts an object. Should re-decryption be necessary - another key should be derived or generated.

Parameters:

  • random_iv (String)

    a randomly generated 16 byte binary string that is to be used as the initialization vector (IV) - this is a requirement for AES decryption in CBC mode - this IV does not need to be treated as a secret

  • cipher_text (String)

    the ciphertext or binary string to be decrypted in order to re-acquire (reconstitute) the plaintext that was converted to ciphertext by the #do_encrypt_text encryption method.

Returns:

  • (String)

    if the plaintext (or binary string) returned here still needs to be kept on the low, derive or generate another key to protect it.

Raises:

  • (ArgumentError)


595
596
597
598
599
600
601
602
603
604
605
606
607
# File 'lib/utils/keys/key.rb', line 595

def do_decrypt_text random_iv, cipher_text

  raise ArgumentError, "Incoming cipher text cannot be nil." if cipher_text.nil?

  crypt_cipher = OpenSSL::Cipher::AES256.new(:CBC)

  crypt_cipher.decrypt()
  crypt_cipher.iv  = random_iv
  crypt_cipher.key = to_aes_key()

  return crypt_cipher.update( cipher_text ) + crypt_cipher.final

end

#do_encrypt_key(key_to_encrypt) ⇒ String

Use the OpenSSL::Cipher::AES256 block cipher in CBC mode and the binary 256bit representation of this key to encrypt the parameter key.

Store the ciphertext provided by this method. To re-acquire (reconstitute) the parameter key use the #do_decrypt_key decryption method with the ciphertext produced here.

Only Encrypt Strong Keys

Never encrypt a potentially weak key, like one derived from a human password (even though it is put through key derivation functions).

Once generated (or regenerated) a potentially weak key should live only as long as it takes for it to encrypt a strong key. The strong key can then be used to encrypt valuable assets.

Enforcing Strong Key Size

If one key is potentially weaker than the other, the weaker key must be this object and the strong key is the parameter key.

This method thus enforces the size of the strong key. A strong key has 384 bits of entropy, and is represented by 64 base64 characters.

Parameters:

  • key_to_encrypt (SafeDb::Key)

    this is the key that will first be serialized into base64 and then locked down using the 256 bit binary string from this host object as the symmetric encryption key.

    This method is sensitive to the size of the parameter key and expects to encrypt exactly 64 base64 characters within the parameter key.

Returns:

  • (String)

    The returned ciphertext should be stored. Its breakdown is as follows. 96 bytes are returned which equates to 128 base64 characters. The random initialization vector (iv) accounts for the first 16 bytes. The actual crypt ciphertext then accounts for the final 80 bytes.

Raises:

  • (ArgumentError)

    the size of the parameter (strong) key is enforced to ensure that it has exactly 384 bits of entropy which are represented by 64 base64 characters.



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/utils/keys/key.rb', line 401

def do_encrypt_key key_to_encrypt

  crypt_cipher = OpenSSL::Cipher::AES256.new(:CBC)

  crypt_cipher.encrypt()
  random_iv = crypt_cipher.random_iv()
  crypt_cipher.key = to_aes_key()

  calling_module = File.basename caller_locations(1,1).first.absolute_path, ".rb"
  calling_method = caller_locations(1,1).first.base_label
  calling_lineno = caller_locations(1,1).first.lineno
  caller_details = "#{calling_module} | #{calling_method} | (line #{calling_lineno})"

  cipher_text = crypt_cipher.update( key_to_encrypt.to_char64 ) + crypt_cipher.final

  binary_text = random_iv + cipher_text
  ones_zeroes = binary_text.unpack("B*")[0]
  ciphertxt64 = Key64.from_bits( ones_zeroes )

  size_msg = "Expected bit count is #{EXPECTED_CIPHER_BIT_LENGTH} not #{ones_zeroes.length}."
  raise RuntimeError, size_msg unless ones_zeroes.length == EXPECTED_CIPHER_BIT_LENGTH

  return ciphertxt64

end

#do_encrypt_text(random_iv, plain_text) ⇒ String

Use the OpenSSL::Cipher::AES256 block cipher in CBC mode and the binary 256bit representation of this key to encrypt the parameter plaintext using the parameter random initialization vector.

Store the ciphertext provided by this method. To re-acquire (reconstitute) the plaintext use the #do_decrypt_text decryption method, giving it the same initialization vector and the ciphertext produced here.

Only Encrypt Once

Despite the initialization vector protecting against switch attacks you should only use this or any other key once to encrypt an object. While it is okay to encrypt small targets using two different keys, it pays not to do the same when the target is large.

Parameters:

  • random_iv (String)

    a randomly generated 16 byte binary string that is to be used as the initialization vector (IV) - this is a requirement for AES encryption in CBC mode - this IV does not need to be treated as a secret

  • plain_text (String)

    the plaintext or binary string to be encrypted. To re-acquire this string use the #do_decrypt_text decryption method, giving it the same initialization vector (provided in the first parameter) and the ciphertext returned from this method.

Returns:

  • (String)

    The returned binary ciphertext should be encoded and persisted until such a time as its re-acquisition by authorized parties becomes necessary.



554
555
556
557
558
559
560
561
562
563
564
# File 'lib/utils/keys/key.rb', line 554

def do_encrypt_text random_iv, plain_text

  crypt_cipher = OpenSSL::Cipher::AES256.new(:CBC)

  crypt_cipher.encrypt()
  crypt_cipher.iv  = random_iv
  crypt_cipher.key = to_aes_key()

  return crypt_cipher.update( plain_text ) + crypt_cipher.final

end

#to_384_bit_keySafeDb::Key

This method uses the SHA384 digest to convert this key’s binary representation into another (newly instantiated) key whose size is precisely 384 bits.

If you take the returned key and call

  • #to_char64 you get a 64 character base64 string

  • #to_s you get a string of 384 ones and zeroes

  • #to_binary you get a 48 byte binary string

Returns:

  • (SafeDb::Key)

    a key with a bit length (ones and zeroes) of precisely 384.

Raises:

  • (RuntimeError)


347
348
349
350
351
352
353
354
355
356
357
# File 'lib/utils/keys/key.rb', line 347

def to_384_bit_key

  a_384_bit_key = Key.from_binary( Digest::SHA384.digest( to_binary() ) )

  has_384_chars = a_384_bit_key.to_s.length == 384
  err_msg = "Digested key length is #{a_384_bit_key.to_s.length} instead of 384."
  raise RuntimeError, err_msg unless has_384_chars

  return a_384_bit_key

end

#to_aes_keyString

This method uses digests to convert the key’s binary representation (which is either 48 bytes for purely random keys or 64 bytes for keys derived from human sourced secrets) into a key whose size is ideal for plying the ubiquitous AES256 symmetric encryption algorithm.

This method should only ever be called when this key has been derived from either a (huge) 48 byte random source or from a key derivation function (KDF) such as BCrypt, SCrypt, PBKDF2 or a union from which the 512 bit (64 byte) key can be reduced to 256 bits.

Returns:

  • (String)

    a binary string of thirty-two (32) eight (8) bit bytes which if appropriate can be used as a symmetric encryption key especially to the powerful AES256 cipher.



330
331
332
# File 'lib/utils/keys/key.rb', line 330

def to_aes_key
  return Digest::SHA256.digest( to_binary() )
end

#to_binaryByte

Return the un-printable binary bytes representation of this key. If you store 128 bits it will produce 22 characters because 128 divide by 6 is 21 characters and a remainder of two (2) bits.

The re-conversion of the 22 characters will now produce 132 bits which is different from the original 128 bits.

Returns:

  • (Byte)

    a non-printable binary string of eight (8) bit bytes which can be used as input to both digest and symmetric cipher functions.



295
296
297
# File 'lib/utils/keys/key.rb', line 295

def to_binary()
  return [ to_s ].pack("B*")
end

#to_char64String

Convert this keys bit value into a printable character set that is suitable for storing in multiple places such as environment variables and INI files.

Returns:

  • (String)

    printable characters from a set of 62 alpha-numerics plus an @ symbol and a percent % sign.

Raises:

  • ArgumentError If the bit value string for this key is nil. Or if the bit string length is not a multiple of six. Or if it contains any character that is not a 1 or 0.



278
279
280
281
# File 'lib/utils/keys/key.rb', line 278

def to_char64()
  assert_non_nil_bits
  return Key64.from_bits( @bit_string )
end

#to_sString

When a key is initialized, it is internally represented as a string of ones and zeroes primarily for simplicity and can be visualized as bits that are either off or on.

Once internalized a key can also be represented as

  • a sequence of base64 (or radix64) characters (1 per 6 bits)

  • a binary string suitable for encryption (1 byte per 8 bits)

  • a 256bit encryption key from Digest(ing) the binary form

Returns:

  • (String)

    a string of literally ones and zeroes that represent the sequence of bits making up this key.



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/utils/keys/key.rb', line 234

def to_s

  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)
  ## Write duplicate ALIAS method called ==> to_bits() <== (bits and pieces)

  ## ---------------------------------------------
  ## +++++++++ WARNING ++++++++
  ## ---------------------------------------------
  ##
  ##      to_s does not need 2b called
  ##      So both the below print the same.
  ##
  ##      So YOU MUST KEEP the to_s method until a proper test suite is in place.
  ##      So YOU MUST KEEP the to_s method until a proper test suite is in place.
  ##
  ##        puts "#{the_key}"
  ##        puts "#{the_key.to_s}"
  ##
  ##      So YOU MUST KEEP the to_s method until a proper test suite is in place.
  ##      So YOU MUST KEEP the to_s method until a proper test suite is in place.
  ##
  ## ---------------------------------------------

  return @bit_string
end