Class: SafeDb::Key
- Inherits:
-
Object
- Object
- SafeDb::Key
- 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
-
.from_binary(binary_text) ⇒ SafeDb::Key
Return a key represented by the parameter binary string.
-
.from_char64(char64_string) ⇒ SafeDb::Key
Return the key represented by the parameter sequence of base64 characters.
-
.from_radix64(radix64_string) ⇒ SafeDb::Key
Convert a string of Radix64 characters into a key.
-
.from_random ⇒ SafeDb::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.
-
.to_binary_from_bit_string(bit_string_to_convert) ⇒ Byte
Return the un-printable binary bytes representation of this key.
- .to_random_bits(the_byte_length) ⇒ Object
Instance Method Summary collapse
-
#can_decrypt_key(key_iv_ciphertext) ⇒ Boolean
Return true if this key object can decrypt the parameter ciphertext that represents a key.
-
#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.
-
#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.
-
#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.
-
#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.
-
#initialize(the_bit_string) ⇒ Key
constructor
Initialize a key object from a bit string of ones and zeroes provided in the parameter string.
-
#to_384_bit_key ⇒ SafeDb::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.
-
#to_aes_key ⇒ String
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.
-
#to_binary ⇒ Byte
Return the un-printable binary bytes representation of this key.
-
#to_char64 ⇒ String
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.
-
#to_s ⇒ String
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.
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 |
| -------- | ------------ | -------------------------------- |
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.
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.
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.
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_random ⇒ SafeDb::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.
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.
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
.
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.}" } 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.
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.
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.
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.
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_key ⇒ SafeDb::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
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_key ⇒ String
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.
330 331 332 |
# File 'lib/utils/keys/key.rb', line 330 def to_aes_key return Digest::SHA256.digest( to_binary() ) end |
#to_binary ⇒ 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.
295 296 297 |
# File 'lib/utils/keys/key.rb', line 295 def to_binary() return [ to_s ].pack("B*") end |
#to_char64 ⇒ String
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.
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_s ⇒ String
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
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 |