class SafeDb::KeyPbkdf2
PBKDF2 is a powerful leading Key Derivation Function (KDF) that exists to convert low entropy human created passwords into a high entropy key that is computationally infeasible to acquire through brute force.
As human generated passwords have a relatively small key space, key derivation functions must be slow to compute with any implementation.
PBKDF2 offers an iteration count that configures the number of iterations performed to create the key.
One million (1,000,000) should be the iteration count's lower bound.
Upgrading the OpenSSL pbkdf2_hmac Behaviour¶ ↑
As soon as the new Ruby and OpenSSL libraries become commonplace this class should be upgraded to use the new and improved {OpenSSL::KDF.pbkdf2_hmac} behaviour rather than {OpenSSL::PKCS5.pbkdf2_hmac}.
The difficulty is in detecting the operating system's C libraries that are directly accessed for OpenSSL functionality. If the distinction can be made accurately, those with newer libraries can reap the benefits immediately.
PBKDF2 Cost Iteration Timings on an Intel i-5 Laptop¶ ↑
An IBM ThinkPad was used to generate the timings.
Memory RAM ~> 15GiB Processors ~> Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
The timing results show that a prudent value is somewhere between one hundred thousand and ten million iterations.
9.6 seconds for 10,000,000 ten million iterations 0.96 seconds for 1,000,000 one million iterations 0.096 seconds for 100,000 one hundred thousand iterations
Open
key sets iteration counts for PBKDF2 in hexadecimal and a valid range starts at 1 and counts up in chunks of a hundred thousand (100,000).
1 ~> 100,000 5 ~> 500,000 10 ~> 1,000,000 16 ~> 16,000,000 256 ~> 256,000,000
The maximum iteration multiplier allowed is 16,384.
Constants
- ONE_HUNDRED_THOUSAND
The quantity used to multiply the iteration multiplier by to gain the iteration count.
- PBKDF2_EXPORT_BIT_LENGTH
For a 384 bit digest the key length is 48 bytes and the bit length is 384 bits.
- PBKDF2_EXPORT_KEY_LENGTH
Documentation for this algorithm says this about the key length.
Make the key length larger than or equal to the output length of the underlying digest function, otherwise an attacker could simply try to brute-force the key.
According to PKCS#5, security is limited by the output length of the underlying digest function, i.e. security is not improved if a key length strictly larger than the digest output length is chosen.
Therefore, when using PKCS5 for password storage, it suffices to store values equal to the digest output length, nothing is gained by storing larger values.
- PBKDF2_ITERATION_MULTIPLIER
One million iterations is necessary due to the growth of GPU driven cloud based computing power that is curently being honed by mining BitCoin and training neural networks.
PBKDF2 Cost Iteration Timings on an Intel i-5 Laptop¶ ↑
An IBM ThinkPad was used to generate the timings.
Memory RAM ~> 15GiB Processors ~> Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz
The timing results show that a prudent value is somewhere between one hundred thousand and ten million iterations.
Open
key sets iteration counts for PBKDF2 in hexadecimal and a valid range starts at 1 and counts up in chunks of a hundred thousand (100,000).1 ~> 100,000 5 ~> 500,000 10 ~> 1,000,000 16 ~> 16,000,000 256 ~> 256,000,000
- PBKDF2_SALT_LENGTH_BYTES
The documented recommended salt length in bytes for the PBKDF2 algorithm is between 16 and 24 bytes. The setting here is at the upper bound of that range.
Public Class Methods
Generate
a 128 bit binary key from the PBKDF2 password derivation function. The most important input to this function is the human generated key. The best responsibly sourced key with at least 95% entropy will contain about 40 characters spread randomly over the set of 95 typable characters.
Aside from the human password the other inputs are
-
a base64 encoded randomly generated salt of 16 to 24 bytes
-
an iteration count of at least 1 million (due to GPU advances)
-
an output key length that is at least 16 bytes (128 bits)
-
a digest algorithm implementation (we use SHA512K)
The {Key} returned by this method encapsulates the derived key of the byte (bit) length specified.
PBKDF2 Output Key Length Note
Documentation for this algorithm says this about the key length.
Typically, the key length should be larger than or equal to the output length of the underlying digest function, otherwise an attacker could simply try to brute-force the key. According to PKCS#5, security is limited by the output length of the underlying digest function, i.e. security is not improved if a key length strictly larger than the digest output length is chosen. Therefore, when using PKCS5 for password storage, it suffices to store values equal to the digest output length, nothing is gained by storing larger values.
Upgrading the OpenSSL pbkdf2_hmac Behaviour
As soon as the new Ruby and OpenSSL libraries become commonplace this class should be upgraded to use the new and improved {OpenSSL::KDF.pbkdf2_hmac} behaviour rather than {OpenSSL::PKCS5.pbkdf2_hmac}.
The difficulty is in detecting the operating system's C libraries that are directly accessed for OpenSSL functionality. If the distinction can be made accurately, those with newer libraries can reap the benefits immediately.
@param human_secret [String]
a robust human generated password with as much entropy as can be mustered. Remember that 40 characters spread randomly over the key space of about 95 characters and not relating to any dictionary word or name is the way to generate a powerful key that has embedded a near 100% entropy rating.
@param pbkdf2_string [String]
this is a relatively small iteration count multiplier separated from the main salt characters by a plus sign. The salt characters will consist of 32 base64 characters which can be stored and fed into the {generate_key}. The salt string presented here must have either been recently generated by {generate_pbkdf2salt} or read from a persistence store and resubmitted here in order to regenerate the same key.
@return [Key]
a key holder containing the key which can then be accessed via many different formats. The {Key} returned by this method encapsulates the derived key with the specified byte count.
# File lib/utils/kdfs/pbkdf2.rb, line 214 def self.generate_key human_secret, pbkdf2_string KeyError.not_new pbkdf2_string, "PBKDF2 Algorithm Salt" multiplier = pbkdf2_string.split("+")[0].to_i pbkdf2_salt = pbkdf2_string.split("+")[1] mult_msg = "Iteration multiplier is an integer from 1 to 16,384 not [#{multiplier}]." raise ArgumentError, mult_msg_msg unless( multiplier > 0 && multiplier < 16385 ) iteration_count = multiplier * ONE_HUNDRED_THOUSAND binary_salt = Key.to_binary_from_bit_string( Key64.to_bits( pbkdf2_salt ) ) err_msg = "Expected salt of #{PBKDF2_SALT_LENGTH_BYTES} bytes not #{binary_salt.length}." raise ArgumentError, err_msg unless binary_salt.length == PBKDF2_SALT_LENGTH_BYTES pbkdf2_key = OpenSSL::PKCS5.pbkdf2_hmac( human_secret, binary_salt, iteration_count, PBKDF2_EXPORT_KEY_LENGTH, OpenSSL::Digest::SHA384.new ) return Key.from_binary( pbkdf2_key ) end
Return a random cryptographic salt generated from twenty-four random bytes produced by a secure random number generator. The returned salt is primarily a Base64 encoded string that can be stored and then passed to the {KeyPbkdf2.generate_key} method.
+ ------------ + -------- + ------------ + ------------- + | | Bits | Bytes | Base64 | | ------------ | -------- | ------------ | ------------- | | PBKDF2 Salt | 192 Bits | 24 bytes | 32 characters | + ------------ + -------- + ------------ + ------------- +
The leading part of the character sequence indicates the length of the salt in chunks of 100,000 and is plus sign separated.
42+12345678abcdefgh12345678ABCDEFGH ~> 4,200,000 iterations 9+12345678abcdefgh12345678ABCDEFGH ~> 900,000 iterations 100+12345678abcdefgh12345678ABCDEFGH ~> 10,000,000 iterations
Note that the generate key method will convert the trailing 32 base64 characters back into a 24 byte binary string.
@return [String]
a relatively small iteration count multiplier separated from the main salt characters by a plus sign. The salt characters will consist of 32 base64 characters which can be stored and fed into the {generate_key}. These 32 characters are a representation of the twenty-four (24) randomly and securely generated bytes.
# File lib/utils/kdfs/pbkdf2.rb, line 144 def self.generate_pbkdf2_salt pbkdf2_salt = Key64.from_bits( Key.to_random_bits( PBKDF2_SALT_LENGTH_BYTES ) ) return "#{PBKDF2_ITERATION_MULTIPLIER}+#{pbkdf2_salt}" end